Buyer-side pitfalls in x402 payments

x402 lets an HTTP client pay for a resource by answering a 402 Payment Required response with a signed payment. This guide covers fourteen buyer-side pitfalls for the common exact scheme on EVM (USDC, EIP-3009), each with what goes wrong, an example and a check; points labelled as general are not taken from the specifications.

Header and field names by version

x402 v2x402 v1
402 requirementsPAYMENT-REQUIRED header, base64JSON body with an accepts array
Client paymentPAYMENT-SIGNATURE header, base64X-PAYMENT header, base64
Server receiptPAYMENT-RESPONSE header, base64X-PAYMENT-RESPONSE header, base64
Price fieldamountmaxAmountRequired
NetworkCAIP-2, for example eip155:8453names, for example base-sepolia

1. Trusting payTo, asset or network from the 402

What goes wrong. Anything that can alter the 402 can change payTo, asset or network, and a client that signs the first accepts entry it can parse pays whoever is named there (general advice). The spec says clients MUST NOT construct a payment for a paymentFlow they do not recognize and SHOULD skip such entries (spec section 6.1).

allow-list   https://api.example.com   payTo 0x1111111111111111111111111111111111111111
402 says     payTo 0x9999999999999999999999999999999999999999   -> refuse, sign nothing

Fix (general advice). Keep an allow-list per origin of payTo, asset and CAIP-2 network, and refuse anything outside it before any signing call.

How to check. Capture a real 402, change its payTo, asset or network, and run it through DeepFirst's dryRun() from the agent-pay SDK, which signs nothing and needs no key. A mutated 402 must give a refusal, for example payee_mismatch or network_not_allowed, and no signature.

2. Reading amounts in the wrong units

What goes wrong. The v2 amount is a string of atomic token units (spec section 5.1.2). USDC has 6 decimals on Base mainnet and Base Sepolia (read on-chain), so "10000" is 0.01 USDC. Neither the core specification nor the EVM scheme defines a decimals field. A seller may add its own decimals hint under extra, and a client that trusts it can read a charge as far smaller than it is.

amount "500000", extra.decimals 18 (seller-supplied, hypothetical)
client:  500000 / 10^18 = 0.0000000000005 USDC   -> under the 0.05 cap, signs
chain:   500000 atomic units of USDC (6 decimals) = 0.50 USDC charged

How to check. Unit test: with a cap of 50000 atomic units (0.05 USDC), a 402 for "500000" that declares decimals: 18 must be refused. Compare amounts as integers in atomic units, taking decimals from the token contract rather than the 402.

3. Accepting any network, or the wrong chain ID

What goes wrong. In version 2, networks are CAIP-2 identifiers: eip155:8453 is Base mainnet and eip155:84532 is Base Sepolia (spec section 11.1). A prefix check such as startsWith("eip155:"), or matching the word "base", approves a chain the owner never allowed (general advice). EIP-712 says the user agent should refuse signing if the domain's chain ID does not match the currently active chain. The EVM scheme requires extra.name and extra.version, the token's EIP-712 domain name and version, and the name differs between networks: USDC's name() is "USD Coin" on Base mainnet but "USDC" on Base Sepolia (read on-chain). Hard-coding the Sepolia name for mainnet produces a digest the token rejects (general EIP-712 behaviour).

allowedNetworks: ["eip155:8453"]     owner approved Base mainnet only
402 network:     "eip155:84532"      refuse: not an exact match
prefix check:    "eip155:84532".startsWith("eip155:") is true -- do not use it

How to check. Serve a 402 for eip155:84532 to a client that allows only eip155:8453; the test must see a refusal and no signature.

4. Validity windows and clock skew

What goes wrong. EIP-3009 defines validAfter as the time after which an authorization is valid and validBefore as the time before which it is valid, both in unix time. The token checks these against block time (general EVM behaviour), not against the client's clock. A fast clock signs a validAfter that has not yet arrived on chain, and a slow clock signs a validBefore that has already passed; the facilitator then reports invalid_exact_evm_payload_authorization_valid_after or invalid_exact_evm_payload_authorization_valid_before (spec section 9). The exact EVM scheme's verification steps say that the amount and validity window "meet the PaymentRequirements", without saying how. Its example has a 65-second window against maxTimeoutSeconds 60, so do not rely on the verifier to enforce your bound (general advice).

"validAfter":  "1740672089"
"validBefore": "1740672154"    -> 65 seconds later; the spec's example 402 says maxTimeoutSeconds 60

How to check. Generate an authorization and assert that validBefore minus now is no more than maxTimeoutSeconds plus an agreed skew allowance. Alert when the local clock differs from the latest block timestamp by more than that allowance.

5. Nonce reuse and retries that sign again

What goes wrong. Each EIP-3009 authorization carries a random 32-byte nonce, and the token refuses to use the same nonce twice; authorizationState(authorizer, nonce) reports whether it was used (spec section 10.1; ERC-3009). That blocks replaying an old payload, but not a retry that signs a new one. If a timeout hid a successful settlement, the fresh nonce is a second valid payment (general advice).

attempt 1: sign nonce 0xa1..., POST /data  -> settles on chain, response lost (timeout)
attempt 2: sign nonce 0xb7..., POST /data  -> settles again
result:    two transfers of 0.01 USDC for one response

Fix (general advice). Persist the signed PAYMENT-SIGNATURE before sending it, resend that same payload on retry, and sign a replacement only when the old nonce is unused on chain and the old validBefore has passed by chain time.

How to check. Simulate a timeout after the server has settled, retry, and assert that the retry sends a byte-identical PAYMENT-SIGNATURE and that exactly one nonce was signed.

6. Paying before the 402 is bound to the request

What goes wrong. The exact EVM payload signs a TransferWithAuthorization over from, to, value, validAfter, validBefore and nonce, plus the token's EIP-712 domain (ERC-3009 typehash). The method, URL and body are not signed, and the resource object in the 402 describes the protected resource (spec section 5.1.2). Nothing in the signed fields binds a payment to one request, so an agent that edits the body between the 402 and the paid retry can pay for something it never evaluated (general advice).

evaluated: POST https://api.example.com/orders   body {"qty": 1}
402:       resource.url = https://api.example.com/orders
retry:     POST https://api.example.com/orders   body {"qty": 100}   same signature

How to check. Change the path or body between the 402 and the paid retry, and assert that the client does not send PAYMENT-SIGNATURE. Also refuse a 402 whose resource.url names a different path from the request being evaluated.

7. Trusting a 200 without reading PAYMENT-RESPONSE

What goes wrong. In the default authorization flow the server verifies, runs the resource, settles and then responds (spec section 6.1), and the outcome comes back in PAYMENT-RESPONSE. A failed settlement is shown as an HTTP 402 carrying a PAYMENT-RESPONSE whose success is false (HTTP transport v2, failure example). A settlement_pending receipt carries a transaction hash and may still confirm, so the hash must be reconciled before retrying (spec section 9). A client that books spend from status codes alone can record a payment that never settled, or retry one that did (general advice).

PAYMENT-RESPONSE, base64 of:
{"success":false,"errorReason":"insufficient_funds","transaction":"","network":"eip155:84532","payer":"0x857b..."}

Fix (general advice). Decode the receipt and require success to be true, a non-empty transaction, and the network you signed for. If a 2xx arrives with no readable receipt, check authorizationState on chain before recording spend. X-PAYMENT-RESPONSE is the version 1 equivalent.

How to check. Fixture tests: a 200 with no receipt must be recorded as unconfirmed and trigger no new signature; a 402 with success: false and insufficient_funds must record no spend; a settlement_pending receipt must be recorded as unconfirmed.

8. Redirects that change the payee

What goes wrong. A client that follows a redirect may land on another host, which can return a 402 with a different payTo, and then sign for that new payee. Many HTTP libraries follow redirects by default (general advice).

GET https://merchant.example/data  -> 302 Location: https://relay.example/pay
GET https://relay.example/pay      -> 402 payTo 0x9999...

Fix (general advice). Do not follow redirects on paid calls, or refuse any 402 that arrives from a different origin than the request it answers.

How to check. Point a local merchant's 302 at a second local server whose 402 names another payTo; the client must make no signature and must report the redirect.

9. Caching 402 requirements too long

What goes wrong. The spec defines maxTimeoutSeconds as the maximum time allowed for payment completion (spec section 5.1.2) and says nothing about how long a 402 may be cached. A cached quote can authorise an amount or payee the seller has since changed, and a cached approval can outlive the owner's decision to lower a cap or drop a merchant (general advice).

t=0    owner approves a 402 for 10000 units under a 50000 cap; quote cached for 24 h
t=2h   owner lowers the cap to 5000 units
       cached approval still signs 10000 units -> stale decision

Fix (general advice). Fetch a fresh 402 for every payment and run the policy at signing time against the live quote. If you cache anything, keep it to seconds.

How to check. Change the mock merchant's price and payTo after the first call; the second call must re-check and refuse the new payee rather than sign from the cache.

10. No per-request price cap

What goes wrong. With no ceiling, an agent pays whatever price a seller asks, and the price in a 402 can change between requests (general advice). Instructions hidden in fetched content can also push an agent to make paid calls it would otherwise refuse, so a cap is the backstop (general advice).

per-request cap: 50000 units (0.05 USDC)
402 amount 10000  -> within cap, sign (0.01 USDC)
402 amount 500000 -> refuse: over cap (0.50 USDC), ask the owner

How to check. Price the mock merchant at ten times the cap (or run the dry-run check from section 1); the client must refuse and produce no signature.

11. Session budgets that reset or ignore pending payments

What goes wrong. A per-request cap does not stop many small calls, so a session or period budget is needed too (general advice). A budget kept only in process memory resets on every restart, so a crash loop spends it again (general advice). A budget that counts only confirmed payments misses authorizations that are signed but not yet confirmed, and those can still settle until their validBefore (ERC-3009).

budget: 1.00 USDC per day, kept in an in-memory Map
process restarts 20 times in a day -> up to 20.00 USDC spent

Fix (general advice). Store spend durably, reserve the amount before signing, and release a reservation only when the authorization is settled, or unused and past its validBefore.

How to check. Kill the client between two paid calls and restart it; the second call must see the first call's reservation, and a signed but unconfirmed authorization must count against the budget.

12. Signing keys reachable by agent tools

What goes wrong. The ERC-3009 security section warns that when a smart contract calls the transfer, an attacker watching the transaction pool can front-run transferWithAuthorization with the extracted authorization, which could leave deposits locked up; receiveWithAuthorization adds a check that the caller is the payee. The reference transferWithAuthorization checks the signature, the time window and the nonce but no caller, so anyone holding a signed authorization can submit it (ERC-3009). Any tool that signs arbitrary typed data or raw hashes can therefore authorise a transfer the model did not intend (general advice).

tool sign_typed_data(domain, types, message)   model supplies every field
tool sign_hash(hash32)                         model supplies any 32-byte value

Fix (general advice). Give the agent a payer that signs only TransferWithAuthorization for allowed tokens, chains, payees and caps, and keep the key out of the model's environment and tool output.

How to check. Search tool definitions, environment dumps and transcripts for private-key patterns (0x followed by 64 hex characters), and assert that the signer rejects typed data whose primaryType is not TransferWithAuthorization or whose verifyingContract is not the allowed token.

13. Logging payment headers and signatures

What goes wrong. The exact EVM payload carries a 65-byte signature together with the authorization fields, in PAYMENT-SIGNATURE (X-PAYMENT in version 1). Anyone holding that header can submit the same transfer to the same payee for the same amount until validBefore or until the nonce is used (ERC-3009 semantics). A debug log, trace or support bundle that keeps the header therefore hands that ability to anyone who can read it (general advice).

DEBUG retry POST /data PAYMENT-SIGNATURE=eyJ4NDAyVmVyc2lvbiI6Mi...   submittable by anyone until validBefore

Fix (general advice). Redact the payment headers, and log only the amount, payTo, network, a hash of the nonce and the transaction hash.

How to check. Run one paid call with debug logging on, then grep the logs for the header value and for a 130-hex-character signature; the check fails on any match.

14. No refund or dispute path

What goes wrong. The exact scheme specification says that "this specification defines no refund, and any remedy is the resource server's own arrangement", and that under upfront the payment commits first, so a handler failure leaves the client charged with nothing delivered. The core specifications and HTTP transports define no refund or dispute message either. A settled EIP-3009 transfer cannot be reversed by the protocol (general EVM behaviour), so losses have to be prevented before signing and recorded afterwards (general advice).

status: paid, not delivered   tx 0x...   0.01 USDC   resource returned 503 on retry
protocol refund: none         next step: report to the owner; do not sign again for the same call

How to check. Run a monthly job that matches every settled transaction to a delivered response, and fail the job on any gap.

Sources and status

Checked on 2026-10-18 against the x402 core specification v2 (sections 5.1.2, 6.1, 9, 10.1 and 11.1), the v1 core specification, the v1 and v2 HTTP transports, the generic and EVM exact scheme specifications (github.com/coinbase/x402), ERC-3009 (github.com/ethereum/ercs) and EIP-712 (github.com/ethereum/EIPs). USDC decimals, name and version were read on-chain from public Base RPC endpoints for mainnet and Sepolia. The dryRun() reference comes from DeepFirst's public SDK README (github.com/DeepFirstHQ/deepfirstsearch). Points labelled (general advice), (general EVM behaviour) or (general EIP-712 behaviour) are not taken from these sources.