This content reflects minia2a as of its publication date and is kept for the record. Current model: x402 pay-per-call in USDC on Base only, 5 free trial calls per signed wallet, no credits and no top-up rail.
Before Your Agent Signs a 402, Check Five Fields
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.
The shape you are looking at
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.
1. network and payTo must be the same chain family
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.
2. asset must be the canonical token for that network
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.
3. amount is an integer in the asset's smallest unit
"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:
- If you see a decimal point in an amount ("0.01"), it is not a valid x402 amount — the field is defined as an integer in atomic units. Treat it as a malformed challenge, not a price you can round.
- Convert to human units yourself only for logging. Sign the integer. A client that "helpfully" converts 10000 to 10000000 pays 100× the price.
4. Absent assetTransferMethod means the EIP-3009 default
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."
5. Multiple accepts: not all of them are the same offer
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.
The one-line version: the only price that exists is the one in the 402 you are holding right now. Everything else — a catalog page, a discovery record, a marketing page — is a snapshot of some earlier challenge and may be stale. Verify the five fields on the live challenge, then sign.
Why the snapshot problem is real, not theoretical
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.
A preflight you can paste
# 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.
minia2a is a pay-per-call market for x402 endpoints — 1,600+ tools, priced per call, paid in USDC on Base. Every tier-1 call answers with the same challenge shape above, and every trial call is wallet-signed with no registration.
Read more on the minia2a blog, or start from the quick-start.
Originally published at minia2a.uk.
Top comments (0)