An agent calling a pay-per-call endpoint gets rejected in one of three ways, and only one of them is a payment problem. Mistaking the other two for "I owe money" is the single most common way agents waste a transaction on a problem the transaction can't solve.
| What you sent | HTTP | MCP tools/call | What it actually means |
|---|---|---|---|
| No wallet, no signature | 402 challenge | 402 challenge | Anonymous access is off. Pay, or sign for a trial. |
| Wallet present, signature missing / invalid / expired | 400 + reason | -32602 (Invalid params) | Client-side signature problem. Re-sign, or drop the wallet param. |
| Wallet + valid signature, trials spent | 402 challenge | 402 challenge | You've used your free calls. Pay to continue. |
Read the last column carefully. A 400 (or -32602 over MCP) is not a 402. It is not "insufficient balance", it is not "this endpoint costs money you haven't sent". It is the server saying: the signature you attached does not prove you are the wallet you named.
The one-line mental model:
402 = a payment question. Pay and it resolves.
400 / -32602 = a cryptography question. Paying does nothing — the signature is broken. Fix the signature.
When a wallet is attached but the signature doesn't verify, the gateway returns an explicit JSON error naming the reason rather than silently treating the call as anonymous:
HTTP 400
{"error":"signature does not match wallet — sign message: ..."}
Over MCP, the same failure surfaces as a JSON-RPC error with the standard "Invalid params" code:
{"jsonrpc":"2.0","error":{"code":-32602,"message":"..."},"id":2}
The message carries the failure reason and a hint: re-sign the trial message, or drop the wallet parameter entirely and pay the 402 challenge like any anonymous caller.
Trial access is EIP-191 personal_sign over this exact string:
minia2a trial:<wallet>:<serviceId>:<unixSeconds>
The wallet must be byte-for-byte the same string you pass in ?wallet= — checksummed or lowercase, just be consistent. The serviceId is the full endpoint id (for /x402/time that is x402-time, not time). The timestamp is the same value you send in X-Trial-Timestamp.
1. The [object Promise] signature. viem's signMessage returns a Promise. Forgetting await makes the header literally the string [object Promise], which verifies against nothing and returns 400. This is a test-script bug, not a platform bug — but it's the most common one I hit.
2. Wrong serviceId. Signing time when the id is x402-time returns the same 400 a bad signature does. If the signature is fresh, check the id before anything else.
3. Address case drift. The signed payload contains the address as a string. If it doesn't match what's in ?wallet= byte-for-byte, the signature is over a different message.
4. Expired timestamp. A stale X-Trial-Timestamp fails the freshness check and lands in the same 400 bucket as an invalid signature.
The failure mode an agent thinks it's in determines what it does next. An agent that sees 400 and concludes "I need to pay" will sign a payment and retry — and get 400 again, because the signature is still broken. An agent that correctly reads 400 as "my signature is broken" re-signs in seconds and moves on.
For protocol design, the takeaway is the same one the x402 tax & accounting working group keeps circling back to: distinguish what the client controls from what the ledger records. A bad signature is a client problem; a spent trial balance is a payment problem. Collapsing both into "402" makes agents pay for their own signing bugs.
The quick fix, in order:
1. Did you attach a wallet? No → it's a 402, pay or sign for a trial.
2. Yes, but the signature is missing/bad/expired → re-sign the exact minia2a trial:... message, or drop ?wallet= to fall through to a clean 402.
3. Signature is valid and you still get 402 → your 5 free trial calls are spent; pay to continue.