x402 Payments (HTTP 402 over XChain)
Wire specification for charging HTTP clients — typically AI agents — in XChain tokens, using an HTTP 402 challenge → on-chain payment → retry loop. The flow is shaped like the x402 convention (status 402, an accepts array, an X-Payment request header, an X-Payment-Response response header) but settles natively on XChain rails; it is not compatible with stablecoin x402 facilitators. Reference implementation: xchain-sdk (X402Gateway server side, X402Client client side).
This is an off-chain HTTP convention over existing on-chain actions. It introduces no new ACTION types and no consensus changes.
Flow
Client Server (gateway) Chain
| GET /resource | |
|------------------------------->| |
| 402 {accepts:[...]} | |
|<-------------------------------| |
| SEND tick/amount -> payTo, MEMO = invoice nonce |
|---------------------------------------------------------------->
| GET /resource X-Payment: <proof> |
|------------------------------->| verify via explorer ----------|
| 200 + X-Payment-Response | (mempool 0-conf / indexed) |
|<-------------------------------| |
Challenge (HTTP 402 response body)
{
"x402Version": 1,
"error": "Payment required",
"resource": "/api/report",
"accepts": [
{ "scheme": "xchain-send", "coin": "DOGE", "tick": "MYTOKEN",
"amount": "5", "payTo": "D...", "invoice": "<32 lowercase hex chars>",
"expiresAt": 1765500000000, "minConfirmations": 0 },
{ "scheme": "xchain-dispenser", "coin": "DOGE", "holdTick": "ACCESS",
"minBalance": "1", "dispenserIndex": 4201, "dispenserAddress": "D..." },
{ "scheme": "xchain-deposit", "coin": "DOGE", "tick": "MYTOKEN",
"depositAddress": "D...", "pricePerCall": "1" }
]
}
amount,minBalance,pricePerCall— arbitrary-precision decimal strings (plain decimal; no exponents).invoice— a server-generated single-use nonce (16 random bytes, 32 lowercase hex chars). The payer embeds it verbatim as the SENDMEMO.expiresAt— Unix milliseconds; servers honor a small grace window (reference: 10 s).minConfirmations—0means the server will grant provisionally on mempool visibility (see Trust model);1+requires indexed, valid rows.
Proof (X-Payment request header)
X-Payment: base64url(JSON):
{ "x402Version": 1, "scheme": "xchain-send", "coin": "DOGE",
"txid": "…", "invoice": "<nonce>", "payer": "<payer address>" }
xchain-dispenser and xchain-deposit proofs carry only {x402Version, scheme, coin, payer} — the server checks chain state (balance / confirmed deposits minus its spend ledger) directly.
Receipt (X-Payment-Response response header)
base64url(JSON): {x402Version, status, txid, blockIndex, provisional, remaining?} where status ∈ confirmed | provisional_0conf | dispenser_verified | deposit_debited.
Verification rules (normative for servers)
A SEND satisfies an xchain-send invoice iff all of:
- The on-chain
sourceequals the proof’spayer(front-runners who copy a mempool memo cannot claim someone else’s payment). - An output of the SEND pays
payTowithtickequal (ticks compare uppercased) andamount >=the invoice amount, compared as exact decimals — never floats, never with an epsilon. - That same output’s
MEMOequals the invoice nonce, strict equality after trimming (SEND v3 carries per-group memos — the memo of the matched output group counts). - The invoice is unexpired and has never been claimed. Claims are atomic and single-use.
- For
minConfirmations >= 1: the row is indexed with statusvalid. For0: a decoder-mempool row suffices, but the grant is provisional.
Multi-output SEND (v1–v3) must be parsed with tick/amount/destination/memo kept paired per output — matching payTo against one output and an amount from another is the classic implementation bug; field counts are strict per version (a | smuggled into a memo changes the count and the parse must fail).
Trust model for 0-conf
Decoder mempool rows are pre-validation: the decoder records whatever parses out of an unconfirmed transaction; the indexer can still reject the action at confirmation (e.g. insufficient token balance). A 0-conf grant is therefore weaker than coin-level 0-conf and must be treated as provisional:
- The reference gateway re-checks provisional grants against indexed rows and either promotes them to
confirmedor marks them failed after a window (default 10 minutes), notifying the operator. - Serve only low-value or revocable resources at
minConfirmations: 0; for anything irrevocable (e.g. handing over a decryption key), require1+— and on BTC consider more, per the value at stake. Reorg re-verification beyond this is out of scope for v1.
Replay and state rules
- Invoice nonces are single-use, claimed under mutual exclusion; the claiming
txidis recorded. - The deposit scheme’s spend ledger debits under per-payer mutual exclusion. A file-backed store is sound only for a single-node server; multi-node deployments must use a shared atomic store.
- A corrupt invoice/ledger state file must fail closed (block verification), never silently reset.
Endpoints used
- Mempool (0-conf):
GET /{COIN}/api/mempool/{payTo}/address— rows carry the raw decoded action string indata; the verifier parses it. - Confirmed:
GET /{COIN}/api/sends/{payTo}/destination— rows carrysource,tick,amount,memo,status. - Dispenser scheme:
GET /{COIN}/api/balances/{payer}.
See also: Token_Gated_Content.md (selling decryption keys through this flow), Error_Codes.md, and the plain-language guide at …/ai-agents/Charging_Agents.md.