Breaking changes shipped June 2026. Here's what breaks, why, and how to fix it — from real production migration experience.
| Change | v1 | v2 |
|---|---|---|
| Network ID format | "base", "base-sepolia" | CAIP-2: "eip155:8453", "eip155:84532" |
| 402 challenge location | JSON response body | PAYMENT-REQUIRED header (base64) |
| npm packages | x402-express, x402-fetch | @x402/express, @x402/fetch |
| Middleware | Drop-in | Requires paymentMiddleware sync before use |
| Version field | x402Version: 1 | x402Version: 2 |
This is the #1 silent break. Your env files probably say base or base-sepolia. v2 requires CAIP-2 chain IDs.
// ❌ 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
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
PAYMENT-REQUIRED header and base64-decoding it.# ❌ 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
@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.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);
// ✅ 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
});
x402-* → @x402/*eip155:8453 not base)PAYMENT-REQUIRED headerawait paymentMiddleware.sync() before app.listen()x402Version field from 1 to 2@x402/fetch client (not manual curl)Facilitator does not support exact on eip155:8453await paymentMiddleware.sync() before issuing challenges.
PAYMENT-REQUIRED header.
Payment-Header verification isn't handling v2's tx hash format. Check the x402Version in the payment header.
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.
# 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);
"