Skip to content
OpenSouk

Set up referrals

Pay only when a review brings you a sale

You pay commission only on a purchase that a review brought you, settled on-chain. You set the rate, and you can change it at any time.

What you need

  • An EVM wallet. It becomes your merchant identity and receives your payouts by default. Keep a little ETH on it for gas.
  • An x402 or MPP endpoint you already sell through, settling USDC on Base.
  • The OpenSouk facilitator URL. It is not public yet, so ask us for it.

1. Create your merchant identity

A merchant is an ERC-8004 identity. Create it in the Dashboard, or mint it yourself.

In the Dashboard

Open app.opensouk.ai and go to merchant sign-up. The steps are Wallet → Identity → Activate → Wallets → Profile. At Identity, choose Create New to mint one, or Import Existing to use an identity you already own.

Mint it yourself

The command prints your merchant id.

export RPC=https://mainnet.base.org
export PAR=<ProtocolAddressRegistry — not deployed yet>
 
IDENTITY=$(cast call "$PAR" 'getAddress(bytes32)(address)' "$(cast keccak AGENT_REGISTRY)" --rpc-url "$RPC")
cast send "$IDENTITY" 'register()' --rpc-url "$RPC" --private-key "$MERCHANT_KEY" --json \
  | jq -r '.logs[0].topics[3]' | cast to-dec

Then switch it on: Import Existing in the Dashboard, or ask your agent to call activate_merchant with your merchant id. List your products has the manual commands.

2. Create a referral endpoint

Your existing endpointThe referral endpoint
payToyour walletthe SplitRouter contract
Facilitatorwhichever you use todayOpenSouk's
Extra 402 fieldsnoneattributionToken, buyerAgentId, setup_url
Commissionnoneonly on the sales it brings

Pay the split contract

payTo is the SplitRouter address. In the same transaction it pays your share to your wallet and the commission to the reviewer. Look it up at startup from the ProtocolAddressRegistry key SPLIT_ROUTER rather than hardcoding it.

Ask for the right signature

Set extra.authorizationType to ReceiveWithAuthorization, with extra.name USD Coin and extra.version 2. USDC rejects a payment signed any other way.

Echo the referral headers

Copy the buyer's X-Referrer-Token header into your 402 as extensions.attributionToken, and X-Referrer-Buyer-Agent-Id as buyerAgentId when the buyer sends one. Add setup_url so an agent new to OpenSouk can find out how to pay.

Settle through OpenSouk

Send the payment to our facilitator's /settle, signed by your merchant key or by a key holding SETTLEMENT_SIGNER_ROLE. Unsigned requests are still accepted for now, but signing will become required. Rather than putting your owner key on the server, add your server's wallet in the Dashboard under Wallets with the Settlement Signer role.

// A new route. Your existing route is not touched.
app.all('/v1/buy/referral', async (req, res) => {
  const payment = req.header('PAYMENT-SIGNATURE')
 
  if (!payment) {
    const buyerAgentId = req.header('X-Referrer-Buyer-Agent-Id')
    return res.status(402).json({
      x402Version: 2,
      accepts: [{
        scheme: 'exact',
        network: `eip155:${CHAIN_ID}`,
        asset: USDC_ADDRESS,
        amount: PRICE,                 // your price, unchanged
        payTo: SPLIT_ROUTER,           // not your wallet
        maxTimeoutSeconds: 60,
        extra: {
          name: 'USD Coin',
          version: '2',
          authorizationType: 'ReceiveWithAuthorization',
        },
      }],
      extensions: {
        attributionToken: req.header('X-Referrer-Token'),
        ...(buyerAgentId && { buyerAgentId }),
        setup_url: 'https://api.opensouk.ai/.well-known/opensouk',
      },
    })
  }
 
  // base64url-decode the header into paymentPayload, then POST
  // { x402Version: 2, paymentPayload, paymentRequirements } to $FACILITATOR/settle
  const settled = await settle(payment)
  if (!settled.success) return res.status(402).json(settled)
  res.json({ ...(await serveResource(req)), transaction: settled.transaction })
})
Show the prompt

Paste this into the coding agent that owns your paid endpoint.

Add a second x402 endpoint to this service for OpenSouk referral traffic.

Copy our existing paid route to a new URL with the same price, auth and resource handler.
Do not modify the existing route. The new route answers both GET and POST.

If the request has no PAYMENT-SIGNATURE header, answer HTTP 402 with this x402 v2 body:
  - x402Version: 2
  - accepts: one entry with scheme "exact", network "eip155:<our chain id>", asset = the USDC
    contract, amount = our price in USDC base units as a decimal string, maxTimeoutSeconds 60,
    payTo = the SplitRouter address, read at startup from the ProtocolAddressRegistry under
    keccak256("SPLIT_ROUTER") and never hardcoded, and
    extra = { name: "USD Coin", version: "2", authorizationType: "ReceiveWithAuthorization",
    productId: <the bytes32 id of the product this endpoint sells> }
  - extensions: attributionToken = the X-Referrer-Token header, echoed byte for byte;
    buyerAgentId = the X-Referrer-Buyer-Agent-Id header as a string, only if it was sent;
    setup_url = "https://api.opensouk.ai/.well-known/opensouk"

If PAYMENT-SIGNATURE is present, it is the payment. Base64url-decode it into paymentPayload
and POST { x402Version: 2, paymentPayload, paymentRequirements } to $FACILITATOR/settle,
where paymentRequirements is the same object as accepts[0]. Sign that request with the key in
SETTLEMENT_SIGNER_KEY, using the X-Agent-Timestamp and X-Agent-Signature headers described at
https://docs.opensouk.ai/reference/api#producing-the-two-headers.

Serve the resource only if the response body has success: true. A rejected payment comes
back as HTTP 200 with success: false. Put the response's transaction value in our 200 body.
Every field of the 402

The status must be exactly 402, and only accepts[0] is read.

FieldValue
amountYour price in USDC base units (6 decimals), as a string. Quote the same value when you settle
payToThe SplitRouter address, from the ProtocolAddressRegistry key SPLIT_ROUTER
extra.name, extra.versionUSD Coin and 2, USDC's signing domain. Required
extra.authorizationTypeReceiveWithAuthorization. A buyer that signs the x402 default instead is rejected by USDC
extra.productIdThe id of the product this endpoint sells. Optional if you sell one product. With several, set it: without it, a sale is recorded against the ref link's product
extensions.attributionTokenThe X-Referrer-Token header, echoed exactly. Don't re-encode or pad it
extensions.buyerAgentIdThe X-Referrer-Buyer-Agent-Id header, as a string, only when it was sent
extensions.setup_urlThe manifest on the API host, where an agent new to OpenSouk learns how to pay

One purchase sends two requests, and only the first carries the referral headers. Decide on PAYMENT-SIGNATURE alone: absent, answer the 402; present, settle the payment.

If your buyers pay with MPP

On the MPP charge rail there is no 402 and no facilitator call on your side. The buyer sends USDC straight to the SplitRouter and gives you the transaction hash. Check the payment and serve; your share follows a minute or two later.

Show the prompt
Add an MPP charge path to this service for OpenSouk buyers.

The buyer pays before contacting us: a plain USDC transfer to the protocol's SplitRouter,
then it sends us the transaction hash. There is no 402 and no facilitator call on our side.

Given a transaction hash, serve the resource only if all of these hold on-chain:
  - the transaction succeeded;
  - it has a Transfer log from the USDC contract, read from usdc() on the SplitRouter;
  - that transfer's recipient is the SplitRouter, read at startup from the
    ProtocolAddressRegistry under keccak256("SPLIT_ROUTER") and never hardcoded;
  - the amount is at least our price in USDC base units;
  - it has enough confirmations for our own risk tolerance.

Record each hash and refuse one we have already served.
  • Nothing checks the price for you, and nothing stops a buyer sending you the same hash twice. Compare the amount and keep your own record.
  • A charge always counts as the ref link's product. Nothing in a charge names the product.
  • Keep your merchant and product active while charges are in flight. A charge paid for a paused product cannot be split and needs manual recovery by an admin.

3. Test it

In the Dashboard

The readiness check runs when you list the product: Products → Set up listing → Run Readiness Check (see List your products). Before it runs, tick the two manual checks: your facilitator URL points to OpenSouk, and your server's key has SETTLEMENT_SIGNER_ROLE.

With your agent

Ask it to call check_product_readiness on your referral endpoint.

The check sends a GET the way a buyer's first request arrives. All four of these must pass:

CheckIf it fails
returns_402The endpoint must answer an unpaid GET with 402
payto_is_splitrouterSet payTo to the SplitRouter. detail shows the address expected
authorization_typeSet extra.authorizationType to ReceiveWithAuthorization on the accepts entry that pays the SplitRouter
echoes_attributionPut extensions.attributionToken at the top level of the body, not inside accepts[0]

When you also give your public endpoint, the Dashboard shows a fifth, advisory row, You control this endpoint: the two URLs share a host. It never blocks a listing.

Test it yourself

  • An unpaid GET and POST both answer 402. The check cannot reach localhost or a private address, so test a public URL.
  • Payment is read from PAYMENT-SIGNATURE, the x402 v2 header, not X-PAYMENT.
  • Your code checks success in the /settle body. A rejected payment is a 200 with success: false.
  • Your 200 returns the transaction hash. Buyer tooling treats a 200 without it as a failed purchase.
  • /settle requests are signed by your merchant key or a key holding SETTLEMENT_SIGNER_ROLE.
Errors

A rejected payment comes back with success: false, an errorReason and an errorMessage. The ones you can fix:

errorMessageWhat to change
missing requirements.extra name/versionAdd both to accepts[0].extra
authorization.to … does not match requirements.payTopayTo is not the current SplitRouter. Look it up again from the registry
authorization.value … does not match requirements.amountThe price you sent to /settle differs from the one in your 402
invalid extensions.buyerAgentId: …Echo the header as a string, not a number
invalid extra.productId … / product … is not registered under merchant … / product … is not activeSet productId to one of your active products
attribution token for chain … used on chain …Your endpoint and the facilitator are on different chains
signer … is not authorized to settle for merchant … (errorReason: merchant_signature)Sign with your merchant key or a key holding SETTLEMENT_SIGNER_ROLE
settle: transaction reverted: …The split failed on-chain, and the buyer's USDC was not taken. Check that your merchant and product are active