2026-10-13 ยท minia2a
x402's whole pitch is that a paywalled API answers an unpaid request with 402 Payment Required plus a JSON body the client can act on โ no docs, no dashboard, no human. That body is machine-readable. It is not automatically machine-verifiable. A client that lifts the amount and payTo and signs will, on some fraction of endpoints, produce a signature for a challenge that was never payable in the first place.
Here is the preflight we run before spending a signature. It is five fields in the challenge's accepts[] array. None of them is exotic; every one of them has a failure mode we have hit.
A canonical x402 v2 challenge, read live and unpaid, looks like this (this is a real read of a first-party endpoint):
{
"accepts": [{
"scheme": "exact",
"network": "eip155:8453",
"amount": "10000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xAb62452b4b019bC4402BFfCca6C706d16d72A7Bf",
"maxTimeoutSeconds": 120,
"extra": { "name": "USD Coin", "version": "2" }
}]
}
An agent's job is to decide, from this alone, "can my wallet settle this?" Five checks.
network is CAIP-2 (eip155:8453 is Base). payTo is an address, and an address alone does not tell you its chain. If an accept advertises eip155:8453 but the payTo is controlled on a different network, the payment cannot settle โ you sign, the transfer reverts, and you paid gas to find out. Check that the payTo is one your settlement path can actually reach on the advertised network. On a marketplace whose accept array is assembled per-endpoint, this is exactly the field that drifts.
USDC is deployed at a different address on every chain. A challenge that says eip155:8453 but carries an asset address from another network is unpayable even though both strings look like USDC. Compare the full checksummed address against the canonical deployment for the advertised network โ not a prefix, not a symbol. Lookalike and bridged assets are the failure this catches.
"10000" above is not $10,000 and it is not 10,000 tokens. USDC has six decimals; the atomic unit is microUSDC, so 10000 microUSDC is $0.01. Two consequences:
Some accepts name a transfer method; some omit it. The absence is not "unspecified" โ per the x402 vocabulary it means the client default, which is the EIP-3009 authorization branch (a signed authorization the facilitator submits, not a transferWithAuthorization you broadcast). The gotcha is not the default; it is when the advertised method does not match the token. If an accept names a transfer method the asset does not implement, that accept is invalid for that asset โ a mismatch should make you skip the accept, not fall back to the default. Treat "method named and not supported by the asset" as a different case from "method omitted."
A challenge can carry an accepts array with several entries โ different rails, different assets, different prices, sometimes a free-trial path beside a paid one. They are alternatives for the server to accept, not interchangeable for your client. Pick the first one your wallet can actually sign end-to-end (right network, right asset, supported transfer method, amount within budget) and ignore the rest. A client that signs accepts[0] unconditionally will fail for any counterparty whose first accept is a rail it does not hold.
x402 discovery catalogs publish a price per resource. That price is written when the entry is created or last refreshed, and it does not automatically track the endpoint. On our own catalog entries we measured the gap directly: of 154 entries pointing at first-party endpoints, 134 matched the live 402 exactly and 20 were understating the price โ one off by 5000ร ($0.01 shown, $50.00 live). The refresh trigger is a payment, so the entries that rot are precisely the ones nobody is buying. An agent that budgets from the catalog and signs blind can be signing for 5000ร what it expected. Read the live challenge; treat the catalog as a hint.
# 1. Fetch the live challenge, unpaid.
curl -s -X POST https://minia2a.uk/x402/math \
-H 'content-type: application/json' -d '{}'
# 2. Assert, before signing:
# accepts[i].network family == the chain you can settle on
# accepts[i].payTo == an address reachable on that network
# accepts[i].asset == canonical USDC for that network (full addr)
# accepts[i].amount == /^[0-9]+$/ (integer, atomic units)
# accepts[i].assetTransferMethod absent -> EIP-3009 default
# present -> asset must implement it
Five checks, all of them cheap, none of them needing a wallet. Do them before the signature, not after the revert.
Read more on the minia2a blog, or start from the quick-start.