How the gateway verifies, settles and records a paid invocation.
| Role | Key material | Where it runs |
|---|---|---|
| Buyer agent | its own payment key | the agent's machine (demo/agent in the demo) |
| Gateway | no buyer or merchant key required | merchant infrastructure |
| Facilitator | a gas-paying signer key | gateway process in local mode; remote service |
| Merchant | address configured; no key required | payTo for x402; recipient for MPP |
The gateway coordinates the protocol without taking custody. Local mode keeps the facilitator signer in the gateway process; security.md says where that mode is allowed.
The diagram shows the default authorization flow: the backend answers before
the payment settles, so a backend failure settles nothing, and a refused
settlement withholds the backend's response. Under the upfront flow,
configured per rail or per resource, settle moves above the backend call; see
x402.
buyer gateway chain / backend
│ │ │
│ 1. tools/call market_report │ │
├───────────────────────────────►│ │
│ │ resolve resource, validate input │
│ │ price 0.01 USDC → paid │
│ │ createRequirement │
│◄───────────────────────────────┤ PaymentRequiredEnvelope │
│ 2. isError + envelope │ (x402 PaymentRequirements) │
│ payment.accepts[0] │ │
│ │ │
│ 3. sign EIP-3009 authorization │ │
│ (to = merchant payTo) │ │
│ │ │
│ 4. tools/call + _payment │ │
├───────────────────────────────►│ │
│ │ verify: signature, recipient, │
│ │ amount, window, │
│ │ network, asset │
│ ├──────────────────────────────────►│ chain: balance / allowance
│ │ replayKey = H(chainId, asset, │
│ │ payer, nonce) │
│ │ reservePaymentAttempt(replayKey) │
│ │ duplicate ⇒ PAYMENT_REPLAYED │
│ │ call merchant backend │
│ ├──────────────────────────────────►│ backend: GET /api/report
│ │◄──────────────────────────────────┤ backend: 200 + body
│ │ settle │
│ ├──────────────────────────────────►│ chain: transferWithAuthorization
│ │◄──────────────────────────────────┤ chain: tx receipt
│ │ saveReceipt(txHash) │
│ │ │
│◄───────────────────────────────┤ │
│ 5. result + receipt │ │
Over HTTP, x402 sends the challenge in the 402 body and base64
PAYMENT-REQUIRED header, accepts the proof in PAYMENT-SIGNATURE, and returns
the settlement result in PAYMENT-RESPONSE. MCP and A2A carry the same proof in
the reserved _payment input field. MCP also accepts the selected rail's
_meta carrier; see Payment over MCP. A2A
also accepts the x402 extension's task metadata; see
x402 extension.
With ERC-8409 quotes enabled, the
challenge also carries a signed quote for each EVM offer.
MPP uses the same verification and replay checks, with these differences:
- The challenge is HMAC-bound to its terms and resource, and the buyer's EIP-3009 nonce must equal the challenge hash, so an authorization answers exactly one challenge.
- Verification runs local checks, then the x402 settlement provider's read-only facilitator check. Both happen before the pipeline reserves the replay key, and neither broadcasts the payment.
- Settlement uses an x402 facilitator. MPP derives the same replay key as x402, so one receipt store cannot reserve the authorization through both rails.
- MPP uses the same flow values as x402 but defaults to
upfront, as itschargeintent requires. Underauthorization, the backend must respond before the challenge expires; see MPP.
Over HTTP, MPP uses WWW-Authenticate, Authorization: Payment ... and
Payment-Receipt. See the complete HTTP header table.
The buyer signs an EIP-3009 TransferWithAuthorization whose to field is the
merchant destination. The signature covers from, to, value,
validAfter, validBefore and nonce, bound to the token contract and chain
id through the EIP-712 domain. A broadcaster can execute that transfer or
nothing; it cannot change the destination or amount.
With Permit2, the buyer signs
PermitWitnessTransferFrom, binding the token, amount, nonce, deadline,
x402 exact proxy as spender, and merchant destination as witness. The proxy
transfers only the signed amount to that destination.
So the gateway can confirm a payment without holding the funds or being able to redirect them.
| Condition | Result | Delivered? |
|---|---|---|
| no proof supplied | PaymentRequiredOutcome, 402 + envelope |
no |
| malformed proof | PAYMENT_INVALID |
no |
| bad signature | PAYMENT_INVALID |
no |
| amount mismatch | PAYMENT_INVALID |
no |
wrong recipient (to != payTo) |
PAYMENT_INVALID |
no |
| wrong network | PAYMENT_INVALID |
no |
| wrong asset | PAYMENT_INVALID |
no |
| authorization expired / not yet valid | PAYMENT_INVALID |
no |
| insufficient balance | PAYMENT_INVALID |
no |
| authorization already spent on chain | PAYMENT_INVALID |
no |
| copy reaches gateway reservation before the original settles | PAYMENT_REPLAYED |
no |
| provider/RPC unavailable during verification | PAYMENT_PROVIDER_UNAVAILABLE |
no |
| settlement transaction fails | PAYMENT_SETTLEMENT_FAILED |
no |
backend fails before settlement (authorization flow) |
BACKEND_ERROR / BACKEND_TIMEOUT |
no; nothing settled; x402 can retry |
backend fails after settlement (upfront flow) |
BACKEND_ERROR / BACKEND_TIMEOUT |
no; payment recorded, delivery failed |
After settlement, a backend failure needs reconciliation rather than a
rollback. The gateway records a settled payment attempt and a
backend.failed event with the same requestId.
- On-chain. EIP-3009 marks
authorizationState[from][nonce]used; a secondtransferWithAuthorizationwith the same nonce reverts. Permit2 marks the owner's nonce used in its own bitmap, across every token. A Solana cluster refuses a transaction it has already processed, and one whose blockhash has expired. NEAR refuses a delegate whose nonce is at or below its access key's nonce. This prevents a double spend. - In the gateway. Concurrent submissions could unlock a second delivery
before the first settles. The pipeline reserves
replayKey, derived from(chainId, asset, payer, nonce)for EIP-3009,(chainId, Permit2, owner, nonce)for Permit2, the network and SHA-256 of the transaction message for Solana, or(network, sender, public key, nonce)for NEAR, under aUNIQUEconstraint before callingsettle. A duplicate returnsPAYMENT_REPLAYED: 409 while the first attempt is unfinished or its status is unavailable; 402 with a fresh challenge when its stored status is treated as finished. A rejected x402 attempt settled nothing: the backend failed, settlement was refused or the facilitator never submitted it. The same authorization can reserve it again. MPP keeps the 402: a credential is single-use per challenge.
Because the key comes from the authorization rather than the request, replaying it against another request still collides.
A resource that also requires an AP2 mandate gets a third, independent reservation, in its own database and on its own key. It is claimed before the payment replay key and released only by a failure that provably moved no money. See AP2.
Canonical amounts are decimal strings in display units, such as "0.01", never
floats. Conversion to base units (6 decimals for USDC) happens inside the
payment provider. An amount with more precision than the
asset supports is a configuration error, not a rounding opportunity.
An x402 payment in the network's default USDC records currency: USDC,
keeping a resource's USD price as metadata.quote on settlement. Other
configured tokens use the resource's currency label.
The demo and CI settle for real, on a chain they own:
- Anvil on its default chain id 31337, advertised as the CAIP-2 network
eip155:31337.health()also probes foranvil_nodeInfo, because an RPC can report any chain id. MockUSDC: 6 decimals, EIP-3009, EIP-712 domain("MockUSDC", "2").- Anvil's well-known accounts as deployer/facilitator, merchant and buyer,
labeled
LOCAL DEVELOPMENT ONLY - DO NOT FUND. - The E2E asserts the buyer's balance falls and the merchant's rises by exactly the price, and that the receipt carries a real transaction hash.
- A second E2E settles on Anvil chains 31338 and 31339 through a loopback
SDK
x402Facilitator, exercising the remote path used for public networks.
No public RPC, no public chain, no hosted facilitator, no real money.
Both x402 and MPP have settled on Base Sepolia (eip155:84532) and Base
mainnet (eip155:8453) through the same pipeline and remote facilitator model.
Public-network configuration follows these rules:
- The deployment mode comes from the network.
eip155:31337is the local chain; every other network is the testnet or mainnet it names, and the in-process facilitator is refused on all of them. - Mainnet is refused unless every guardrail is satisfied - an explicit
allowMainnet, a remote facilitator over HTTPS, a second explicit acknowledgment if no facilitator credential is configured, a non-development settlement destination, and canonical USDC with its EIP-712 domain on built-in mainnets. Config validation checks these before startup;agent-commerce validatereports missing guards. See configuration.md. assetNameis part of that. Base mainnet's USDC reports"USD Coin", Base Sepolia's reports"USDC", and the buyer signs that string into their EIP-712 domain. Config validation rejects a mismatch before the buyer signs.health()asks the right question per mode. In local mode it probesanvil_nodeInfo, so a local facilitator pointed at a real node reports unhealthy and/readyrefuses to serve. In remote mode it asks the facilitator what it supports and fails if our scheme and network are not on its list: a facilitator that is up but cannot settle this pair would otherwise fail every payment after the buyer signed.
allowMainnet acknowledges real-money operation; it does not enable a separate
runtime path.
Running against a public network is configuration.md; a
worked config for each is in examples/base-sepolia/ and
examples/base-mainnet/.