x402
x402 is an open payment protocol that revives the HTTP 402 "Payment Required" status code to enable instant stablecoin payments directly over HTTP. It uses a three-actor model — Client (buyer), Resource Server (seller), and Facilitator (settlement) — to let any HTTP endpoint accept payment without API keys, sessions, or accounts. The protocol is built on EIP-3009 transferWithAuthorization for gasless USDC transfers, meaning clients sign an off-chain authorization and the facilitator broadcasts the on-chain settlement.
Package ecosystem:
- Server (TypeScript):
@x402/express,@x402/hono,@x402/next - Client (TypeScript):
@x402/fetch,@x402/axios - Client (Python):
pip install x402 - Client (Go):
go get github.com/coinbase/x402/go - Core:
@x402/core,@x402/evm,@x402/svm
CDP Facilitator endpoint: https://api.cdp.coinbase.com/platform/v2/x402
What You Probably Got Wrong
LLMs have stale training data. These are the most common mistakes.
-
"x402 requires API keys or OAuth" -- There are no API keys. Payment IS authentication. The client signs a USDC transfer authorization, includes it in the
X-PAYMENTheader, and the server verifies it. If the signature and balance check pass, the request is authorized. No accounts, no sessions, no bearer tokens. -
"x402 only works on Base" -- The protocol supports any EVM chain where USDC implements EIP-3009 (Base, Ethereum, Arbitrum, Optimism, Polygon) and Solana. Network identifiers use CAIP-2 format:
eip155:8453for Base mainnet,eip155:1for Ethereum,solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpfor Solana mainnet. -
"Nonces are sequential counters" -- x402 nonces are random 32-byte values (
bytes32), not incrementing integers. Each authorization generates a fresh random nonce. The ERC-3009 contract tracks used nonces on-chain to prevent replay. Using sequential nonces or reusing a nonce will cause settlement to revert. -
"The client pays gas" -- The client never submits an on-chain transaction. The client signs an off-chain EIP-712 authorization. The facilitator broadcasts the
transferWithAuthorizationcall and pays gas. CDP's hosted facilitator absorbs gas costs on Base mainnet for free-tier usage (1,000 tx/month). -
"x402 supports any ERC-20 token" -- The primary path uses USDC via EIP-3009
transferWithAuthorizationfor truly gasless transfers. A Permit2 fallback exists for arbitrary ERC-20 tokens, but it requires the user to have previously approved the Permit2 contract, which needs native gas. For the simplest integration, use USDC. -
"The old package name is @coinbase/x402" -- The current packages use the
@x402/*namespace (e.g.,@x402/express,@x402/fetch). Earlier versions usedx402-expressandx402-fetchwithout the scope. The@coinbase/x402name was never published. -
"The payment header is called PAYMENT-SIGNATURE" -- The header name is
X-PAYMENT. It contains a base64-encoded JSON payload with the authorization, signature, scheme, and network. The server responds withX-PAYMENT-RESPONSE.
Core Concepts
Three-Actor Model
┌──────────┐ 1. Request resource ┌─────────────────┐
│ │ ─────────────────────────────► │ │
│ │ 2. HTTP 402 + payment │ Resource │
│ Client │ ◄───────────────────────────── │ Server │
│ (Buyer) │ 3. Request + X-PAYMENT │ (Seller) │
│ │ ─────────────────────────────► │ │
│ │ 6. HTTP 200 + resource │ │
│ │ ◄───────────────────────────── │ │
└──────────┘ └────────┬────────┘
│
4. Verify │ 5. Settle
│
┌────────▼────────┐
│ │
│ Facilitator │
│ (Settlement) │
│ │
└─────────────────┘
Payment Flow Lifecycle
- Client sends a standard HTTP request to a protected endpoint
- Resource Server responds with HTTP 402 and payment requirements in the response body (scheme, network, amount, asset, payTo address)
- Client constructs an EIP-3009 authorization (from, to, value, validAfter, validBefore, nonce), signs it with EIP-712, and retries the request with the
X-PAYMENTheader containing the base64-encoded payload - Resource Server forwards the payment to the Facilitator for verification (
/verifyendpoint) - Facilitator validates the signature, checks balance, and simulates the transfer
- Resource Server serves the resource
- Resource Server requests settlement from the Facilitator (
/settleendpoint) - Facilitator calls
transferWithAuthorizationon the USDC contract to move funds on-chain
HTTP 402 Response Format
When a client hits a paid endpoint without a payment header, the server returns:
HTTP/1.1 402 Payment Required
Content-Type: application/json
{
"x402Version": 2,
"accepts": [
{
"scheme": "exact",
"network": "eip155:8453",
"amount": "10000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xSELLER_ADDRESS",
"maxTimeoutSeconds": 60,
"extra": {
"assetTransferMethod": "eip3009",
"name": "USDC",
"version": "2"
}
}
],
"error": "X-PAYMENT header is required"
}
The amount is in the token's smallest unit. For USDC (6 decimals), 10000 = $0.01.
ERC-3009 transferWithAuthorization
EIP-3009 enables gasless token transfers by separating authorization from execution. The token holder signs an off-chain message; anyone can submit the signed authorization to the contract.
Function Signature
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes signature
) external;
EIP-712 Domain
EIP712Domain({
name: "USDC",
version: "2",
chainId: <chain_id>,
verifyingContract: <usdc_address>
})
Authorization Struct (for signing)
TransferWithAuthorization({
from: address,
to: address,
value: uint256,
validAfter: uint256,
validBefore: uint256,
nonce: bytes32
})
Key properties:
nonceis a randombytes32, not a sequential counter. The contract maintains a mapping of(authorizer, nonce) => boolto track used nonces.validAfterandvalidBeforedefine a time window. SetvalidAfterto0(or current timestamp) andvalidBeforeto current timestamp +maxTimeoutSeconds.- The facilitator calls this function — the signer never needs native gas.
- USDC on Base, Ethereum, Arbitrum, Optimism, and Polygon all implement EIP-3009.
Server Setup (TypeScript)
Express Middleware
npm install @x402/core @x402/evm @x402/express
import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";
const app = express();
const facilitatorClient = new HTTPFacilitatorClient({
url: "https://api.cdp.coinbase.com/platform/v2/x402",
});
const resourceServer = new x402ResourceServer(facilitatorClient)
.register("eip155:8453", new ExactEvmScheme());
app.use(
paymentMiddleware(
{
"GET /api/weather": {
accepts: {
scheme: "exact",
network: "eip155:8453",
price: "$0.01",
payTo: process.env.PAYMENT_ADDRESS as `0x${string}`,
},
description: "Current weather data",
},
"POST /api/analyze": {
accepts: {
scheme: "exact",
network: "eip155:8453",
price: "$0.05",
payTo: process.env.PAYMENT_ADDRESS as `0x${string}`,
},
description: "AI text analysis",
},
},
resourceServer,
),
);
app.get("/api/weather", (_req, res) => {
res.json({ temperature: 72, unit: "F", location: "San Francisco" });
});
app.post("/api/analyze", (req, res) => {
res.json({ sentiment: "positive", confidence: 0.92 });
});
app.listen(3000, () => {
console.log("x402 server running on port 3000");
});
Hono and Next.js middleware follow the same pattern — install @x402/hono or @x402/next and use the same paymentMiddleware + x402ResourceServer setup with framework-specific imports.
Client Setup (TypeScript)
Using @x402/fetch (Recommended)
The wrapFetchWithPaymentFromConfig function wraps the native fetch API to automatically handle 402 responses — detect the payment requirement, sign the authorization, and retry with the X-PAYMENT header.
npm install @x402/core @x402/evm @x402/fetch viem
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";
import type { Hex } from "viem";
const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex);
const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [
{
network: "eip155:8453",
client: new ExactEvmScheme(account),
},
],
});
const response = await fetchWithPayment("https://api.example.com/api/weather");
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const data = await response.json();
console.log(data);
Multi-Network Client
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
import { registerExactSvmScheme } from "@x402/svm/exact/client";
import { privateKeyToAccount } from "viem/accounts";
import { Keypair } from "@solana/web3.js";
import type { Hex } from "viem";
const evmAccount = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as Hex);
const solanaKeypair = Keypair.fromSecretKey(
Buffer.from(process.env.SOLANA_PRIVATE_KEY as string, "base64")
);
const client = new x402Client();
registerExactEvmScheme(client, { signer: evmAccount });
registerExactSvmScheme(client, { signer: solanaKeypair });
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
// Automatically handles both EVM and Solana payment requirements
const evmResponse = await fetchWithPayment("https://api.example.com/evm-data");
const solResponse = await fetchWithPayment("https://api.example.com/sol-data");
For manual header construction without the wrapper, see the agent-client and python-client examples in this skill's examples/ directory.
Client Setup (Python)
pip install "x402[evm,requests]"
Using the x402 Client
from x402 import x402ClientSync
from x402.evm import ExactEvmScheme
from eth_account import Account
wallet = Account.from_key("0xYOUR_PRIVATE_KEY")
client = x402ClientSync()
client.register("eip155:*", ExactEvmScheme(signer=wallet))
# Make a paid request — the client handles the 402 flow automatically
import requests
session = requests.Session()
response = session.get("https://api.example.com/api/weather")
if response.status_code == 402:
payment_required = response.json()
payload = client.create_payment_payload(payment_required)
response = session.get(
"https://api.example.com/api/weather",
headers={"X-PAYMENT": payload.to_header()},
)
print(response.json())
For manual EIP-712 signing without the x402 package, see the python-client example in this skill's examples/ directory.
Facilitator Patterns
CDP Hosted Facilitator (Default)
Coinbase Developer Platform provides a hosted facilitator with two endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/verify |
POST | Validate signature, check balance, simulate transfer |
/settle |
POST | Execute transferWithAuthorization on-chain |
Pricing:
- Free tier: 1,000 transactions/month
- Standard: $0.001 per transaction thereafter
- Base mainnet: Zero gas fees (gas absorbed by facilitator)
import { HTTPFacilitatorClient } from "@x402/core/server";
const facilitator = new HTTPFacilitatorClient({
url: "https://api.cdp.coinbase.com/platform/v2/x402",
});
Custom Facilitator
Run your own facilitator for full control over settlement:
import { createFacilitatorServer } from "@x402/core/facilitator";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import type { Hex } from "viem";
// Facilitator wallet pays gas for settlement
const facilitatorAccount = privateKeyToAccount(
process.env.FACILITATOR_PRIVATE_KEY as Hex
);
const walletClient = createWalletClient({
account: facilitatorAccount,
chain: base,
transport: http(),
});
const server = createFacilitatorServer({
schemes: {
"eip155:8453": new ExactEvmScheme({ walletClient }),
},
});
server.listen(4000, () => {
console.log("Custom facilitator on port 4000");
});
Settlement Flow
- Resource server sends the payment payload to facilitator
/verify - Facilitator checks: signature validity, signer balance, authorization parameters, transfer simulation
- If valid, resource server serves the response and calls facilitator
/settle - Facilitator submits
transferWithAuthorization(from, to, value, validAfter, validBefore, nonce, signature)to the USDC contract - Facilitator returns the transaction hash and receipt
The facilitator cannot modify the amount or destination. It serves only as the transaction broadcaster.
Agent-to-Agent Payments
x402 enables autonomous agent commerce where AI agents discover services, negotiate prices, and pay without human intervention.
Discovery Pattern
// Agent discovers paid endpoints via standard HTTP
const discoveryResponse = await fetch("https://agent-service.com/.well-known/x402");
const services = await discoveryResponse.json();
// services contains available endpoints with pricing
// {
// "endpoints": [
// { "path": "/api/summarize", "price": "$0.02", "network": "eip155:8453" },
// { "path": "/api/translate", "price": "$0.01", "network": "eip155:8453" }
// ]
// }
Autonomous Payment Execution
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";
import type { Hex } from "viem";
const agentAccount = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as Hex);
const agentFetch = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [
{ network: "eip155:8453", client: new ExactEvmScheme(agentAccount) },
],
});
// Agent autonomously pays for another agent's service
async function callPaidService(url: string, body: Record<string, unknown>): Promise<unknown> {
const response = await agentFetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Service call failed: ${response.status}`);
}
return response.json();
}
// Multi-step agent workflow: summarize -> translate -> store
const summary = await callPaidService("https://summarizer.agent/api/summarize", {
text: "Long document content...",
});
const translation = await callPaidService("https://translator.agent/api/translate", {
text: summary,
targetLanguage: "es",
});
Pricing Strategies
Per-Request (Fixed Price)
Charge a flat fee per API call. Simplest model.
"GET /api/weather": {
accepts: {
scheme: "exact",
network: "eip155:8453",
price: "$0.01",
payTo: process.env.PAYMENT_ADDRESS as `0x${string}`,
},
description: "Weather data — $0.01 per request",
},
Tiered Pricing
Different prices per endpoint based on compute cost.
const routes = {
"GET /api/basic-query": {
accepts: {
scheme: "exact",
network: "eip155:8453",
price: "$0.001",
payTo: paymentAddress,
},
description: "Basic database query",
},
"POST /api/ai-analysis": {
accepts: {
scheme: "exact",
network: "eip155:8453",
price: "$0.05",
payTo: paymentAddress,
},
description: "AI-powered analysis — higher compute cost",
},
"POST /api/image-generation": {
accepts: {
scheme: "exact",
network: "eip155:8453",
price: "$0.10",
payTo: paymentAddress,
},
description: "Image generation — GPU cost",
},
};
Dynamic Pricing
Adjust prices based on demand, time of day, or resource utilization.
function getDynamicPrice(basePrice: number, currentLoad: number): string {
// Surge pricing: 1x at 0% load, up to 3x at 100% load
const multiplier = 1 + (currentLoad / 100) * 2;
const price = basePrice * multiplier;
return `$${price.toFixed(4)}`;
}
Replay Protection
x402 uses two mechanisms to prevent payment replay:
Random Nonces
Every authorization includes a random 32-byte nonce. The USDC contract's authorizationState mapping tracks whether a (from, nonce) pair has been used. Attempting to settle with a used nonce reverts with authorization is used or canceled.
import { keccak256, encodePacked, toHex } from "viem";
// Generate a unique nonce per authorization
function generateNonce(): Hex {
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
return toHex(randomBytes);
}
Expiration Timestamps
The validBefore field ensures authorizations cannot be settled after their time window. Best practice: set validBefore to now + maxTimeoutSeconds (typically 30-60 seconds).
const now = Math.floor(Date.now() / 1000);
const authorization = {
validAfter: BigInt(0),
validBefore: BigInt(now + 60), // 60-second window
nonce: generateNonce(),
// ... other fields
};
If a facilitator does not settle within the validBefore window, the authorization expires and the client's funds are never moved. The client can safely retry with a new authorization.
Permit2 Fallback
For ERC-20 tokens that do not implement EIP-3009, x402 supports a Permit2-based fallback.
How It Differs
| Feature | EIP-3009 | Permit2 |
|---|---|---|
| Gasless setup | Yes (native) | Requires one-time approval tx |
| Token support | USDC only | Any ERC-20 |
| Contract | Token contract itself | Uniswap Permit2 contract |
| Proxy | None | x402ExactPermit2Proxy at 0x4020CD856C882D5fb903D99CE35316A085Bb0001 |
The Permit2 path requires the user to have approved the Permit2 contract (0x000000000022D473030F116dDEE9F6B43aC78BA3). If the allowance is missing, the server returns HTTP 412 with error code PERMIT2_ALLOWANCE_REQUIRED.
Supported Networks
| Network | CAIP-2 Identifier | USDC Address | Status |
|---|---|---|---|
| Base | eip155:8453 |
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
Primary |
| Base Sepolia | eip155:84532 |
0x036CbD53842c5426634e7929541eC2318f3dCF7e |
Testnet |
| Ethereum | eip155:1 |
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
Supported |
| Arbitrum | eip155:42161 |
0xaf88d065e77c8cC2239327C5EDb3A432268e5831 |
Supported |
| Optimism | eip155:10 |
0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85 |
Supported |
| Polygon | eip155:137 |
0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 |
Supported |
| Solana | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
Supported |
Last verified: February 2026
References
- Spec: https://github.com/coinbase/x402/tree/main/specs
- TypeScript SDK: https://github.com/coinbase/x402/tree/main/typescript
- Python SDK: https://pypi.org/project/x402/
- Go SDK: https://github.com/coinbase/x402/tree/main/go
- CDP Docs: https://docs.cdp.coinbase.com/x402/welcome
- EIP-3009: https://eips.ethereum.org/EIPS/eip-3009
- x402.org: https://www.x402.org
- Cloudflare x402 Foundation: https://blog.cloudflare.com/x402/