Make your 402 self-describing
Declare the attribution contract in the response body
A referral-mode 402 carries three extension fields. They are not what attributes the payment —
the facilitator reads attribution off the buyer's payload, and the protocol's own tooling
fills those in from the token it already holds. What the fields do is declare what a payment to
you must carry, so an agent with no prior knowledge of this protocol can read your response and
comply — as long as it signs the ERC-3009 authorisation itself rather than delegating to a
stock x402 SDK, whose EIP-712 type table is pinned to TransferWithAuthorization. This rail
needs ReceiveWithAuthorization; see Rails. They are also what our
readiness probe asserts. The token itself, and what it binds, is on
The attribution token.
What you will build
One extensions object on the 402 you already return in referral mode:
{
"extensions": {
"attributionToken": "eyJ2Ijo0LCJy…",
"buyerAgentId": "7",
"setup_url": "https://api.opensouk.ai/.well-known/referrer-agent"
}
}It sits at the top level of the 402 body, beside x402Version and accepts — not inside
accepts[0]. The full response shape is in
Accept agent payments over x402.
The three fields
| Field | Value | Required for a payment to settle |
|---|---|---|
attributionToken | The X-Referrer-Token request header, echoed verbatim | The buyer must send it on the payload; your echo is a declaration, not the source |
buyerAgentId | The X-Referrer-Buyer-Agent-Id request header, as a string | No. Absent means an agent-less buyer |
setup_url | The protocol API's /.well-known/referrer-agent URL | No |
Echo, do not re-encode. attributionToken is unpadded base64url and the facilitator decodes it
strictly. Adding padding, switching to standard base64 or URL-escaping it produces a value a buyer
copying your declaration cannot spend.
buyerAgentId is a string, and only present when the header was. A number is rejected by the
facilitator when a buyer copies it into the payload, and an absent header echoed as "" is read as
an agent-less buy. Agent id 0 is a real identity, so never treat a 0 as "not sent".
setup_url points at the API, not at the facilitator. The manifest is served by the protocol's
API host at api.opensouk.ai; the facilitator serves /verify, /settle, /supported,
/attribution/public-key and the charge route, and nothing at /.well-known. Pointing it at the
facilitator's base URL yields a 404 for the one reader it exists for — a cold agent that has just
failed to pay and is looking for how to onboard.
Prompt mode
Show the prompt
Add the referral extensions object to this service's referral-mode 402 on Base mainnet.
At the TOP LEVEL of the 402 body (a sibling of x402Version and accepts, not inside
accepts[0]), add an "extensions" object with:
- attributionToken: the X-Referrer-Token request header value, echoed byte for byte. Do
not re-encode, pad, or URL-escape it.
- buyerAgentId: the X-Referrer-Buyer-Agent-Id request header value as a JSON STRING,
included only when that header was actually sent. Never coerce it to a number, and
never treat the string "0" as absent.
- setup_url: the protocol API's https://<api host>/.well-known/referrer-agent URL. This
is the API host, not the facilitator's base URL.
Leave the rest of the 402 unchanged.Manual mode
Read the two request headers
const attributionToken = req.header('X-Referrer-Token')
const buyerAgentId = req.header('X-Referrer-Buyer-Agent-Id')X-Referrer-Token is the value you echo, not a switch to read: a referral-dedicated endpoint
answers this way for every request. (An endpoint serving both modes from one URL does use the
header's presence to choose, which is one of the reasons the dedicated one is simpler.)
X-Referrer-Buyer-Agent-Id is informational, and its absence is meaningful rather than an error.
Build the object with the optional field omitted
const extensions = {
attributionToken,
...(buyerAgentId ? { buyerAgentId } : {}),
setup_url: SETUP_URL,
}Both an omitted key and an empty string settle; the omission is the honest declaration that this buyer sent no identity.
Configure setup_url once
Store it as configuration, alongside your facilitator URL. Both are deployment settings.
export SETUP_URL=https://api.opensouk.ai/.well-known/referrer-agent
curl -s "$SETUP_URL" | head -c 200The manifest is one JSON document, with a .txt prose companion at the same path. It carries the
per-chain attestation public key an agent verifies a token against, the contract addresses per
chain, and a native-integration recipe.
One line of that recipe is stale. It tells an agent to POST the payment in an X-PAYMENT
header, the x402 v1 name. The protocol's buyer tooling sends PAYMENT-SIGNATURE, and a merchant
that reads only X-PAYMENT never sees a payment from a protocol buyer.
Check your own response
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'X-Referrer-Token: readiness-probe' https://rpc.example.com/v1/mainnet
curl -s -H 'X-Referrer-Token: readiness-probe' \
https://rpc.example.com/v1/mainnet | jq '{x402Version, extensions, payTo: .accepts[0].payTo}'readiness-probe is the literal value our own probe sends, and it is not a real token — which is
the point. It exercises the mode switch and the response shape without a purchase.
Verify
The readiness probe's echoes_attribution check is what asserts this object exists. It tests for
the presence of the attributionToken key inside extensions and nothing else: an empty
string passes, and a wrong value passes. So a passing check means "the field is declared", not "the
echo is correct" — the echo is verified by the facilitator at payment time, and by reading your own
response as in the last step.
Errors and retries
| What you see | Cause |
|---|---|
echoes_attribution fails with pass: false | No extensions object, the key spelled differently, or the object nested inside accepts[0] |
A buyer reports the payment was refused with missing extensions.attributionToken | The buyer's payload carried no token. Your 402 cannot cause this and cannot fix it; the rejection itself carries setup_url |
A buyer reports invalid extensions.buyerAgentId | You echoed a number, or a re-formatted string. Echo the header text |
setup_url returns 404 | It points at the facilitator's base URL instead of the API host |
Next steps
- Accept agent payments over x402 — the whole response, and the retry that follows it
- Merchant routes — the readiness route and what each check reports
- The attribution token — what you are echoing, and what it binds