Refuse before you charge: how an x402 API stops billing agents for bad requests
To stop charging agents for bad requests, put validation in front of the payment challenge: an x402 seller should answer a request that can never succeed with 400, 415 or 422 and no PAYMENT-REQUIRED header, and should issue a 402 only for a request it is prepared to fulfil. Then, because handlers can still fail, cancel the signed payment instead of settling it when the handler returns an error. The Red Cup seller at market.561.group does both, and the responses below were captured from the live endpoint on 2026-10-22.
Why is charging for a bad request a problem specific to agents?
A human who pays for a broken download sends an email. An agent that pays for a broken call has already spent: the spend authorization is signed, possibly settled, and the agent's budget is lower by an amount it cannot dispute through any channel the protocol defines. The x402 specification standardizes the challenge and the payment, not the order in which a seller validates things. The default order in many tutorials is the order of middleware: payment first, handler second, validation inside the handler. That order makes every typo a purchase.
The order should be the reverse of the order of cost. Checks that cost the seller nothing (is it JSON, does it match the schema, is it within size limits) come first. Quoting comes second. Payment verification and settlement, which involve a facilitator and a chain, come last.
What does a bad request look like on the live seller?
All of these target POST /v1/data/json-pointer, whose body schema requires a string pointer and a value, with no additional properties. None of them returns a 402.
| Request body | Status | Content type | Body (abbreviated) |
|---|---|---|---|
{"nope": (truncated JSON) | 400 | application/json | {"error":"request body must be valid JSON"} |
{"value":1} (missing pointer) | 400 | application/problem+json | issue: must have required property 'pointer' |
{"value":1,"pointer":"/a","extra":true} | 400 | application/problem+json | issue: must NOT have additional properties |
{"value":1,"pointer":5} | 400 | application/problem+json | issue at /pointer: must be string |
{"value":1,"value":2,"pointer":""} (duplicate member) | 422 | application/json | {"error":"duplicate-key at $.value: duplicate object name: \"value\"","code":"duplicate-key","path":"$.value"} |
any body with content-type: text/plain | 415 | application/problem+json | expected application/json |
GET on a POST-only offer | 405 |
Reproduce the table with curl. This costs nothing:
U=https://market.561.group/v1/data/json-pointer
curl -s -i -X POST $U -H 'content-type: application/json' -d '{"value":1,"pointer":5}'Every one of those responses has cache-control: no-store and none has a payment-required header. Only the well-formed body
{"value":{"a/b":[1,null]},"pointer":"/a~1b/1"}reaches the 402. The refusal bodies are RFC 9457 application/problem+json documents with an issues array naming the JSON path and the failed keyword, which an agent can read and repair without a human.
Why do duplicate JSON members get a 422?
JSON allows an object to repeat a name, and parsers disagree about which occurrence wins. If a paid offer silently chose one, the buyer would pay for an answer to a question the seller guessed. The Red Cup seller's intake admits received bytes through a bounded canonical JSON codec before quoting or executing anything: duplicate members, malformed UTF-8 or Unicode, and non-finite numbers are refused rather than resolved. The result is 422 Unprocessable, distinct from the 400 of a syntactically broken body. The distinction is useful to a client: 400 means "fix the document", 422 means "the document parses but the content is ambiguous".
The same principle applies to every offer in the catalog. Offers that convert formats refuse unrepresentable inputs before payment: the CSV converter refuses duplicate headers and ragged records, and the FOUR-valued logic offers refuse unknown value names:
curl -s -i -X POST https://market.561.group/v1/four/reading-negate \
-H 'content-type: application/json' -d '{"name":"maybe"}'
# HTTP/2 400 ... "keyword":"enum","message":"must be equal to one of the allowed values"Each offer's catalog entry says so in its description: "Unknown names/families refuse before payment" appears in the text of the FOUR offers, and the JSON offers say they "reject duplicate keys, invalid Unicode and non-finite numbers before payment".
Where is the schema that decides?
The decision uses the same input contract that the seller publishes. Each entry of `/v1/catalog` carries a request.bodySchema and an example, and the OpenAPI document is derived from that same catalog. So an agent that reads the catalog knows in advance what will be refused, and the seller cannot refuse a body that its own documentation permits. There is no second schema to drift.
What if the request is valid but the handler fails?
Validation cannot catch everything: an upstream data source can be down, a computation can throw. For those cases the engine's fetch server orders the work as: verify the payment, run the handler, and only then settle. If the handler throws, or returns any status of 400 or above, the engine calls the payment library's cancellation hook with a reason, handler_threw or handler_failed, and passes the handler's error response to the buyer. The facilitator is never asked to move the funds. In the engine source the relevant branch reads:
if (handlerResponse.status >= 400) {
if (claim !== undefined) await releasePurchase(purchaseStore, claim);
await cancellationDispatcher.cancel({ reason: "handler_failed", responseStatus: handlerResponse.status });
// ... return the handler's own error response, minus internal headers
}(Excerpt from the Red Cup x402 engine, fetch-server.mjs; abridged.) A seller that wants to bill for an honest negative answer must say so. One offer, the Base transaction check, documents that it "returns found:false (billably) for a hash the node does not know". That is a successful answer to a question, so it is charged and it is stated in the offer description.
What are the limits of this approach?
Pre-payment validation proves the request is well-formed, not that the seller will succeed. An upstream outage after validation is handled by cancellation, not prevention. Validation itself consumes the seller's compute without payment, which is why bodies are bounded (the JSON offers cap input at 64 KiB, the request carrier at 100 KiB) and why each refusal is cheap and no-store. And an unsigned 402 is not a sale; the seller treats a quote, a facilitator success and a delivered response as separate observations.
How do I apply this to my own x402 route?
- Publish a JSON Schema for every paid route and validate against it first, with
additionalProperties: falseunless you truly mean otherwise. - Parse JSON with a parser that detects duplicate members, and answer
422. - Check
content-typeand answer415rather than guessing. - Return RFC 9457 problem documents so agents can self-correct.
- Let the quote follow validation, not precede it. Price from the validated, canonical body (Per-byte pricing).
- Cancel, do not settle, when the handler fails.
The Apache-2.0 x402 boundary reference shows how to configure the explicit route set for an Express app; put your validation middleware ahead of the payment middleware. For the rest of the seller's policies, return to what an x402 seller should do beyond the spec.