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.
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.
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 openEach 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:
- Issuer authentication: read the issuer from
/.well-known/agent-commerceon the gateway origin that serves MCP and A2A. An operator may also publish it elsewhere. - Request schemes:
agent-commerce/request/v1only. - Carriage: the extension above over HTTP, MCP and A2A.
- 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.
- Payer binding: verification checks the EIP-3009
fromor Permit2 owner. - Submission event: the gateway calls the facilitator's
settleonce per admitted payment attempt, before the backend underupfrontor after it succeeds underauthorization. The replay key blocks resubmission of a settled or unresolved payment; a confirmed unspent authorization may be retried. - Execution deadline: EIP-3009
validBeforeor Permit2deadline, 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 withvalidUntil. 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.
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.