<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: labstek9-glitch</title>
    <description>The latest articles on DEV Community by labstek9-glitch (@labstek9glitch).</description>
    <link>https://dev.to/labstek9glitch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4090404%2F52dd7533-edb0-4cf0-a062-fbfa6809e540.png</url>
      <title>DEV Community: labstek9-glitch</title>
      <link>https://dev.to/labstek9glitch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/labstek9glitch"/>
    <language>en</language>
    <item>
      <title>HTTP 402 was the easy part — x402scan still refused the tunnel</title>
      <dc:creator>labstek9-glitch</dc:creator>
      <pubDate>Sun, 23 Aug 2026 04:26:56 +0000</pubDate>
      <link>https://dev.to/labstek9glitch/http-402-was-the-easy-part-x402scan-still-refused-the-tunnel-344h</link>
      <guid>https://dev.to/labstek9glitch/http-402-was-the-easy-part-x402scan-still-refused-the-tunnel-344h</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The protocol looked finished when &lt;code&gt;curl&lt;/code&gt; printed &lt;code&gt;402&lt;/code&gt;. The catalog disagreed. An ephemeral Cloudflare tunnel was a perfect Payment Required and still not a listing. The smash was realizing those are different bugs.&lt;/p&gt;

&lt;h1&gt;
  
  
  HTTP 402 was the easy part — x402scan still refused the tunnel
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The Setting
&lt;/h2&gt;

&lt;p&gt;Tek Labs needed an HTTP service that speaks &lt;a href="https://github.com/x402-foundation/x402/blob/main/specs/transports-v2/http.md" rel="noopener noreferrer"&gt;x402 v2&lt;/a&gt; on the wire and can lint any URL for the same shape. The service is &lt;code&gt;x402-linter&lt;/code&gt;: it is itself gated by Payment Required. &lt;code&gt;POST /lint&lt;/code&gt; advertises 10000 atomic USDC ($0.01). &lt;code&gt;POST /health&lt;/code&gt; advertises 1000 ($0.001). Both use scheme &lt;code&gt;exact&lt;/code&gt;, network &lt;code&gt;eip155:8453&lt;/code&gt; (Base), asset &lt;code&gt;0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&lt;/code&gt; (Base USDC), and &lt;code&gt;payTo&lt;/code&gt; &lt;code&gt;0xAe51ada17E9C47B4de6E1b2405049C79D1BBE3fA&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first build was local-only. No wallet keys, no facilitator, no settlement. A missing &lt;code&gt;PAYMENT-SIGNATURE&lt;/code&gt; always 402s unless a local bypass header is set. The job was never "ship a paid product." It was: return a valid v2 Payment Required, then see whether a public catalog (x402scan) would accept the origin. Those two checks are not the same.&lt;/p&gt;

&lt;p&gt;The live origin now is &lt;code&gt;https://tek-labs-402-linter.labstek9.workers.dev&lt;/code&gt;. Getting there is the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Sign
&lt;/h2&gt;

&lt;p&gt;Unpaid &lt;code&gt;POST /lint&lt;/code&gt; already returned HTTP 402 with a JSON &lt;code&gt;PaymentRequired&lt;/code&gt; body. A local smoke against our own &lt;code&gt;/health&lt;/code&gt; passed the status check. That felt like the protocol was done.&lt;/p&gt;

&lt;p&gt;It was not. Two failures showed up in order.&lt;/p&gt;

&lt;p&gt;First, our own linter scored the same 402 as incomplete. Status 402 is only one finding. The v2 HTTP transport also wants a &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; header: base64 of the same JSON as the body. Without it the lint report marks &lt;code&gt;missing_header&lt;/code&gt;. Cross-origin clients never see a custom header unless CORS exposes it. Returning 402 and stopping there is a protocol bug even when the body looks right.&lt;/p&gt;

&lt;p&gt;Second, we pointed a trycloudflare hostname at the same process so a catalog could fetch it. x402scan did not argue about &lt;code&gt;x402Version&lt;/code&gt; or &lt;code&gt;accepts&lt;/code&gt;. It refused the origin:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tunnel URLs are ephemeral and can't be reliably discovered by agents. Deploy your API to a permanent URL to register.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The origin was &lt;code&gt;https://arranged-players-computation-pipeline.trycloudflare.com&lt;/code&gt;. That is a tunnel, not a listing. &lt;code&gt;registerFromOrigin&lt;/code&gt; was never going to stick on a hostname that dies with the process.&lt;/p&gt;

&lt;p&gt;A correct 402 on localhost, and even a correct 402 on a public tunnel, still failed the thing we actually wanted: durable discovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Investigation
&lt;/h2&gt;

&lt;p&gt;We treated the 402 as a checklist, not a status code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status.&lt;/strong&gt; Unpaid &lt;code&gt;POST /lint&lt;/code&gt; and &lt;code&gt;POST /health&lt;/code&gt; must be 402. Anything else is &lt;code&gt;http_status&lt;/code&gt; and a hard score cut. That part was already green.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Header.&lt;/strong&gt; The linter reads &lt;code&gt;payment-required&lt;/code&gt; (HTTP is case-insensitive). It base64-decodes the value and &lt;code&gt;JSON.parse&lt;/code&gt;s it. If that fails, the finding is &lt;code&gt;header_b64&lt;/code&gt;. If it succeeds, the same object is walked as the body: &lt;code&gt;x402Version === 2&lt;/code&gt;, &lt;code&gt;resource.url&lt;/code&gt; present, &lt;code&gt;accepts&lt;/code&gt; a non-empty array, first accept using &lt;code&gt;exact&lt;/code&gt;, CAIP-2 &lt;code&gt;network&lt;/code&gt; (colon required), &lt;code&gt;amount&lt;/code&gt; a string of digits, &lt;code&gt;asset&lt;/code&gt;, &lt;code&gt;payTo&lt;/code&gt;, &lt;code&gt;maxTimeoutSeconds&lt;/code&gt; a number. Then we compare decoded header to the JSON body. Match is &lt;code&gt;body_matches_header&lt;/code&gt;. Mismatch is a warn. Body-only 402s can still be graded, but they lose the header points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CORS.&lt;/strong&gt; A browser or a browser-shaped indexer will not expose &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; unless &lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt; lists it. We also needed &lt;code&gt;OPTIONS&lt;/code&gt; 204 and &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; so a fetch from another origin could read the 402 at all. Without expose-headers, the Node server can send the header and a JS client still reports it missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovery documents.&lt;/strong&gt; Status-plus-header is the payment challenge. Catalogs do not only POST your paid route and hope. They look for a machine-readable map: OpenAPI 3.1 at &lt;code&gt;/openapi.json&lt;/code&gt; with &lt;code&gt;x-payment-info&lt;/code&gt; and &lt;code&gt;x-discovery.ownershipProofs&lt;/code&gt;, plus &lt;code&gt;GET /.well-known/x402&lt;/code&gt; listing resource URLs and the same ownership address. Resource URLs are built from the request host and &lt;code&gt;x-forwarded-proto&lt;/code&gt;, so a proxy does not advertise &lt;code&gt;http://127.0.0.1:8402&lt;/code&gt; to the world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The catalog.&lt;/strong&gt; After the wire format was complete, the remaining failure was policy, not JSON. x402scan's message is explicit: ephemeral tunnels are not a discovery target. Agents cannot bookmark a hostname that dies with the process. Cheap permanent hosts we actually had: latest Wrangler wanted Node 22 (the box is Node 20.19.2). GitHub Pages is static and cannot POST 402. &lt;code&gt;*.vercel.app&lt;/code&gt; is disallowed by the catalog. A Cloudflare Worker on a stable &lt;code&gt;*.workers.dev&lt;/code&gt; hostname was the path that worked.&lt;/p&gt;

&lt;p&gt;Wrong theories we dropped: "x402scan is down," "the 402 body is invalid v2," "we need a real &lt;code&gt;PAYMENT-SIGNATURE&lt;/code&gt; to register." None of those matched the tunnel error. The catalog never got as far as verifying payment. It rejected the hostname class.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;Two layers, one story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — incomplete 402.&lt;/strong&gt; HTTP 402 means Payment Required. x402 v2 means a specific object: version 2, a resource, an &lt;code&gt;accepts&lt;/code&gt; list with scheme/network/asset/amount/payTo, plus the same payload in &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; so intermediaries and clients that do not parse the body still see the challenge. CORS that does not expose that header makes the challenge invisible to browsers. A server that only &lt;code&gt;writeHead(402)&lt;/code&gt; and dumps JSON is "402-correct" and still lint-wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — discovery is not the 402.&lt;/strong&gt; OpenAPI and &lt;code&gt;/.well-known/x402&lt;/code&gt; tell an agent &lt;em&gt;what&lt;/em&gt; is paid and &lt;em&gt;who&lt;/em&gt; owns it. They do not make a trycloudflare name stable. A catalog that indexes agents has to refuse origins that vanish. That is not a parse error. It is an origin lifetime check. We confused "the challenge is well-formed" with "this URL can be listed."&lt;/p&gt;

&lt;p&gt;The payTo address is part of both layers: it is &lt;code&gt;accepts[].payTo&lt;/code&gt; on the 402, &lt;code&gt;ownershipProofs&lt;/code&gt; on well-known, and &lt;code&gt;x-discovery.ownershipProofs&lt;/code&gt; on OpenAPI. Same address everywhere. That is ownership, not hosting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local POST /lint     --&amp;gt;  402 + PAYMENT-REQUIRED + CORS   (lint can pass)
well-known + OpenAPI --&amp;gt;  what/who to pay                 (agents can parse)
trycloudflare origin --&amp;gt;  ephemeral                       (x402scan refuses)
workers.dev origin   --&amp;gt;  permanent HTTPS                 (catalog accepts)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;We completed the wire format, then moved the same process onto a permanent Worker.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;send402&lt;/code&gt; writes the header and the body from the same object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;send402&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;paymentRequired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PAYMENT-REQUIRED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;corsHeaders&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CORS exposes the header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;corsHeaders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Methods&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET, POST, OPTIONS, HEAD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Headers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Expose-Headers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PAYMENT-REQUIRED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Discovery is two GETs. &lt;code&gt;/openapi.json&lt;/code&gt; is OpenAPI 3.1 with &lt;code&gt;x-payment-info&lt;/code&gt; (&lt;code&gt;$0.01&lt;/code&gt; lint, &lt;code&gt;$0.001&lt;/code&gt; health) and &lt;code&gt;x-discovery.ownershipProofs: [payTo]&lt;/code&gt;. &lt;code&gt;/.well-known/x402&lt;/code&gt; returns &lt;code&gt;{ version: 1, x402Version: 2, resources: [origin/lint, origin/health], ownershipProofs: [payTo] }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Worker is &lt;code&gt;https://tek-labs-402-linter.labstek9.workers.dev&lt;/code&gt;. Unpaid &lt;a href="https://tek-labs-402-linter.labstek9.workers.dev/lint" rel="noopener noreferrer"&gt;&lt;code&gt;POST /lint&lt;/code&gt;&lt;/a&gt; is still HTTP 402 with the same payTo. &lt;code&gt;GET /openapi.json&lt;/code&gt; is 200. x402scan &lt;code&gt;checkDiscovery&lt;/code&gt; found the OpenAPI map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"openapi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"resourceCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"https://tek-labs-402-linter.labstek9.workers.dev/lint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"authMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"paid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Lint a URL"&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"https://tek-labs-402-linter.labstek9.workers.dev/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"authMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"paid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Health"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="nl"&gt;"ownershipProofs"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"0xAe51ada17E9C47B4de6E1b2405049C79D1BBE3fA"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;registerFromOrigin&lt;/code&gt; then split the two routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"registered"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"openapi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"failedDetails"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"https://tek-labs-402-linter.labstek9.workers.dev/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"validation: Missing input schema — add a requestBody or parameter schema to your OpenAPI spec"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="nl"&gt;"originId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ef9b1c25-6a34-4420-8de4-d114187475d8"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/lint&lt;/code&gt; was accepted on that register call. &lt;code&gt;/health&lt;/code&gt; was not, because OpenAPI had no requestBody. That is a leftover discovery bug, not a payment bug. We did not buy a listing and did not re-pay the catalog search to re-confirm the row. We did not settle USDC. Tradeoff: the Worker still does not verify &lt;code&gt;PAYMENT-SIGNATURE&lt;/code&gt;. Local lint still uses &lt;code&gt;X-TEK-UNPAID-OK: 1&lt;/code&gt;. That is a lab bypass, not a facilitator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Aftermath
&lt;/h2&gt;

&lt;p&gt;Local unpaid 402 is now a real v2 challenge: status, header, body match, schema fields, CORS. Discovery documents live on the same origin. x402scan can fetch them today (&lt;code&gt;checkDiscovery&lt;/code&gt; found the OpenAPI map). A prior &lt;code&gt;registerFromOrigin&lt;/code&gt; accepted &lt;a href="https://tek-labs-402-linter.labstek9.workers.dev/lint" rel="noopener noreferrer"&gt;&lt;code&gt;POST /lint&lt;/code&gt;&lt;/a&gt; as originId &lt;code&gt;ef9b1c25-6a34-4420-8de4-d114187475d8&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What we will not claim: there is no unpaid permalink. &lt;code&gt;/origin/ef9b1c25-6a34-4420-8de4-d114187475d8&lt;/code&gt; and &lt;code&gt;/resource?url=…/lint&lt;/code&gt; both 404. The &lt;a href="https://www.x402scan.com/resources" rel="noopener noreferrer"&gt;Marketplace&lt;/a&gt; HTML does not currently show the name. Catalog search APIs themselves 402 if you do not pay. Discovery working is not the same as a public listing page.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/health&lt;/code&gt; still fails registration for a missing input schema. The process change is the useful part: treat "can an agent find this tomorrow?" as a separate acceptance test from "does curl get 402?" and treat "does OpenAPI describe the body?" as a third test after the origin is stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;A status code is not a protocol. x402 v2 lives in the header, the body, and the CORS expose list. If you only test &lt;code&gt;curl -w '%{http_code}'&lt;/code&gt;, you will ship a 402 that your own linter fails.&lt;/p&gt;

&lt;p&gt;Discovery is not payment. OpenAPI and well-known answer what and who. A registry also asks where, and it will reject a tunnel that answers 402 perfectly. Do not treat a trycloudflare URL as a listing candidate.&lt;/p&gt;

&lt;p&gt;The useful split for the next paid HTTP endpoint: (1) challenge correctness, (2) discovery documents, (3) origin lifetime, (4) OpenAPI request/response schemas the catalog can validate. We finished 1–3. 4 still bites &lt;code&gt;/health&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Submitted for DEV Summer Bug Smash 2026, Smash Stories track.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>webdev</category>
      <category>x402</category>
    </item>
  </channel>
</rss>
