📅 Historical page. 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. → See current
← All posts

Five places your catalogue hands out an endpoint URL

November 12, 2026

On 12 November we moved three services off a URL that could not take money. The interesting part was not the fix. It was discovering, one surface at a time, how many places a catalogue had already told buyers to go there.

The failure: a 402 that advertises a header nobody reads

Three Solana endpoints — x402-sol-report, x402-sol-onchain, x402-sol-find — were listed at /x402/sol/<action>. That path is served by a handler with three exits: a trial branch, a prepaid-credit branch, and a 4xx pass-through. It never reads X-PAYMENT or PAYMENT-SIGNATURE, and the route is registered without the payment verifier that the sibling /x402/:id registration passes in.

The 402 it returned was not obviously broken. Same message, same accepts[0], same payTo as the good door. Its nextSteps[0] said:

Pay: include PAYMENT-SIGNATURE (V2) or X-PAYMENT (V1)

So an agent following our own discovery documents did exactly what the challenge said, and the header it sent was never read. Two doors, one character apart in the path, one able to settle and one not — and nothing machine-readable said which was meant.

Live is not the same as buyable. Every "is this endpoint alive" probe we ran saw a clean 402 the whole time. Liveness checks the door is open. It does not check that the person standing behind it can take cash.

The five surfaces

A catalogue is not one document. These all named an endpoint URL for the same services:

SurfaceWhat it isPopulation on 12 Nov
/api/servicesthe catalogue itself1,875 rows
/.well-known/x402the discovery manifest1,956 resource URLs
/openapi.jsongenerated from the catalogue1,875 paths
/health.jsona daily liveness report, rendered by a public page1,875 rows
served pagesdocs, listings, examples690 literal /x402/ references

When we fixed the catalogue, three of these agreed within the hour. The fourth did not: /health.json is rewritten once a day by a probe job, and between 05:43 and the next 05:13 it kept handing the old alias URL to a page a buyer can actually read. The window is bounded — at most one probe cycle — but it is a window in which our own buyer-facing page pointed at a door that cannot settle, and no guard existed to say so.

That is the general shape worth stealing: the failure lived in the join between two things we already had. The catalogue knew an endpoint per service. The route table knew, per route, whether the handler can settle. Nobody had ever joined them.

The join, in one predicate

for each published endpoint URL:
    path  = url after the host
    route = first match in the route table (literal segments first)
    require route.class == "paywalled"

Two details do most of the work.

Literal segments before parameters. A table that matches /x402/:id before /x402/uuid-go will judge a free fast-path as a paid door and hide exactly the exception you were looking for. Sort candidates by number of literal characters, not by the order you happened to write them down.

The class has to mean "can settle", not "is named paywalled". Alias families — /svc/:name, prepaid top-up doors, redirect shims — commonly return a 402 that looks payable and still do not read a payment header. If the route table calls them paywalled, the table is the bug. We keep the settlement capability as a separate class and treat it as a failure when an advertised endpoint lands there.

Run over today's data, the predicate says: 1,875 catalogue rows, 1,956 manifest resources, 1,875 OpenAPI paths, 1,875 health rows, and every non-blog /x402/ path on a served page, all resolve to a route that has a settlement branch. Before the fix, three rows did not.

Two footnotes on the page scan, because it is the messiest population. Of the literal paths found in served pages, 690 matched a paywalled route; three matched a free Go fast-path (/x402/uuid-go, documented in a discovery table and returning a UUID without payment); and a dozen were GitHub URL fragments that a regex picked up and no reader would mistake for an endpoint. Dated posts under /blog/ were excluded — they record what was true when they were written.

What the check does not cover

Three honest limits, because a published audit that hides its blind spots is a brochure:

For anyone publishing x402 endpoints

If you publish a catalogue, write the join before you write the next listing:

  1. Enumerate every surface that names an endpoint URL. Include the boring ones — health reports, status pages, generated SDKs, the example in your README.
  2. Build the route table by hand once, then make a guard prove that every route registered in the deployed source appears in it. A table that silently misses a route turns "advertised on a door that cannot settle" into "no route matches", which is the same red for a different reason.
  3. Judge capability, not naming. The 402 body is not evidence: it is generated before the handler looks at the request.
  4. Run the predicate against a pre-fix copy of the surface as a control. If the saved bytes do not reproduce the reds you saw live, your population moved and your green means nothing.

The alias door still exists at the gateway, and it still answers with a 402 whose instructions assume a payment header is read. Removing it is a routing change we have not made; the catalogue, the manifest, OpenAPI and the health report now all point at the settle-capable spelling. That is an honest partial fix, and it is written down here as one.

← All posts