How a gated request actually plays out on the wire, and what the facilitator validates before it accepts a payment.
Server Buyer (browser / CLI)
│ │
│ ◄── GET /odata/v4/prices/Quotes ─────│
│ │
│ ──── 402 + accepts[0] (price, payTo) ►│
│ │
│ ┌────────┴────────┐
│ │ build tx │
│ │ sign via CIP-30 │
│ │ base64-encode │
│ └────────┬────────┘
│ │
│ ◄── GET /odata/v4/prices/Quotes ─────│
│ PAYMENT-SIGNATURE: <base64> │
│ │
┌──────┴──────┐ │
│ 6 checks │ │
│ + submit │ │
│ + poll │ │
│ + onAccepted│ │
└──────┬──────┘ │
│ │
│ ─────── 200 OK + data ───────────────►│
│ X-PAYMENT-RESPONSE: <base64> │
The header value is the base64 encoding of:
{
"x402Version": 2,
"scheme": "exact",
"network": "cardano:preprod",
"payload": {
"transaction": "<base64-CBOR of signed tx>",
"nonce": "<txHash>#<outputIndex>"
}
}The nonce references a UTxO that must also appear as an input of the payment tx. Once the tx settles, that UTxO is consumed; replay defense is on-chain, no DB table needed.
Every accepted payment passes all six (in order):
| # | Check | Code on failure |
|---|---|---|
| 1 | Network matches requirements | network_mismatch |
| 2 | At least one output to payTo |
wrong_recipient |
| 3 | Sum of payTo outputs for asset ≥ required | insufficient_amount |
| 4 | Exact policy + asset-name match | wrong_asset |
| 5 | Nonce UTxO referenced as tx input and unspent on chain | nonce_not_referenced / replay_detected |
| 6 | Validity-range upper bound still in future | expired_ttl |
Plus a sanity guard: tx has at least one vkey witness → unsigned_transaction.
When submit succeeds but the tx is not indexed within the settle poll
budget (~60s), the server answers 402 with pending: true + the tx
hash. The buyer's contract is to re-send the same PAYMENT-SIGNATURE
(never pay again); x402Fetch / x402Axios do this automatically.
A re-send that arrives after the tx got indexed would trip check 5:
the nonce is now spent, by this very payment. To avoid rejecting a
paid buyer with replay_detected forever, the facilitator applies a
fallback: if the envelope's own tx is on chain and its server-observed
blockTime is within pendingGraceMs (default 5 minutes, 0
disables), the request is accepted. Inside that window the same
envelope is re-servable, an implicit mini-grant, deliberately
equivalent in spirit to the X402Grants feature. Consequence for
consumers: onAccepted (and the receipts INSERT, which dedupes on
txHash) can fire more than once per payment inside the window, so
audit callbacks must be idempotent on claim.txHash.
Rejected requests get 402 with an error field of the form "<base> (<code>): <reason>" so clients can parse the code without breaking wire format.
The full list of code values lives in facilitator-protocol.md.
x402 is the dormant HTTP 402 Payment Required status code, revived. Servers respond 402 with a machine-readable body describing the price, asset, and recipient. Clients build, sign, and submit a payment, then retry the request with a PAYMENT-SIGNATURE header. Settlement happens on-chain.
The original x402 spec is Coinbase / EVM-flavoured. The Cardano-x402-v2 spec adapts it to Cardano's UTxO model: cardano:mainnet|preprod|preview (colon-separated) network strings, ${policyId}.${assetNameHex} asset format, and an exact / client | facilitator | server module split. This library is a from-scratch v2 implementation in TypeScript.