{"uid":"cap_pGNZedXgDresKNGE_ZroI","slug":"www-paylog-dev-b543f4e9","name":"paylog.dev x402 Implementation Article","description":"8 Things That Tripped Me Up Implementing x402","url":"https://www.paylog.dev/api/v1/content/x402-implementation","method":"POST","headers":{},"bodySchema":null,"responseSchema":null,"example":{"request":{"slug":"x402-implementation"},"response":{"date":"2026-04-05","slug":"x402-implementation","title":"8 Things That Tripped Me Up Implementing x402","content":"<h2>Background</h2>\n<p>Adding x402 (Base) support to paylog.dev had more edge cases than I expected. This post documents the eight specific bugs I hit and how I fixed each one.</p>\n<hr>\n<h2>1. x402.org is testnet-only</h2>\n<p>x402.org is a <strong>Base Sepolia testnet</strong> facilitator. Send it a Base mainnet payment and it returns <code>invalid_payload</code> every time, with no further explanation.</p>\n<pre><code>// sending a Base mainnet payment to x402.org\n{ &quot;success&quot;: false, &quot;error&quot;: &quot;invalid_payload&quot; }\n</code></pre>\n<p>For mainnet you need the CDP facilitator (<code>https://api.cdp.coinbase.com/platform/v2/x402</code>) plus a set of CDP API credentials (<code>CDP_API_KEY_ID</code> + <code>CDP_API_KEY_SECRET</code>).</p>\n<p><strong><a href=\"mailto:x402-next@1.1.0\">x402-next@1.1.0</a></strong> silently falls back to x402.org when <code>facilitatorConfig</code> is <code>undefined</code>. Easy to miss in production.</p>\n<hr>\n<h2>2. Misreading the EIP-3009 <code>from</code> field</h2>\n<p>My first implementation filtered Alchemy&#39;s <code>alchemy_getAssetTransfers</code> response like this:</p>\n<pre><code class=\"language-typescript\">// ❌ wrong\nconst CDP_FACILITATOR = &#39;0x...&#39;\nif (t.from.toLowerCase() !== CDP_FACILITATOR.toLowerCase()) continue\n</code></pre>\n<p>Here is what EIP-3009 (Transfer With Authorization) actually does:</p>\n<ol>\n<li>The user signs an authorization and hands it to the facilitator.</li>\n<li>The facilitator calls <code>transferWithAuthorization</code> on-chain.</li>\n<li>The ERC20 Transfer event&#39;s <code>from</code> field is the <strong>user wallet</strong>, not the facilitator.</li>\n</ol>\n<p>Alchemy surfaces the ERC20 event&#39;s <code>from</code> directly, so <code>t.from === user wallet</code>. Since the Alchemy request already has <code>fromAddress=wallet</code>, the facilitator filter was redundant and was dropping every result.</p>\n<hr>\n<h2>3. CAIP-2 network string mismatch</h2>\n<p><code>services-x402.json</code> is collected from the Bazaar / CDP discovery API, which returns network identifiers in <strong>CAIP-2 format</strong> (<code>&quot;eip155:8453&quot;</code>):</p>\n<pre><code class=\"language-json\">{ &quot;network&quot;: &quot;eip155:8453&quot;, &quot;payTo&quot;: &quot;0x...&quot; }\n</code></pre>\n<p>The filter I had written expected the plain string <code>&quot;base&quot;</code>:</p>\n<pre><code class=\"language-typescript\">// ❌ wrong\n.filter(e =&gt; e.network === &#39;base&#39;)\n</code></pre>\n<p>All 104 entries were filtered out. <code>X402_SERVICE_MAP</code> was completely empty and service names never resolved. It ran that way silently for days.</p>\n<pre><code class=\"language-typescript\">// ✅ fixed\nconst BASE_NETWORKS = new Set([&#39;base&#39;, &#39;eip155:8453&#39;])\n.filter(e =&gt; BASE_NETWORKS.has(e.network))\n</code></pre>\n<hr>\n<h2>4. Wrong HTTP method on the <code>supported</code> JWT</h2>\n<p>The CDP facilitator&#39;s <code>supported</code> endpoint is a GET, but I was signing the JWT with POST:</p>\n<pre><code class=\"language-typescript\">// ❌ wrong\nsupported: await makeHeader(&#39;/platform/v2/x402/supported&#39;)  // defaults to POST\n\n// ✅ fixed\nsupported: await makeHeader(&#39;/platform/v2/x402/supported&#39;, &#39;GET&#39;)\n</code></pre>\n<p>CDP&#39;s JWT auth embeds the request method in the token. A method mismatch causes an authentication error.</p>\n<hr>\n<h2>5. <code>invalidMessage</code> gets silently dropped</h2>\n<p><a href=\"mailto:x402-next@1.1.0\">x402-next@1.1.0</a>&#39;s <code>VerifyError</code> discards the <code>invalidMessage</code> field that CDP includes in verify failures. All you see is the string <code>&quot;invalid_payload&quot;</code> — no further context.</p>\n<p>To surface the real message you need a proxy:</p>\n<pre><code>CDP_DEBUG=true → /api/v1/x402/cdp-proxy/[verify|settle|supported] → CDP\n</code></pre>\n<p>The proxy logs the full response:</p>\n<pre><code>[cdp-proxy/verify] invalidReason: invalid_payload\n| invalidMessage: transferWithAuthorization.from does not match expected payer\n| payer: 0x...\n</code></pre>\n<hr>\n<h2>6. <code>useFacilitator</code> makes its own HTTP calls</h2>\n<p><a href=\"mailto:x402@1.1.0\">x402@1.1.0</a>&#39;s <code>useFacilitator</code> fetches <code>facilitatorConfig.url</code> directly. There is no hook to intercept those requests from outside.</p>\n<p>I spent time writing a <code>cdpFetch</code> wrapper inside route.ts with logging, only to discover it was never called. The fix was a proper proxy route at <code>/api/v1/x402/cdp-proxy/[...path]</code>.</p>\n<hr>\n<h2>7. Unset <code>X402_RECIPIENT_ADDRESS</code> silently burns revenue</h2>\n<pre><code class=\"language-typescript\">const X402_RECIPIENT_ADDRESS = (\n  process.env.X402_RECIPIENT_ADDRESS ?? &#39;0x0000000000000000000000000000000000000000&#39;\n) as `0x${string}`\n</code></pre>\n<p>Forget to set the environment variable and every payment goes to the zero address. The code runs without errors; the money is just gone.</p>\n<p>Pre-deploy checklist for Vercel:</p>\n<ul>\n<li><code>X402_RECIPIENT_ADDRESS</code> — your receiving address</li>\n<li><code>CDP_API_KEY_ID</code> + <code>CDP_API_KEY_SECRET</code> — Base mainnet auth</li>\n<li><code>ALCHEMY_API_KEY</code> — Base RPC access</li>\n</ul>\n<hr>\n<h2>8. Always pass <code>req.url</code> as the <code>resource</code></h2>\n<pre><code class=\"language-typescript\">// ❌ static path — x402 client retries without query params\nconfig: { resource: &#39;/api/v1/x402/report&#39; }\n\n// ✅ pass the full request URL\nconfig: { resource: req.url as `${string}://${string}` }\n</code></pre>\n<p>On a 402 response the x402 client retries the request at the URL in the <code>resource</code> field. If <code>resource</code> is a static path, the retry arrives without <code>?wallet=0x...</code> and the handler blows up on missing parameters.</p>\n<hr>\n<h2>Summary</h2>\n<table>\n<thead>\n<tr>\n<th>#</th>\n<th>Symptom</th>\n<th>Root cause</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>1</td>\n<td><code>invalid_payload</code></td>\n<td>x402.org is testnet-only</td>\n</tr>\n<tr>\n<td>2</td>\n<td>Zero transfer logs</td>\n<td>EIP-3009 <code>from</code> is the user wallet, not the facilitator</td>\n</tr>\n<tr>\n<td>3</td>\n<td>Empty SERVICE_MAP</td>\n<td>CAIP-2 <code>&quot;eip155:8453&quot;</code> vs plain <code>&quot;base&quot;</code></td>\n</tr>\n<tr>\n<td>4</td>\n<td>JWT auth error</td>\n<td><code>supported</code> endpoint requires GET, not POST</td>\n</tr>\n<tr>\n<td>5</td>\n<td>Undebuggable errors</td>\n<td><code>invalidMessage</code> stripped by VerifyError</td>\n</tr>\n<tr>\n<td>6</td>\n<td>Logging wrapper never called</td>\n<td><code>useFacilitator</code> bypasses custom fetch</td>\n</tr>\n<tr>\n<td>7</td>\n<td>Zero revenue</td>\n<td><code>X402_RECIPIENT_ADDRESS</code> not set</td>\n</tr>\n<tr>\n<td>8</td>\n<td>Parameters vanish on retry</td>\n<td>Static <code>resource</code> URL strips query string</td>\n</tr>\n</tbody></table>\n<p>Server-side x402 has real sharp edges, but once it clicks the client side is genuinely pleasant — a few lines with <code>@x402/fetch</code> and payments just work.</p>\n"}},"exampleRequest":{},"tags":["x402"],"displayCostAmount":"0.01","displayCostAsset":"USDC","priceDynamic":false,"priceHint":null,"priceStatus":"priced","priceSource":"settled","requiresHandshake":false,"reviewCount":0,"rating":{"score":"0.00","successRate":"0.00","reviews":0,"stars":null,"state":"unrated"},"availabilityStatus":"unknown","priceObserved":null,"sessionDeposit":null,"pricing":{"kind":"static","summary":"$0.01/call","primary":{"kind":"static","protocol":"x402","network":"base","amountUsd":"0.01","per":"call","confidence":"exact"},"accepted":[{"kind":"static","protocol":"x402","network":"base","amountUsd":"0.01","per":"call","confidence":"exact"}]},"paymentMethods":[{"uid":"pm_8Qf2W-9zd-BwFpWcKVsge","protocol":"x402","methodType":"crypto","chain":"base","mode":"charge","costAmount":"0.01","costPer":"request","priority":0,"asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","unit":"request","depositMicros":null,"planRef":null}],"brandName":null,"brandSlug":null,"brandBaseUrl":null,"brandDocsUrl":null,"whatItDoes":"Delivers a paid article about common pitfalls and lessons learned when implementing the x402 HTTP payment protocol","exampleAgentPrompt":"Can you fetch the paylog article about the 8 things that tripped someone up when implementing x402 — the one with the slug 'x402-implementation'?","exampleUseCases":null,"resultDescription":"The full content of a developer-focused article describing 8 lessons and pitfalls encountered during real-world x402 HTTP payment protocol implementation, delivered as JSON after a micropayment of $0.01 USDC","failureModes":["Missing X-PAYMENT header returns 402 with payment instructions and accepted asset details","Invalid or expired payment token returns 402 error","Wrong slug returns not found or empty content","Network issues on Base blockchain cause payment verification failure","Payment amount below maxAmountRequired (10000 units) causes rejection"],"whenToPreferThis":"Use this endpoint when an AI agent or developer needs to read the paylog.dev article specifically about real-world x402 implementation pitfalls. Ideal when exploring x402 adoption, debugging x402 integrations, or researching HTTP-native payment protocol patterns on Base/USDC.","instructions":null,"reviewSummary":null,"reviewSummaryHighlights":null,"reviewSummaryConcerns":null,"reviewSummaryGeneratedAt":null,"activationCount":0,"lastUsedAt":null,"lastSuccessfullyRanAt":null,"lastHealthCheckAt":"2026-09-15T00:31:17.641Z","isFirstParty":false}