An agent discovers an endpoint, reads the input example in the 402, and builds the call. If it is a POST client, it reads extensions.bazaar.info.input.body โ JSON, whatever shape the handler wants. If it is a GET client, it reads a different field, info.input.queryParams. Those two fields are populated from the same sample, and the map between them is not lossless. When the sample contains an array, the query surface inherits a value a query string physically cannot carry.
This is the sibling of the empty-example trap we wrote about last week. Same class of failure โ a published example that a copy-first caller cannot turn into a successful request โ but a different mechanism, and one that lives only on the GET side of the dispatcher.
A resource does not know in advance how it will be called, so it cannot publish one canonical example. It publishes one per method family, and the method family is decided at request time. On our gateway the dispatch is literal โ handler_x402.go, in the bazaar extension builder:
// bodyType present โ BodyInput (POST/PUT/PATCH); absent โ QueryInput (GET/HEAD/DELETE).
switch c.Request.Method {
case "POST", "PUT", "PATCH":
inp["bodyType"] = "json"
if c.Request.Method == "POST" && len(paramSample) > 0 {
inp["body"] = paramSample
}
default: // GET/HEAD/DELETE โฆ
delete(inp, "bodyType")
delete(inp, "body")
inp["queryParams"] = paramSample
}
paramSample is one map, built once, from the handler's declared fields and the verified examples we hold for them. The case branch publishes it as body; the default branch publishes the same map as queryParams. Nothing in between asks whether a value that is fine in a JSON body is expressible as a query parameter. A JSON body key can hold an array or an object. A query parameter holds a string. That is the gap.
Here is a live example, read straight off our GET surface. The discovery payload says: the caller sends these values as query parameters.
GET /x402/x402-autocorrelation
"input": {
"method": "GET",
"queryParams": {
"lag": 1,
"values": [1, 2, 3]
},
"type": "http"
}
A client that does what the payload says โ copy the keys and values into the query string โ has to render values=[1,2,3]. Whether that works is up to the handler's parser, not the discovery layer's contract. Some handlers coerce a comma-separated string; some reject it; some accept it and compute on the wrong thing. The example is not wrong in the sense of naming a field that does not exist โ it is wrong in the sense that it cannot be sent in the form it is declared. We found four of these in a 48-endpoint sample:
| endpoint | published queryParams value |
|---|---|
| x402-autocorrelation | {"lag": 1, "values": [1, 2, 3]} |
| x402-entropy | {"values": [1, 2, 3, 4]} |
| x402-bollinger-bands | {"multiplier": 2, "period": 5, "values": [1โฆ7]} |
| x402-rotate | {"k": 2, "values": [1, 2, 3, 4]} |
The other shape is the one from the POST-side post: when a field has no verified example, the fallback emits an obviously-unfilled sentinel rather than an empty string, so a caller can tell "we have no value here" from "the value is empty". On the query surface that sentinel arrives in the same field:
"queryParams": {
"pdf": "<PLACEHOLDER: pdf>",
"url": "<PLACEHOLDER: url>"
}
A sentinel is honest โ it names the field and says "fill this yourself". But it is still a value a copy-first client will paste verbatim, and a placeholder is a string like any other, so it passes the type check and fails at the handler. The distinction between the two traps matters: Trap 1 is a shape error the surface did not refuse; Trap 2 is a known-unknown the caller must read as such.
Over a 48-endpoint sample of our 1,693 first-party resources: 37 are plain scalars a GET client can send verbatim, 7 are sentinel placeholders, and 4 carry a value whose type a query string cannot represent. None were blank. This is an upper bound on the trap, not a defect count, and we hold that distinction deliberately: some of those four handlers coerce a comma-separated string and work fine; every endpoint named here also answers on the POST surface, where the example arrives as valid JSON. A caller who posts the body is unaffected. Separating "coerces" from "rejects" needs a real call through each handler, and a paid call to sharpen a count is not a trade we make with someone else's budget.
While reading the dispatcher we found the same family one branch over. On PUT and PATCH, the code sets bodyType: "json" but deliberately does not publish a concrete body โ because the gateway only merges a JSON body into the input on POST. A PUT caller would get an empty object and a wrong-or-empty result instead of an error. The code comment states the reasoning plainly: giving a caller a field list it will then have silently ignored is worse than omitting it. That is the same rule as this whole post, applied one level up: do not publish an input contract you will not honour.
The dispatch is quoted from the deployed gateway source (internal/httpapi/handler_x402.go, method-family switch); the four array-valued examples and the 11/48 split are read live from our own 402 discovery payloads on 2026-10-14. No figure here is estimated and none is drawn from a third party.