x402 v1 → v2 Migration Guide

Breaking changes shipped June 2026. Here's what breaks, why, and how to fix it — from real production migration experience.

What Changed (TL;DR)

Changev1v2
Network ID format"base", "base-sepolia"CAIP-2: "eip155:8453", "eip155:84532"
402 challenge locationJSON response bodyPAYMENT-REQUIRED header (base64)
npm packagesx402-express, x402-fetch@x402/express, @x402/fetch
MiddlewareDrop-inRequires paymentMiddleware sync before use
Version fieldx402Version: 1x402Version: 2

1. Network IDs: CAIP-2 Format

This is the #1 silent break. Your env files probably say base or base-sepolia. v2 requires CAIP-2 chain IDs.

⚠️ Silent failure: Old network strings don't throw errors — they just produce 402 challenges that v2 clients can't parse. Your endpoint looks live but no one can pay.
// ❌ v1 (broken in v2)
{ network: "base", maxAmountRequired: 5000 }

// ✅ v2 (CAIP-2 format)
{ network: "eip155:8453", maxAmountRequired: 5000 }

// Common CAIP-2 network IDs:
// eip155:1       = Ethereum mainnet
// eip155:8453    = Base
// eip155:84532   = Base Sepolia
// eip155:137     = Polygon
// eip155:42161   = Arbitrum
// solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp  = Solana mainnet

2. Payment Challenge: Body → Header

v2 402 responses have empty bodies. The payment challenge is base64-encoded JSON in the PAYMENT-REQUIRED header.

// ❌ v1: Challenge in response body
HTTP/1.1 402 Payment Required
Content-Type: application/json
WWW-Authenticate: Payment realm="x402" version="1", ...

{"accepts":[{...}], "x402Version":1}  ← JSON body

// ✅ v2: Challenge in header
HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: eyJhY2NlcHRzIjpbeyJu...  ← base64 JSON header
Content-Length: 0                          ← empty body
💡 Pro tip: If you're parsing 402 response bodies, your code breaks silently. Switch to reading the PAYMENT-REQUIRED header and base64-decoding it.

3. npm Packages: New Scope

# ❌ Old packages (stop at 1.2.0, no v2 support)
npm uninstall x402-express x402-fetch

# ✅ New packages (v2, @x402/ scope)
npm install @x402/express @x402/fetch @x402/evm @x402/core
Note: @x402/express has an optional peer dependency on @x402/paywall for browser UIs. Skip it for server-only deployments — it pulls in wagmi, WalletConnect, and Solana deps you don't need.

4. Middleware Initialization

v2 middleware requires a sync step before it can issue challenges. This means offline tests break — you need network connectivity even for local dev.

// ✅ v2 middleware setup
import { createX402Middleware } from '@x402/express';

const x402 = createX402Middleware({
  network: 'eip155:8453',
  maxAmountRequired: 5000,
  payTo: '0x...',
});

// REQUIRED: Sync supported payment kinds before use
await x402.paymentMiddleware.sync();
// Without this: "Facilitator does not support exact on eip155:8453"

app.use(x402);

5. Client-Side Changes

// ✅ v2 client: @x402/fetch
import { x402fetch } from '@x402/fetch';

const res = await x402fetch('https://api.example.com/x402/data', {
  method: 'POST',
  body: JSON.stringify({ query: '...' }),
  // v2 SDK handles: 402 → parse PAYMENT-REQUIRED header → sign tx → retry
});

Migration Checklist

  1. Update npm packages: x402-*@x402/*
  2. Change network IDs to CAIP-2 format (eip155:8453 not base)
  3. Move payment challenges from response body to PAYMENT-REQUIRED header
  4. Add await paymentMiddleware.sync() before app.listen()
  5. Update x402Version field from 1 to 2
  6. Test with @x402/fetch client (not manual curl)
  7. Validate with minia2a's free spec checker

Common Errors & Fixes

Error: Facilitator does not support exact on eip155:8453
Fix: You forgot await paymentMiddleware.sync() before issuing challenges.
Error: Silent 402 but client can't parse
Fix: You're sending the challenge in the response body, not the header. v2 clients only read PAYMENT-REQUIRED header.
Error: Payment succeeds but client retries infinitely
Fix: Your Payment-Header verification isn't handling v2's tx hash format. Check the x402Version in the payment header.

Live v2 Example: minia2a

minia2a runs both v1 and v2 simultaneously — one of the first dual-version endpoints in production.

# v1 endpoint (network: "base", JSON body 402)
curl -s -D - https://minia2a.uk/x402/find?q=test

# v2 endpoint (CAIP-2 eip155:8453, PAYMENT-REQUIRED header, empty body)
curl -s -D - https://minia2a.uk/x402/v2/recall \
  -H "Content-Type: application/json" \
  -d '{"query":"test"}'

Both return HTTP 402 with the correct format for their version. The v2 endpoint uses @x402/express v2.18.0 with the Dexter facilitator.

Testing Your Migration

# Test with minia2a's free validator
curl "https://minia2a.uk/x402/validate?url=https://your-api.com/x402/endpoint"

# Test with @x402/fetch
node -e "
const { x402fetch } = require('@x402/fetch');
x402fetch('https://your-api.com/x402/endpoint')
  .then(r => r.json())
  .then(console.log);
"

Resources