Skip to content

Latest commit

 

History

History
293 lines (258 loc) · 19.7 KB

x402.md

File metadata and controls

293 lines (258 loc) · 19.7 KB

x402

The x402 provider supports v2 exact on EVM through EIP-3009 transferWithAuthorization or, per option, Permit2, on Solana and on NEAR. Both EVM methods are supported on built-in EVM networks, with settlement demonstrated on Base Sepolia and Base. Solana has settled on devnet and mainnet. NEAR remains partial: testnet and mainnet payments used the test suite's SDK facilitator, with no hosted-facilitator settlement demonstrated. EVM requirements name the method in extra.assetTransferMethod; EIP-3009 also includes the token's EIP-712 domain name and version.

Verification checks the scheme, signature, recipient, amount, validity window, payer balance, network and asset. Local refusals use x402 error codes where available, such as invalid_network. The provider derives a replay key from the authorization, which the pipeline reserves before settlement. A successful settlement records the transaction hash, or the signature on Solana. A success that names another network, or no transaction in its family's form, unlocks nothing and leaves the payment's outcome unknown.

A Permit2 proof must name the x402 exact Permit2 proxy as spender and match the offer's token, amount and payTo witness recipient. The gateway checks these before the facilitator checks the signature, deadline, balance and Permit2 allowance. Other spenders could redirect funds and are refused as invalid_permit2_spender. A proof using the wrong method or both authorizations is unsupported_scheme: the SDK facilitator chooses Permit2 whenever that authorization is present. Since an owner's Permit2 nonce spans all tokens, the replay key uses the chain id, Permit2 address, owner and nonce.

On Solana, the buyer signs a token transfer with the facilitator's fee payer, named in extra.feePayer from /supported. The facilitator checks the fee payer, signatures, instruction layout, mint, recipient token account and amount, then simulates the transaction. The gateway matches the echoed offer's addresses case-sensitively. Its replay key uses the network and SHA-256 of the transaction message, excluding signatures, so changing signature bytes, including the fee payer's, still collides. Unconfirmed broadcasts, duplicate settlements in flight, and confirmed transactions whose transfers are not yet visible leave the outcome unknown. The payer recorded is the one verification reported: the Solana scheme's settlement example names the fee payer as payer, where x402 v2 defines the payer's wallet.

On NEAR, the buyer signs a NEP-366 delegate containing one ft_transfer of the offered amount to payTo, with one attached yoctoNEAR. The facilitator's relayer submits it and pays gas. NEAR adds no scheme-specific extra; an upfront offer still carries extra.paymentFlow. Before calling the facilitator, the gateway rejects mismatched tokens, recipients, amounts, methods or deposits, extra actions, and non-canonical base64 Borsh. The facilitator checks the signature, full-access key, nonce, delegate block window and chain state. The replay key uses the network, sender, public key including its curve, and nonce. Different delegate bytes under the same key and nonce still collide. The signature names no chain: network binding relies on the offer and facilitator's RPC. Settlement requires a successful inner ft_transfer receipt. settlement_failed without a hash leaves the outcome unknown because submission may have preceded the SDK exception; with a hash, it reports a failed inner transfer. Chain-read failures also leave no verdict; during settlement the SDK reports them before submission, releasing the attempt. The NEAR specification leaves failure reasons to the facilitator, with an empty transaction even after a submission, so any other failure is a rejection only when its reason is one the SDK returns before submitting: invalid_exact_near_*, insufficient_funds or a version, scheme or network refusal. Any other reason leaves the outcome unknown.

The signature covers neither the flow nor the resource, so the proof's accepted must match the offer: amount, asset, payTo (case-insensitive on EVM, case-sensitive on Solana and NEAR), and every extra key the gateway sent. An absent extra.paymentFlow means authorization. A proof's resource.url must equal the challenge's when present. A mismatch is invalid_payment_requirements, refused before the facilitator is called. maxTimeoutSeconds and extra keys added by the client are not compared. The facilitator receives the payload without resource, since its resource:// URL is the gateway's own identifier, and without extensions: verification and settlement do not use echoed quotes, and a Permit2 gas-sponsoring extension would activate unsupported SDK facilitator behavior.

With several options, the challenge uses resource://agent-commerce/resources/<id>. The comparison above selects the proof's offer; zero or multiple matches yield invalid_payment_requirements. A resource with its own offers accepts only proofs matching that list; resources may share offers.

x402 v1 is not accepted. A payload declaring an x402Version other than 2 is refused as invalid_x402_version on every carrier. A request carrying only the v1 X-PAYMENT header gets the 402 challenge with error: "invalid_x402_version" in PAYMENT-REQUIRED.

payments.x402.paymentFlow chooses when an x402 payment settles, and payments.mpp.paymentFlow does the same for MPP. A resource's paymentFlow overrides the setting of whichever rail serves it:

Flow Order Backend fails Settlement refused after backend
authorization (x402 default) verify, reserve, call backend, settle no settlement; attempt recorded as rejected response withheld; 402
upfront (MPP default) verify, reserve, settle, call backend payment stays settled; receipt records failed delivery not applicable

The gateway omits extra.paymentFlow for the authorization flow. It sets extra.paymentFlow: "upfront" for the other flow, as required by x402 v2. The gateway verifies the proof before calling the backend in either flow. For upfront, it adds a read-only check before the x402 v2 settle → resource sequence. Under authorization, the backend may finish work before settlement. A refusal leaves that work unpaid; an uncertain settlement leaves its payment status unknown. In both cases, the gateway withholds the backend response. Use upfront when backend side effects must follow payment. Backend time counts toward maxTimeoutSeconds: the payment authorization must remain valid at settlement.

After a backend failure under authorization, a refused settlement or an unsubmitted NEAR settlement, the client can retry the unspent payload and reclaim the rejected attempt. On-chain replay checks prevent double settlement. MPP credentials are single-use per challenge, so a retry returns 402 PAYMENT_REPLAYED with a fresh challenge.

Unsupported: the Permit2 gas-sponsoring extensions (eip2612GasSponsoring, erc20ApprovalGasSponsoring), the upto, batch-settlement and auth-capture schemes, multi-asset routing and dynamic pricing.

Facilitator authentication supports none, bearer and cdp; config rejects other types. Local mode requires Anvil on eip155:31337; all public networks require remote mode. Mainnet also requires the guardrails in configuration.md.

Payment identifier

Experimental and disabled by default. A resource with idempotency.redelivery declares x402's payment-identifier extension in its challenge, setting info.required from redelivery.required. To retrieve a stored response without another charge, add an id to the echoed info and reuse it with the same input. An exact resend of the proof skips verification while the response is retained, even after the proof has expired or if the wallet can no longer cover the price. A fresh proof must verify against a current offer as the same payer; it is not settled. A refused proof, including one from a wallet that cannot cover the price, gets the normal 402 without a charge. Retry after a lost response describes the full rules.

Keep one id per purchase. The SDK's appendPaymentIdentifierToExtensions generates a new ID on each call unless given an explicit id. A new ID can start a second purchase, so generate one randomly and reuse it on retries. The proof's unsigned extensions carry the id, so PAYMENT-SIGNATURE contains both the proof and retry key. With matching input and any required mandate, it retrieves the stored response until retention ends. Keep this credential out of proxy, APM and access logs; the gateway's logger redacts it.

The wire format follows x402's payment-identifier extension, with one deviation: its table specifies 409 for the same id with a different payload. The gateway fingerprints only the input digest, allowing a fresh proof from the same verified payer to retrieve the response for the same id and input, even if the network, asset or amount has changed. This supports retries with new signatures. Other input or another payer gets 409 payment_identifier_conflict after one verification that moves no funds. Invalid proofs get the normal refusal without revealing whether the id exists.

During retry-key extraction, a supplied id must contain 16 to 128 characters from A-Z, a-z, 0-9, _ and -. Invalid IDs fail with INPUT_INVALID and reason payment_identifier_invalid; a missing ID with required: true fails with reason payment_identifier_required. Both errors carry the request ID and emit payment.rejected. Verification checks the echoed info.required, rejecting a changed or missing value as extension_echo_mismatch. Additional info fields are allowed. With required: false, omitting the extension disables redelivery unless the caller supplies CanonicalRequest.retryKey directly. The facilitator receives no extensions.

ERC-8409 signed quotes

Experimental and disabled by default. With payments.x402.quote enabled, challenges whose request data can be canonicalized carry one ERC-8409 quote per EVM offer, signed with EIP-712 by a dedicated issuer key. The ERC is a draft; this implementation follows commit 0ee33230. A buyer can check the terms and authenticate the signer against the merchant's published issuer before authorizing a payment.

HTTP, MCP and A2A carry quotes in PaymentRequired.extensions["erc-8409"].info.quotes. The extension's schema describes info, as x402 requires. Each entry contains chainId and signature strings, plus a quote object whose fields are strings:

Quote field Value
issuer payments.x402.quote.issuer in /.well-known/agent-commerce
payer the zero address: a bearer quote issued before the payer is known
recipient, asset, amount the offer's payTo, asset and amount in base units
requestScheme agent-commerce/request/v1
requestHash Keccak-256 of the RFC 8785 serialization of { "input": <input>, "resource": <resource id> }
quoteId 32 random bytes, new for each signed body
validAfter, validUntil the signing time in Unix seconds, and twice maxTimeoutSeconds later

The hash uses the validated resource input: the HTTP JSON body, MCP tool arguments or A2A data part's input, with _payment, _authorization and __proto__ removed. Omitted input becomes {}. The EIP-712 domain is { name: "Signed Service Payment Quotes", version: "1", chainId }, with no verifyingContract. A buyer matches a quote to an offer by chain, asset, recipient and amount, which identify a unique offer within the challenge, then checks the signer:

import { recoverTypedDataAddress } from "viem";

const { chainId, quote, signature } =
  paymentRequired.extensions["erc-8409"].info.quotes[0];
const signer = await recoverTypedDataAddress({
  domain: {
    name: "Signed Service Payment Quotes",
    version: "1",
    chainId: Number(chainId),
  },
  types: {
    ServicePaymentQuote: [
      { name: "issuer", type: "address" },
      { name: "payer", type: "address" },
      { name: "recipient", type: "address" },
      { name: "asset", type: "address" },
      { name: "amount", type: "uint256" },
      { name: "requestScheme", type: "string" },
      { name: "requestHash", type: "bytes32" },
      { name: "quoteId", type: "bytes32" },
      { name: "validAfter", type: "uint64" },
      { name: "validUntil", type: "uint64" },
    ],
  },
  primaryType: "ServicePaymentQuote",
  message: {
    ...quote,
    amount: BigInt(quote.amount),
    validAfter: BigInt(quote.validAfter),
    validUntil: BigInt(quote.validUntil),
  },
  signature,
});
// Accept only if signer is the published issuer, requestHash is the buyer's
// own, and the window is open

Each provider caches up to 1024 signed quotes and evicts the oldest when full. Unchanged offer terms and input reuse a cached quote while at least maxTimeoutSeconds remains in its validity window, including on the paid retry. If the input cannot be represented under RFC 8785, such as a string with a lone surrogate, the challenge omits quotes. Solana and NEAR offers have no quotes, and MPP challenges carry none.

Quotes are informational: verification and settlement use the selected offer and payer authorization without reading echoed quotes. Altering an echoed quote does not change payment processing.

ERC-8409 requires transport bindings to define the following:

  1. Issuer authentication: read the issuer from /.well-known/agent-commerce on the gateway origin that serves MCP and A2A. An operator may also publish it elsewhere.
  2. Request schemes: agent-commerce/request/v1 only.
  3. Carriage: the extension above over HTTP, MCP and A2A.
  4. Bearer policy: every quote is a bearer quote and requires separate payer authorization to move funds. The gateway adds no client nonce. For non-idempotent requests, this leaves the ERC's nonce recommendation to the client and application; another payer could race a copied quote.
  5. Payer binding: verification checks the EIP-3009 from or Permit2 owner.
  6. Submission event: the gateway calls the facilitator's settle once per admitted payment attempt, before the backend under upfront or after it succeeds under authorization. The replay key blocks resubmission of a settled or unresolved payment; a confirmed unspent authorization may be retried.
  7. Execution deadline: EIP-3009 validBefore or Permit2 deadline, enforced on chain. Clients must check quote validity before authorizing and before a new submission. The gateway does not enforce the quote window or compare the payment deadline with validUntil. Quote expiry does not cancel a submitted payment.

Not implemented: payer-bound quotes; contract issuers (ERC-1271, ERC-6492, EIP-7702 delegates); quote consumption, single-use enforcement or revocation; client nonces; quotes for MPP, Solana or NEAR; signed receipts and x402's offer-receipt extension; remote signers and KMS; per-resource toggles.

Support matrix

Local settlement runs in the offline E2E suite against real contracts. Public-network runs come from the smoke tests, and the README lists their transactions.

Route Support Local settlement Public testnet Mainnet Limits
EIP-3009 on Base Sepolia and Base supported Anvil Base Sepolia, x402.org Base, PayAI
EIP-3009 on Ethereum, Polygon, Arbitrum One and Sepolia, Avalanche C-Chain supported two custom-id Anvil chains, remote facilitator not run not run needs a facilitator that lists the network; PayAI did not list Ethereum on 2026-10-10
Permit2 on the built-in EVM networks supported Anvil with the deployed Permit2 and proxy bytecode Base Sepolia, x402.org Base, PayAI the buyer approves Permit2 once, paying gas; no gas-sponsoring extensions
EVM networks declared in config partial custom-id Anvil chains the operator supplies the chain and all four asset fields
Custom token on a built-in EVM network partial Anvil (chain id 84532), remote facilitator not run not run explicit token metadata and facilitator support required
Solana supported solana-test-validator, SDK facilitator devnet, x402.org mainnet, PayAI the merchant's token account must exist; a signed transfer expires with its blockhash
NEAR partial NEAR sandbox, SDK facilitator testnet, SDK facilitator run by the suite mainnet, SDK facilitator run by the suite not run through a hosted facilitator; FastNEAR's requires an API key. The merchant needs a storage deposit, the buyer a full-access key

Every public-network x402 payment was signed by the x402 SDK's own client for its family. Other wallets and clients have not been tested.