In a subscription API, a malformed request costs you latency. In a pay-per-call API it costs the caller money. That single difference makes your input schema a billing artifact, and it raises the price of leaving it vague by exactly one call per agent that has to learn your contract the hard way.
We measured what that looks like from the inside. One external wallet spent an hour walking our catalog and paid for every step:
Every one of those refusals was the handler doing its job. Every one was also avoidable, because the request the buyer sent was consistent with the contract we publish.
Between 18:07 and 19:06 UTC, one wallet made 89 settled calls across 84 distinct endpoints, walking the catalog in order. 58 returned a result. 31 came back refused โ and all 31 were input-shaped, with these messages:
| handler message | count |
|---|---|
text required | 4 |
expr required | 4 |
invalid domain | 3 |
valid email required | 2 |
domain required | 2 |
xml required, topic required, country name required, token param required, n and k required, http(s) url required, โฆ | 1 each |
The buyer is sending no arguments. Not wrong arguments โ none. It calls the endpoint, the endpoint says "I need text", and the call is over.
That is not a dumb client. It is a client that read the contract and believed it.
A discovery surface for a paid API has to answer two different questions: what does this cost, and what do I send. Ours answers the first well โ every operation carries the price, the network, the asset, and the x402 challenge shape. For the second, the only machine-readable source is the OpenAPI document, and here is what it publishes for a service that refuses an empty call:
{
"get": {
"parameters": [
// โฆ the trial and probe flags are on this operation too, elided here โฆ
{ "name": "domain", "in": "query", "required": false }
]
}
}
Every parameter on that operation is emitted as optional โ the trial flags, the probe flag, and domain itself. The handler behind it answers {"ok":false,"error":"invalid domain"} when domain is absent. The document does not contradict the handler โ it just never states the constraint, and in a schema, silence is a statement. required: false is not "we didn't say"; it is "you do not need to send this."
The POST form of the same operation is the same shape one layer down: a body schema with properties and no required array.
"requestBody": {
"required": true,
"content": { "application/json": { "schema": {
"type": "object",
"properties": { "domain": { "type": "string" } },
"additionalProperties": true
}}}
}
"required": true at the top of that object means a body must be present. Every schema validator reads it that way, and every one of them will then happily accept {}, because nothing inside schema is required.
The fields in that list are not four separate inputs. They are aliases โ text, input, source, code might all name the same string, and the handler accepts whichever one you send. That convention is friendly, and it is also what makes the schema hard to state: the real constraint is "at least one of these", not "all of these" and not "none of these".
OpenAPI has no ergonomic word for "at least one of" over query parameters, so the generator emits each alias as required: false, which is individually defensible and collectively wrong. The body case has a clean answer that goes unused:
"schema": { "type": "object", "minProperties": 1, "properties": { โฆ } }
Where the constraint genuinely is "any one of these N", minProperties: 1 states it exactly, is machine-checkable, and is what a client needs in order to refuse to spend money on a call that cannot succeed. For the query-parameter form there is no equally clean primitive, but there is an honest fallback: mark the primary alias required, or describe the constraint in the operation's own text. Either beats a field that reads as optional.
A 402 is a contract about payment terms. It is not a statement that your request is well formed. A challenge can be perfectly shaped โ right price, right asset, right payTo, valid signature domain โ while the request it accompanies is one your handler will reject. Nothing in the protocol requires those two to agree, so if your discovery layer does not carry the input contract, the payment path becomes the discovery path. An agent learns what you require by paying and being told no.
It would be satisfying to say the generator dropped a flag. It did not; there was never a flag to drop. Our schema file maps each service to a flat list of accepted field names โ ["cards", "numbers", "nums"], ["input", "text"] โ with no distinction between required and optional. That file is assembled from a comment header in each handler, and those headers list names too.
So the information exists only in the handler body, as validation logic, in a form no generator can read without guessing. We are deliberately not guessing: an inferred required is a new claim that can be wrong in the same direction as the one we are trying to fix. The fix is to declare requiredness where the field list is already declared, propagate it through, and emit it โ and until that exists, documented-optional is at least consistent with the document, even though it is not consistent with the handler.
Two habits make this class of failure cheap to avoid, and neither requires the seller to change anything:
For sellers, the inverse is the one that pays: every input constraint you leave unstated is a call your customer will make, once, and lose. Publishing minProperties, or a required array, or even a sentence in the operation description, converts a refusal into a request your client never sends.
There is a second question hiding behind the first: when a call is refused for a bad request, should the money come back? It is a policy decision about who owns an error the caller made against a service that did nothing, and it is genuinely arguable in both directions โ in a per-call rail, adjudicating it after the fact costs a transaction and a reconciliation path.
We are not going to settle that here, and it is not the interesting part. The interesting part is that it is a question you only have to answer because the request was made at all. A contract that states its constraints turns the policy question into a no-op: the call that would have been refused is never made, no money moves, and nobody has to decide what justice looks like for one cent.
If you take one thing: silence in a schema is not the absence of a claim. required: false is a claim, and a client that is spending money on every request will act on it exactly as written.