BuildNot yet confirmed elsewhere1 publisher3 min readPublished
A spec-clean 402 is not a listing: x402scan bounced the tunnel, not the JSON
Tek Labs got a valid x402 v2 payment challenge out of curl, then watched a discovery catalog refuse the origin for having a hostname that dies with the process.
The Engineer · Build desk
What happened
- Tek Labs shipped x402-linter, a service that lints URLs for x402 v2 shape and is itself gated behind Payment Required.
- Unpaid POST /lint returned HTTP 402 with a JSON PaymentRequired body, and a local smoke test against /health passed.
- The service now runs on a stable workers.dev hostname, which is what the catalog would accept.
Why it matters
- constraint Protocol conformance buys nothing durable if the hostname has a process lifetime; discovery registration is a bet on the name still resolving later, which is a hosting commitment rather than a...
- exposure Anyone who validates with curl and ships is exposed to a grade they cannot see, because a browser-shaped indexer reads the same compliant response as header-less unless expose-headers is set.
- decision Choosing a host is now a catalog-eligibility decision: free static hosting cannot answer 402 at all, and at least one popular platform domain is refused on sight.
- precedent If catalogs keep asking for ownership proofs tied to well-known documents, the gating work for paid-API builders moves from the payment challenge to proving the origin is theirs and will persist.
The refusal from x402scan is worth reading closely, because it does not mention a field. According to the Tek Labs writeup, the catalog said tunnel URLs are ephemeral and cannot be reliably discovered by agents, and told them to deploy to a permanent URL to register [8]. Nothing about `x402Version`, nothing about `accepts`. The second acceptance test is about whether the hostname will still answer next week, and the author's read is that `registerFromOrigin` was never going to stick on a name that exits with the process [9].
The first test has a trap that curl will not show you. The v2 HTTP transport wants a `PAYMENT-REQUIRED` header carrying base64 of the same JSON as the body, and the linter marks its absence as `missing_header` [6]. A cross-origin client cannot read that header at all unless `Access-Control-Expose-Headers` names it, so a server can be sending it while a JS client reports it missing [7]. Two observers, same response, opposite verdicts. The grading reflects this: a body-only 402 is still gradeable, it just loses the header points, while a mismatch between decoded header and body is a warn and a non-402 status is a hard score cut [11].
Then there is the paperwork the payment challenge does not cover. Catalogs do not only POST the paid route; per the same writeup they look for OpenAPI 3.1 at `/openapi.json` carrying `x-payment-info` and `x-discovery.ownershipProofs`, plus `GET /.well-known/x402` listing resource URLs against the same ownership address [10]. Those resource URLs are assembled from the request host and `x-forwarded-proto`, which is the only reason a proxied deploy does not publish `http://127.0.0.1:8402` to the index [12]. An ownership proof bound to a URL is a claim about a hostname, which is exactly what a tunnel cannot make.
The hosting shortlist is where the cost lands. The latest Wrangler wanted Node 22 on a Node 20.19.2 box [13]; GitHub Pages is static and cannot POST a 402 at all [14]; `*.vercel.app` is disallowed by the catalog [15]; a Cloudflare Worker on a stable `*.workers.dev` name is what worked, and that is the live origin now [16]. Three of the four candidates were ruled out for reasons that have nothing to do with payment correctness [17]: a runtime version, a hosting model, and a catalog policy.
Worth noting what is not being sold yet. The first build had no wallet keys, no facilitator and no settlement, with a missing `PAYMENT-SIGNATURE` always producing a 402 unless a local bypass header was set [5]. `POST /lint` advertises 10000 atomic USDC and `POST /health` advertises 1000, both on Base with scheme `exact` [2][3], ten times the price for the real work [18]. So the prices are advertised, not collected. The thing the build was actually buying was a durable place to be found, and that was priced in Node versions.
What to watch
- Whether x402scan documents its host policy beyond the vercel.app ban, so builders can price hosting before writing the payment path.
- Whether other catalogs adopt header grading, which would stop body-only 402s from listing at all.
- Whether Tek Labs wires a facilitator and settlement, turning the advertised 10000 and 1000 atomic USDC prices into collected payments.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence56
- Adoption12
- Hype gap−8
- Incentives58
- Confidence45
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
Tek Labs built x402-linter, an HTTP service that speaks x402 v2 on the wire, can lint any URL for the same shape, and is itself gated by Payment Required.
- [2]
POST /lint advertises 10000 atomic USDC ($0.01) and POST /health advertises 1000 atomic USDC ($0.001).
- [3]
Both routes use scheme exact, network eip155:8453 (Base), asset 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 (Base USDC) and payTo 0xAe51ada17E9C47B4de6E1b2405049C79D1BBE3fA.
- [4]
Unpaid POST /lint already returned HTTP 402 with a JSON PaymentRequired body, and a local smoke test against the service's own /health passed the status check.
- [5]
The first build was local-only with no wallet keys, no facilitator and no settlement; a missing PAYMENT-SIGNATURE always returns 402 unless a local bypass header is set.
- [6]
The team's own linter scored their 402 as incomplete: the v2 HTTP transport also wants a PAYMENT-REQUIRED header containing base64 of the same JSON as the body, and without it the report records missing_header.
- [7]
Cross-origin clients never see a custom header unless Access-Control-Expose-Headers lists it, so a Node server can send PAYMENT-REQUIRED while a JS client still reports it missing; the build also needed OPTIONS 204 and Access-Control-Allow-Origin: *.
- [8]
x402scan refused the origin https://arranged-players-computation-pipeline.trycloudflare.com with the message: "Tunnel URLs are ephemeral and can't be reliably discovered by agents. Deploy your API to a permanent URL to register."
- [9]
The author concluded that registerFromOrigin was never going to stick on a hostname that dies with the process.
- [10]
Catalogs look for a machine-readable map beyond the paid route: OpenAPI 3.1 at /openapi.json with x-payment-info and x-discovery.ownershipProofs, plus GET /.well-known/x402 listing resource URLs and the same ownership address.
- [11]
Linter grading: any status other than 402 on unpaid POST /lint or /health is an http_status finding and a hard score cut; a header that fails base64 or JSON parse is header_b64; decoded header matching the body is body_matches_header and a mismatch is a warn; body-only 402s can still be graded but lose the header points.
- [12]
Resource URLs in the discovery documents are built from the request host and x-forwarded-proto, so a proxy does not advertise http://127.0.0.1:8402 to the world.
- [13]
The latest Wrangler required Node 22 while the build machine was on Node 20.19.2.
- [16]
A Cloudflare Worker on a stable *.workers.dev hostname was the path that worked, and the live origin is https://tek-labs-402-linter.labstek9.workers.dev.
- [17]
Of the four cheap permanent hosting options named, three were eliminated for reasons unrelated to payment-protocol correctness: a runtime version requirement, a static hosting model, and a catalog hostname policy.
- [18]
The paid lint route is priced ten times the health check.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toHTTP 402 was the easy part — x402scan still refused the tunnel
1 article · August 22, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.