Core concepts
Payment flow
The paid round trip end to end, why money cannot be redirected, and every way it fails closed.
Roles
| Role | Holds a key? | Where it runs |
|---|---|---|
| Buyer agent | yes — its own | the agent's machine |
| Gateway | no | merchant infrastructure |
| Facilitator | a gas-paying signer | local dev chain in the demo; external in production |
| Merchant | destination address only | configuration (payTo) |
The gateway is in the middle of the protocol and outside the custody.
The round trip
buyer gateway chain / backend
│ 1. tools/call market_report │ │
├──────────────────────────────►│ resolve, validate, price → paid │
│◄──────────────────────────────┤ PaymentRequiredEnvelope │
│ 2. isError + envelope │ │
│ 3. sign EIP-3009 authorisation│ │
│ (to = merchant payTo) │ │
│ 4. tools/call + _payment │ │
├──────────────────────────────►│ verify signature, recipient, │
│ │ amount, window, network, asset │
│ │ reserve replayKey │
│ │ settle ──────────────────────────►│ transferWithAuthorization
│ │◄──────────────────────────────────┤ tx receipt
│ │ call merchant backend ───────────►│ GET /api/report
│ │ save receipt (txHash) │
│◄──────────────────────────────┤ │
│ 5. result + receipt │ │Over plain HTTP the steps are the same: the challenge arrives as a 402 body and in the base64 PAYMENT-REQUIRED header, the proof travels in the PAYMENT-SIGNATURE header instead of the _payment tool input, and the settlement result comes back in PAYMENT-RESPONSE.
Why the money cannot be redirected
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. Whoever broadcasts it can only execute exactly that transfer, or nothing.
Fail-closed matrix
| Condition | Result | Delivered? |
|---|---|---|
| no proof supplied | 402 + envelope | no |
| malformed proof, bad signature | PAYMENT_INVALID | no |
| wrong amount, recipient, network or asset | PAYMENT_INVALID | no |
| authorisation expired or not yet valid | PAYMENT_INVALID | no |
| insufficient balance | PAYMENT_INVALID | no |
| authorisation already seen | PAYMENT_REPLAYED | no |
| provider or RPC unreachable | PAYMENT_PROVIDER_UNAVAILABLE (retryable) | no |
| settlement transaction fails | PAYMENT_SETTLEMENT_FAILED | no |
| backend fails after settlement | BACKEND_ERROR / BACKEND_TIMEOUT | no — payment recorded, delivery failed |
payment_attempt with status settled and a backend.failed event sharing the same requestId.Replay: two independent defences
- On-chain
EIP-3009 marks
authorizationState[from][nonce]used; a second transfer with the same nonce reverts. This prevents a double spend. - In the gateway
A replayed authorisation could still unlock a second delivery before the first settles. So the pipeline reserves a
replayKeyderived only from(chainId, asset, payer, nonce)under aUNIQUEconstraint before callingsettle. The same authorisation replayed against a different request still collides.
A resource that also requires an AP2 mandate gets a third, independent reservation in its own database. See AP2 mandates.
Amounts
Canonical amounts are decimal strings in display units — "0.01" — never floats. Conversion to base units (6 decimals for USDC) happens inside the payment provider, deterministically. An amount with more precision than the asset supports is a configuration error, not a rounding opportunity.
Local deterministic settlement
- Anvil with
--chain-id 84532, advertised aseip155:84532. That id is shared with Base Sepolia, so nothing infers “public network” from it. MockUSDC: 6 decimals, EIP-3009, EIP-712 domain("MockUSDC", "2").- Anvil's well-known accounts as facilitator, merchant and buyer — local development only, do not fund.
- The end-to-end test asserts the buyer's balance falls and the merchant's rises by exactly the price, with a real transaction hash in the receipt.
Public networks use the same pipeline with a remote facilitator — see Networks & facilitators.