{"slug": "migrating-to-x402-v2-what-actually-changed-and-the-traps-nobody-documents", "title": "Migrating to x402 v2: what actually changed (and the traps nobody documents)", "summary": "The FiatDock team migrated its entire stack—Express server, fetch client, and MCP server—from x402 v1 to protocol v2 in a single evening, eliminating all 24 transitive npm vulnerabilities in the process. The migration required switching from the deprecated v1 packages to the new `@x402` scoped packages, with key changes including empty-body 402 responses that now carry challenge data in the `PAYMENT-REQUIRED` header instead of JSON, and explicit wiring that adds per-route discovery metadata. The team documented undocumented traps, such as the optional `@x402/paywall` peer dependency that reintroduces the dependency bloat v2 was meant to escape, and the fact that the `facilitator.x402.org` host does not resolve—requiring use of `https://x402.org/facilitator` instead.", "body_md": "We run [FiatDock](https://fiatdock.com) — a non-custodial USDC ↔ bank on/off-ramp where AI agents pay $0.05 per call over x402. This week we migrated the whole stack (Express server, fetch client, MCP server) from x402 v1 to protocol v2. It took an evening, killed all 24 of our transitive npm vulnerabilities, and almost none of it was documented anywhere. Here is the map we wish we'd had.\n\nThe packages you're using (`x402-express`\n\n, `x402-fetch`\n\n, `x402`\n\n) are the **v1 line and they stop at 1.2.0**. There is no v2 of them. Protocol v2 lives under the ** @x402 scope**:\n\n```\nnpm rm x402-express x402-fetch\nnpm i @x402/express @x402/fetch @x402/evm @x402/core\n```\n\n`@coinbase/x402`\n\nis a separate CDP-flavoured package — not the core.\n\n**Trap:** `@x402/express`\n\nhas an optional peer dependency on `@x402/paywall`\n\n. Do **not** install it unless you want the browser paywall UI — it pulls wagmi/walletconnect/solana, which is the exact dependency jungle (and the `uuid`\n\n/`ws`\n\nvulnerabilities) you're escaping by leaving v1.\n\nv1's one-liner becomes explicit wiring — and you gain discovery metadata per route:\n\n``` js\nimport { paymentMiddleware, x402ResourceServer } from \"@x402/express\";\nimport { ExactEvmScheme } from \"@x402/evm/exact/server\";\nimport { HTTPFacilitatorClient } from \"@x402/core/server\";\n\nconst facilitator = new HTTPFacilitatorClient({ url: \"https://x402.org/facilitator\" });\nconst server = new x402ResourceServer(facilitator)\n  .register(\"eip155:84532\", new ExactEvmScheme());\n\napp.use(paymentMiddleware({\n  \"POST /v1/offramp/session\": {\n    accepts: { scheme: \"exact\", price: \"$0.05\", network: \"eip155:84532\", payTo: PAY_TO },\n    description: \"Sell agent USDC to fiat in the owner's bank account\",\n    mimeType: \"application/json\",\n    serviceName: \"FiatDock\",\n    tags: [\"offramp\", \"usdc\"],\n  },\n}, server));\n```\n\nThree things to notice:\n\n`base-sepolia`\n\n→ `eip155:84532`\n\n, `base`\n\n→ `eip155:8453`\n\n. Keep a friendly-name map if your env files say `base-sepolia`\n\n.`https://x402.org/facilitator`\n\nserves v2 (`/supported`\n\n, `/verify`\n\n, `/settle`\n\n) behind a 308 redirect. The `facilitator.x402.org`\n\nhost you'll see in some READMEs does not resolve.`paymentMiddleware`\n\narg). Without it, even `Facilitator does not support exact on eip155:84532`\n\n. Your offline tests now need network access — plan for it.v1 put payment requirements in the JSON body. **v2 402 responses have an empty body**; the challenge is base64 JSON in the `PAYMENT-REQUIRED`\n\nheader:\n\n``` js\nconst challenge = JSON.parse(atob(res.headers.get(\"payment-required\")));\n// { x402Version: 2, resource: {...}, accepts: [{ scheme, network, amount, asset, payTo, ... }] }\n```\n\nIf you surface 402s to agents (we decode them into MCP tool errors), update that path — your users will otherwise see `{}`\n\nand file confused issues.\n\n``` js\nimport { wrapFetchWithPaymentFromConfig } from \"@x402/fetch\";\nimport { ExactEvmScheme } from \"@x402/evm\";\nimport { privateKeyToAccount } from \"viem/accounts\";\n\nconst payFetch = wrapFetchWithPaymentFromConfig(fetch, {\n  schemes: [{ network: \"eip155:*\", client: new ExactEvmScheme(privateKeyToAccount(KEY)) }],\n});\n```\n\nThe `eip155:*`\n\nwildcard means the client follows whatever EVM network the server's challenge names — our testnet→mainnet switch later requires zero client changes.\n\nv2 has a first-class discovery story. Register the extension and your paid routes get catalogued by the facilitator (which is what indexers like x402scan and the x402 Bazaar read):\n\n``` js\nimport { bazaarResourceServerExtension, declareDiscoveryExtension } from \"@x402/extensions\";\n\nserver.registerExtension(bazaarResourceServerExtension);\n// per route: extensions: declareDiscoveryExtension({ method: \"POST\", input: {...}, inputSchema: {...}, bodyType: \"json\", output: { example: {...} } })\n```\n\nThis is also the v2 answer to the old `outputSchema.output`\n\nfield scanners used to ask for.\n\nWe verified the full handshake against the real facilitator with an unfunded throwaway key: challenge → parse → sign → retry with `X-PAYMENT`\n\n→ facilitator verify. The only rejection was `invalid_exact_evm_insufficient_balance`\n\n— i.e. the wire format was accepted end to end.\n\nAnd `npm audit`\n\n: **24 moderate → 0**, in both the server and our published MCP package ([ fiatdock-mcp](https://www.npmjs.com/package/fiatdock-mcp)).\n\n*FiatDock is machine-first: llms.txt · OpenAPI · MCP *\n\n`npx fiatdock-mcp`\n\n· source mirror. If your agent earns USDC and you want it in a bank account, that's literally our whole product.", "url": "https://wpnews.pro/news/migrating-to-x402-v2-what-actually-changed-and-the-traps-nobody-documents", "canonical_source": "https://dev.to/fiatdock/migrating-to-x402-v2-what-actually-changed-and-the-traps-nobody-documents-46k3", "published_at": "2026-06-12 10:24:09+00:00", "updated_at": "2026-06-12 10:41:23.865116+00:00", "lang": "en", "topics": ["ai-agents", "ai-infrastructure"], "entities": ["FiatDock", "x402", "Express", "MCP", "npm", "Coinbase", "wagmi", "walletconnect"], "alternates": {"html": "https://wpnews.pro/news/migrating-to-x402-v2-what-actually-changed-and-the-traps-nobody-documents", "markdown": "https://wpnews.pro/news/migrating-to-x402-v2-what-actually-changed-and-the-traps-nobody-documents.md", "text": "https://wpnews.pro/news/migrating-to-x402-v2-what-actually-changed-and-the-traps-nobody-documents.txt", "jsonld": "https://wpnews.pro/news/migrating-to-x402-v2-what-actually-changed-and-the-traps-nobody-documents.jsonld"}}