<?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: ukenal</title>
    <description>The latest articles on DEV Community by ukenal (@ukenal).</description>
    <link>https://dev.to/ukenal</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%2F4044531%2F113fb473-507e-490b-9161-155eb6643d7c.jpg</url>
      <title>DEV Community: ukenal</title>
      <link>https://dev.to/ukenal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ukenal"/>
    <language>en</language>
    <item>
      <title>Migrating a live x402 service from V1 to V2 on Base mainnet</title>
      <dc:creator>ukenal</dc:creator>
      <pubDate>Thu, 23 Jul 2026 21:53:13 +0000</pubDate>
      <link>https://dev.to/ukenal/migrating-a-live-x402-service-from-v1-to-v2-on-base-mainnet-1mg9</link>
      <guid>https://dev.to/ukenal/migrating-a-live-x402-service-from-v1-to-v2-on-base-mainnet-1mg9</guid>
      <description>&lt;h1&gt;
  
  
  x402 V1 → V2 Migration Guide
&lt;/h1&gt;

&lt;p&gt;Field notes from migrating a &lt;strong&gt;live, payment-gated API&lt;/strong&gt; from x402 V1 to V2 on Base mainnet — the undocumented gotchas, working code from a service in production, and a checklist at the bottom.&lt;/p&gt;

&lt;p&gt;Written from &lt;a href="https://github.com/ukenal/x402ai" rel="noopener noreferrer"&gt;x402ai&lt;/a&gt;, which settles real USDC on Base via the Coinbase CDP facilitator. Every code sample here is running in production, not reconstructed from the spec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last verified:&lt;/strong&gt; July 2026 · &lt;code&gt;@x402/*&lt;/code&gt; 2.19.0 · &lt;code&gt;@coinbase/x402&lt;/code&gt; 2.1.0&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In a hurry?&lt;/strong&gt; → Migration checklist&lt;/p&gt;




&lt;h2&gt;
  
  
  Why you're probably being forced into this
&lt;/h2&gt;

&lt;p&gt;If you built an x402 service before mid-2026, you're on V1. You may not have noticed, because V1 services keep serving 402s and keep settling payments. Nothing crashes. The migration pressure comes from two directions instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovery rejects you.&lt;/strong&gt; Submitting a V1 origin to x402scan returns, in effect, &lt;em&gt;"x402 v1 response detected — migrate to v2 spec"&lt;/em&gt;, plus a second complaint about the absence of a discovery document. Since x402scan is where autonomous agents actually find services, a V1 service is invisible to the thing that generates traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents are already failing against you silently.&lt;/strong&gt; Check your logs for &lt;code&gt;invalid_payload&lt;/code&gt; errors you never explained. Those are V2-capable clients attempting payment against V1 middleware and bouncing. You've been losing real requests, and the error message doesn't say why.&lt;/p&gt;

&lt;p&gt;That second one is the part I'd flag hardest. I had those errors in my logs for weeks and filed them mentally as noise.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;Five things, in rough order of how much time they'll cost you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The package line moved.&lt;/strong&gt; &lt;code&gt;x402-hono&lt;/code&gt; is frozen at 1.2.0. The live line is the scoped &lt;code&gt;@x402/*&lt;/code&gt; namespace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The challenge moved from the body to a header.&lt;/strong&gt; A V2 402 has an &lt;em&gt;empty&lt;/em&gt; JSON body and carries the challenge base64-encoded in a &lt;code&gt;payment-required&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network identifiers are CAIP-2 now.&lt;/strong&gt; &lt;code&gt;eip155:8453&lt;/code&gt; rather than the old string form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDP facilitator auth is supported natively.&lt;/strong&gt; If you were reaching into SDK internals to mint JWTs, you can delete that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;X-Forwarded-Proto&lt;/code&gt; is ignored.&lt;/strong&gt; If you terminate TLS at a tunnel or proxy, your resource URLs will advertise as &lt;code&gt;http://&lt;/code&gt; unless you set them explicitly.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The package landscape
&lt;/h2&gt;

&lt;p&gt;This tripped me up before I wrote a line of code, because searching for x402 packages surfaces the frozen ones first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frozen (V1):&lt;/strong&gt; &lt;code&gt;x402-hono&lt;/code&gt; @ 1.2.0 — final release, do not build on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live (V2):&lt;/strong&gt; the scoped namespace —&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@x402/hono&lt;/code&gt; (or your framework's equivalent)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@x402/core&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@x402/evm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@x402/extensions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@coinbase/x402&lt;/code&gt; — for CDP facilitator config&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At time of writing the &lt;code&gt;@x402/*&lt;/code&gt; packages were at 2.19.0 and &lt;code&gt;@coinbase/x402&lt;/code&gt; at 2.1.0. Check current versions; this line moves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install gotcha
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @x402/hono@2 @x402/core@2 @x402/evm@2 @coinbase/x402@2 &lt;span class="nt"&gt;--legacy-peer-deps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will likely need &lt;code&gt;--legacy-peer-deps&lt;/code&gt;. The optional paywall peer dependency wants React 19; if your project is on React 18 (or has any other React in the tree), npm refuses the install outright. The paywall is optional and unrelated to server-side payment gating, so the flag is safe here — but know &lt;em&gt;why&lt;/em&gt; you're using it rather than reflexively adding it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The server rewrite
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before (V1):&lt;/strong&gt; middleware took a routes object and a facilitator, with auth headers you assembled yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After (V2):&lt;/strong&gt; you build a resource server, register a scheme against a network, then hand that to the middleware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch the import subpaths
&lt;/h3&gt;

&lt;p&gt;This cost me time before a single line of logic ran. The V2 packages use subpath exports, and the obvious top-level import is not the one you want:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;paymentMiddleware&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@x402/hono&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;x402ResourceServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HTTPFacilitatorClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@x402/core/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createFacilitatorConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@coinbase/x402&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ExactEvmScheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@x402/evm/exact/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@x402/core/server&lt;/code&gt;, not &lt;code&gt;@x402/core&lt;/code&gt;. &lt;code&gt;@x402/evm/exact/server&lt;/code&gt;, not &lt;code&gt;@x402/evm&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the handler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;facilitatorClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HTTPFacilitatorClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;createFacilitatorConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CDP_API_KEY_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CDP_API_KEY_PRIVATE_KEY&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;resourceServer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;x402ResourceServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;facilitatorClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eip155:8453&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExactEvmScheme&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;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST /api/embed&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="na"&gt;accepts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/embed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eip155:8453&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;payTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WALLET_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/embed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&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="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Text embeddings (768-dim) via nomic-embed-text. POST {input: string}.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...one entry per paid route&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;paymentMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resourceServer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;register()&lt;/code&gt; is fluent — it returns the resource server, so it chains off the constructor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route config shape changed
&lt;/h3&gt;

&lt;p&gt;V1 nested &lt;code&gt;description&lt;/code&gt; under a &lt;code&gt;config&lt;/code&gt; key — and silently ignored a top-level &lt;code&gt;description&lt;/code&gt;, which is a fun thing to discover after wondering why your route descriptions never showed up anywhere.&lt;/p&gt;

&lt;p&gt;V2 keys routes by &lt;code&gt;METHOD /path&lt;/code&gt;, takes &lt;code&gt;accepts&lt;/code&gt; as a single object rather than an array, and puts &lt;code&gt;description&lt;/code&gt; and &lt;code&gt;mimeType&lt;/code&gt; at the top level as siblings of &lt;code&gt;accepts&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deleting the JWT hack
&lt;/h2&gt;

&lt;p&gt;If you were on CDP's facilitator under V1, there's a decent chance you did what I did: imported &lt;code&gt;generateJwt&lt;/code&gt; through &lt;code&gt;@coinbase/cdp-sdk&lt;/code&gt;'s internal &lt;code&gt;_cjs&lt;/code&gt; path via &lt;code&gt;createRequire&lt;/code&gt;, then hand-built auth headers on every request.&lt;/p&gt;

&lt;p&gt;That was never supported. It worked, but it depended on the internal module layout of someone else's package.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;createFacilitatorConfig(apiKeyId, apiKeySecret)&lt;/code&gt; from &lt;code&gt;@coinbase/x402&lt;/code&gt; is the supported replacement. The entire hack — the &lt;code&gt;createRequire&lt;/code&gt; shim, the internal import, the &lt;code&gt;createAuthHeaders&lt;/code&gt; function — deletes cleanly. This was the single most satisfying part of the migration and the strongest argument for doing it even if discovery weren't forcing your hand.&lt;/p&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;https&lt;/code&gt; gotcha — and the workaround you can now delete
&lt;/h2&gt;

&lt;p&gt;Under V1, if you terminated TLS upstream (Cloudflare tunnel, nginx, anything), the middleware saw the internal request as plain HTTP and advertised your resource URLs as &lt;code&gt;http://&lt;/code&gt;. The standard fix was middleware that injected forwarded headers:&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="c1"&gt;// V1-era workaround&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;c&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;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Forwarded-Proto&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="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;c&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;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Forwarded-Host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&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;&lt;strong&gt;V2 ignores those headers.&lt;/strong&gt; So the workaround silently stops doing anything, your resource URLs revert to &lt;code&gt;http://&lt;/code&gt;, and the middleware that was supposed to prevent exactly that is still sitting in your file looking load-bearing.&lt;/p&gt;

&lt;p&gt;The V2 fix is the explicit &lt;code&gt;resource&lt;/code&gt; field per route, shown above. Runtime resolution is effectively &lt;code&gt;routeConfig.resource || adapter.getUrl()&lt;/code&gt; — anything you set explicitly wins, anything you don't falls back to the derived (wrong) URL. Set it on every paid route; there is no global override.&lt;/p&gt;

&lt;p&gt;Once that's in place, delete the forwarded-header middleware. I still had mine months after the migration, which is how this section came to be written.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verifying your 402
&lt;/h2&gt;

&lt;p&gt;This is where people think the migration broke something.&lt;/p&gt;

&lt;p&gt;A correct V2 402 returns an &lt;strong&gt;empty JSON body&lt;/strong&gt; — literally &lt;code&gt;{}&lt;/code&gt;. The challenge lives in the &lt;code&gt;payment-required&lt;/code&gt; response header, base64-encoded. If you're used to V1's body-carried challenge, an empty body reads like a bug.&lt;/p&gt;

&lt;p&gt;To check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-api.example/api/endpoint &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"input":"check"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; payment-required &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/payment-required: //'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\r'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're looking for &lt;code&gt;x402Version: 2&lt;/code&gt;, your CAIP-2 network (&lt;code&gt;eip155:8453&lt;/code&gt; for Base mainnet), the correct asset and &lt;code&gt;payTo&lt;/code&gt; address, and a &lt;code&gt;resource&lt;/code&gt; field that starts with &lt;code&gt;https://&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two things worth adding while you're in there
&lt;/h2&gt;

&lt;p&gt;Neither is required by V2, but both are easier to do during a migration than to retrofit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate input &lt;em&gt;before&lt;/em&gt; the payment gate.&lt;/strong&gt; Register your validation middleware ahead of the payment middleware so a malformed request gets a &lt;code&gt;400&lt;/code&gt; instead of a &lt;code&gt;402&lt;/code&gt;. Otherwise you're asking an agent to pay before you tell it the request was never going to work — which is bad behavior, and worse when the payer is an automated client with no human to notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guard the startup window.&lt;/strong&gt; The payment handler is built asynchronously (it constructs a facilitator client), so there's a window where the server can accept connections before the handler exists. A delegate middleware that returns &lt;code&gt;503 service initializing&lt;/code&gt; until the handler resolves is cheap insurance against serving unprotected routes during a restart.&lt;/p&gt;




&lt;h2&gt;
  
  
  Client side
&lt;/h2&gt;

&lt;p&gt;The client rewrite is smaller than the server one, and the same subpath trap applies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;x402&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;x402ClientConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SchemeRegistration&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;x402.mechanisms.evm.exact.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ExactEvmScheme&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;x402.http.clients.requests&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;wrapRequestsWithPaymentFromConfig&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;x402ClientConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;schemes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;SchemeRegistration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eip155:8453&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ExactEvmScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;account&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;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wrapRequestsWithPaymentFromConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire integration. The wrapper handles the whole dance — sends the unpaid request, receives the 402, decodes the header challenge, signs the EIP-3009 authorization, retries. You never hand-parse a challenge.&lt;/p&gt;

&lt;h3&gt;
  
  
  The settlement receipt is also in a header
&lt;/h3&gt;

&lt;p&gt;Symmetric with the request side, and easy to miss: on success, settlement details come back base64-encoded in an &lt;code&gt;X-PAYMENT-RESPONSE&lt;/code&gt; header (some stacks emit &lt;code&gt;payment-response&lt;/code&gt; — check both). Decode it for the transaction hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;settle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-PAYMENT-RESPONSE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment-response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://basescan.org/tx/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;transaction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're logging anything about payments, that header is where the on-chain proof lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your payer wallet doesn't need ETH
&lt;/h3&gt;

&lt;p&gt;Worth stating plainly because it surprises people: &lt;strong&gt;EIP-3009 is gasless for the payer.&lt;/strong&gt; The buyer signs a transfer authorization; the facilitator submits and pays gas. A payer wallet needs USDC on Base and nothing else — no ETH, ever.&lt;/p&gt;

&lt;p&gt;This is also why a legitimate agent wallet can show zero self-submitted transactions on a block explorer while having successfully paid you many times. When I got my first payment from an unfamiliar address, its transaction history was empty, which reads as suspicious until you remember the payer never submits anything.&lt;/p&gt;

&lt;p&gt;Keep your V1 client around as a &lt;code&gt;.bak&lt;/code&gt; until the V2 one settles. Mine earned its keep.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discovery: the part that's actually required
&lt;/h2&gt;

&lt;p&gt;Migrating to V2 isn't sufficient for x402scan. It also wants a discovery document, and in practice &lt;strong&gt;&lt;code&gt;/openapi.json&lt;/code&gt; is what gets crawled.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An OpenAPI 3.1 document, served &lt;em&gt;before&lt;/em&gt; your payment gate, with per-endpoint payment metadata:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;x-payment-info&lt;/code&gt; carrying a fixed decimal USD amount (&lt;code&gt;0.010000&lt;/code&gt;, not &lt;code&gt;0.01&lt;/code&gt; — use consistent decimal precision)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protocols: [{ x402: {} }]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;request and response schemas&lt;/li&gt;
&lt;li&gt;the 402 response documented&lt;/li&gt;
&lt;li&gt;free endpoints (&lt;code&gt;/health&lt;/code&gt;, discovery routes) marked &lt;code&gt;security: []&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three things I'd flag:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Match your response field names to your actual handlers.&lt;/strong&gt; My OpenAPI advertised generic field names while the handlers returned &lt;code&gt;embedding&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt;, and &lt;code&gt;answer&lt;/code&gt;. A crawler probing the documented shape gets a mismatch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unify your request field names before you publish the spec.&lt;/strong&gt; My three endpoints had grown independently and took &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt;, and &lt;code&gt;question&lt;/code&gt; respectively — while the OpenAPI doc advertised &lt;code&gt;input&lt;/code&gt; for all three. Two of the three would have failed any automated probe. Standardizing on one field name across every endpoint is worth doing regardless; a public spec makes it mandatory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two different price representations coexist, and it's easy to mix them up.&lt;/strong&gt; The V1-shaped &lt;code&gt;/.well-known/x402&lt;/code&gt; manifest expresses price in atomic token units as a string — &lt;code&gt;'10000'&lt;/code&gt; for $0.01 USDC, which has six decimals. The OpenAPI &lt;code&gt;x-payment-info&lt;/code&gt; block expresses it as decimal USD — &lt;code&gt;'0.010000'&lt;/code&gt;. Same price, two encodings, and no error if you put the wrong one in the wrong place.&lt;/p&gt;

&lt;p&gt;Worth noting: my &lt;code&gt;/.well-known/x402&lt;/code&gt; manifest was still V1-shaped (&lt;code&gt;x402Version: 1&lt;/code&gt;, &lt;code&gt;network: 'base'&lt;/code&gt; rather than &lt;code&gt;eip155:8453&lt;/code&gt;) at registration time and nothing complained, because registration went through &lt;code&gt;/openapi.json&lt;/code&gt;. If you're short on time, prioritize the OpenAPI doc — but know you're carrying a stale manifest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two things that cost me time and aren't written down anywhere
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The CDP facilitator has a minimum price floor.&lt;/strong&gt; Price anything below it and settlement fails. I ended up at $0.01 / $0.02 / $0.04 across three endpoints, which sits just above it. If your business model assumed sub-cent pricing, verify against the current floor before building around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;x402.org&lt;/code&gt;'s facilitator is testnet-only.&lt;/strong&gt; It's the obvious default when you're reading the spec, and it will not settle on mainnet. For Base mainnet you need CDP's, or another mainnet-capable facilitator.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Confirm you're on V1 (check for &lt;code&gt;invalid_payload&lt;/code&gt; errors in your logs)&lt;/li&gt;
&lt;li&gt;[ ] Install &lt;code&gt;@x402/*&lt;/code&gt; scoped packages with &lt;code&gt;--legacy-peer-deps&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Use the correct subpaths: &lt;code&gt;@x402/core/server&lt;/code&gt;, &lt;code&gt;@x402/evm/exact/server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Rewrite handler: &lt;code&gt;HTTPFacilitatorClient&lt;/code&gt; → &lt;code&gt;x402ResourceServer&lt;/code&gt; → &lt;code&gt;.register('eip155:8453', new ExactEvmScheme())&lt;/code&gt; → &lt;code&gt;paymentMiddleware&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Delete any JWT/internal-path hack; use &lt;code&gt;createFacilitatorConfig&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Restructure routes: method+path keys, &lt;code&gt;accepts&lt;/code&gt; object, sibling &lt;code&gt;description&lt;/code&gt;/&lt;code&gt;mimeType&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Add explicit &lt;code&gt;resource&lt;/code&gt; field per paid route (fixes &lt;code&gt;http://&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Delete the now-dead &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; middleware&lt;/li&gt;
&lt;li&gt;[ ] Verify 402 via the &lt;code&gt;payment-required&lt;/code&gt; header, not the body&lt;/li&gt;
&lt;li&gt;[ ] Move input validation ahead of the payment gate (400, not 402)&lt;/li&gt;
&lt;li&gt;[ ] Rewrite the client; keep V1 as &lt;code&gt;.bak&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Confirm on-chain settlement with a real (small) payment — tx hash is in the &lt;code&gt;X-PAYMENT-RESPONSE&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;[ ] Unify request field names across endpoints&lt;/li&gt;
&lt;li&gt;[ ] Publish OpenAPI 3.1 at &lt;code&gt;/openapi.json&lt;/code&gt;, before the payment gate&lt;/li&gt;
&lt;li&gt;[ ] Match documented response fields to actual handler output&lt;/li&gt;
&lt;li&gt;[ ] Submit origin to x402scan&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  If you're starting now
&lt;/h2&gt;

&lt;p&gt;Do the OpenAPI document first, even before the code migration. It forces you to confront your API's inconsistencies — mismatched field names, undocumented response shapes, endpoints that grew independently — while you can still fix them freely. I did it last and had to go back and standardize things I'd have caught earlier.&lt;/p&gt;

&lt;p&gt;And migrate before you need to. The V1 stack still works right up until the moment discovery matters, and by then you're doing an urgent migration instead of a calm one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Landy Ukena. I run &lt;a href="https://api.x402ai.dev" rel="noopener noreferrer"&gt;x402ai&lt;/a&gt; — three payment-gated AI inference endpoints on Base mainnet, self-hosted and settling real USDC via the Coinbase CDP facilitator. &lt;a href="https://www.linkedin.com/in/landyukena/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>api</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
