Integrate x402 Payments
Build a complete payment flow that lets users pay for API calls with USDC on Base. No credit cards, no API key signup—just crypto-native payments.
What You'll Learn
- Handle 402 Payment Required responses
- Sign payment authorizations with viem
- Build a payment confirmation UI
- Implement budget controls
- Test with real transactions
Time: ~45 minutes
Difficulty: Intermediate
Prerequisites
- Wallet with USDC on Base network
- Node.js 18+ or modern browser
- Basic understanding of Ethereum signatures
How x402 Works
1. Request API endpoint
2. If free tier exhausted → 402 response with payment details
3. Sign a USDC authorization message
4. Retry request with payment header
5. Server verifies & settles payment → returns data
The key insight: you're not sending a transaction. You're signing an authorization that the server uses to pull funds via a facilitator.
Step 1: Basic Request Without Payment
First, let's see what happens when you hit an endpoint:
JavaScript
const BASE_URL = 'https://api.web3identity.com';
async function getENSProfile(name) {
const response = await fetch(`${BASE_URL}/api/ens/resolve/${name}`);
console.log('Status:', response.status);
if (response.status === 402) {
// Payment required!
const paymentInfo = await response.json();
console.log('Payment required:', paymentInfo);
return null;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// First 100 calls/day are free
const profile = await getENSProfile('vitalik.eth');
console.log(profile);
cURL
# Check response headers
curl -I https://api.web3identity.com/api/ens/resolve/vitalik.eth
# If 402, see payment details
curl -s https://api.web3identity.com/api/ens/resolve/vitalik.eth | jq
402 Response:
{
"error": true,
"code": "PAYMENT_REQUIRED",
"price": "$0.01",
"priceUSD": 0.01,
"priceWei": "10000",
"network": {
"name": "Base",
"chainId": 8453
},
"asset": {
"symbol": "USDC",
"address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6
},
"recipient": "0xF499102c8707c6501CaAdD2028c6DF1c6C6E813b",
"x402": {
"version": "1",
"facilitator": "https://api.cdp.coinbase.com/platform/v2/x402"
}
}
Step 2: Create Payment Authorization
Install dependencies:
npm install viem
Create the Payment Signer
// x402-payment.mjs
import { createWalletClient, http, parseUnits, encodeFunctionData } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
const BASE_URL = 'https://api.web3identity.com';
// USDC on Base uses EIP-3009 TransferWithAuthorization
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
// EIP-712 domain for USDC on Base
const USDC_DOMAIN = {
name: 'USD Coin',
version: '2',
chainId: 8453,
verifyingContract: USDC_ADDRESS,
};
// EIP-3009 types
const TRANSFER_WITH_AUTH_TYPES = {
TransferWithAuthorization: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
],
};
async function createPaymentAuthorization(wallet, paymentInfo) {
// Generate random nonce
const nonce = '0x' + [...crypto.getRandomValues(new Uint8Array(32))]
.map(b => b.toString(16).padStart(2, '0')).join('');
// Payment valid for 5 minutes
const validAfter = 0n;
const validBefore = BigInt(Math.floor(Date.now() / 1000) + 300);
// Amount in USDC (6 decimals)
const value = parseUnits(paymentInfo.priceUSD.toString(), 6);
const message = {
from: wallet.account.address,
to: paymentInfo.recipient,
value,
validAfter,
validBefore,
nonce,
};
// Sign the authorization
const signature = await wallet.signTypedData({
domain: USDC_DOMAIN,
types: TRANSFER_WITH_AUTH_TYPES,
primaryType: 'TransferWithAuthorization',
message,
});
// Encode as payment header
return btoa(JSON.stringify({
...message,
value: value.toString(),
validAfter: validAfter.toString(),
validBefore: validBefore.toString(),
signature,
}));
}
export async function fetchWithPayment(endpoint, wallet) {
const url = `${BASE_URL}${endpoint}`;
// Initial request
let response = await fetch(url);
// If free tier available, return data
if (response.status === 200) {
return response.json();
}
// Handle payment required
if (response.status === 402) {
const paymentInfo = await response.json();
console.log(`Payment required: $${paymentInfo.priceUSD} USDC`);
// Create and sign payment authorization
const paymentHeader = await createPaymentAuthorization(wallet, paymentInfo);
// Retry with payment
response = await fetch(url, {
headers: {
'x-payment': paymentHeader,
'payment-signature': paymentHeader, // Required by x402 middleware
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `Payment failed: ${response.status}`);
}
return response.json();
}
throw new Error(`Unexpected status: ${response.status}`);
}
// Usage
const account = privateKeyToAccount(process.env.WALLET_KEY);
const wallet = createWalletClient({
account,
chain: base,
transport: http('https://mainnet.base.org'),
});
const profile = await fetchWithPayment('/api/ens/resolve/vitalik.eth', wallet);
console.log('Profile:', profile);
Run it:
WALLET_KEY=0x... node x402-payment.mjs
Step 3: Python Implementation
#!/usr/bin/env python3
"""
x402 Payment Integration for Python
"""
import os
import json
import base64
import secrets
import time
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data
BASE_URL = "https://api.web3identity.com"
USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
def create_payment_authorization(account, payment_info: dict) -> str:
"""Create signed payment authorization."""
# Generate random nonce
nonce = "0x" + secrets.token_hex(32)
# Payment valid for 5 minutes
valid_before = int(time.time()) + 300
# Amount in USDC smallest unit (6 decimals)
value = int(payment_info["priceUSD"] * 1_000_000)
# EIP-712 typed data
domain = {
"name": "USD Coin",
"version": "2",
"chainId": 8453,
"verifyingContract": USDC_ADDRESS,
}
message = {
"from": account.address,
"to": payment_info["recipient"],
"value": value,
"validAfter": 0,
"validBefore": valid_before,
"nonce": nonce,
}
types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
"TransferWithAuthorization": [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "validAfter", "type": "uint256"},
{"name": "validBefore", "type": "uint256"},
{"name": "nonce", "type": "bytes32"},
],
}
# Sign the message
signable = encode_typed_data(
domain_data=domain,
message_types={"TransferWithAuthorization": types["TransferWithAuthorization"]},
message_data=message,
)
signed = account.sign_message(signable)
# Encode as payment header
payload = {
**message,
"value": str(value),
"validAfter": "0",
"validBefore": str(valid_before),
"signature": signed.signature.hex(),
}
return base64.b64encode(json.dumps(payload).encode()).decode()
def fetch_with_payment(endpoint: str, private_key: str) -> dict:
"""Fetch API endpoint with automatic x402 payment."""
account = Account.from_key(private_key)
url = f"{BASE_URL}{endpoint}"
# Initial request
response = requests.get(url)
if response.status_code == 200:
return response.json()
if response.status_code == 402:
payment_info = response.json()
print(f"Payment required: ${payment_info['priceUSD']} USDC")
# Create payment authorization
payment_header = create_payment_authorization(account, payment_info)
# Retry with payment
response = requests.get(url, headers={
"x-payment": payment_header,
"payment-signature": payment_header,
})
response.raise_for_status()
return response.json()
response.raise_for_status()
if __name__ == "__main__":
private_key = os.environ.get("WALLET_KEY")
if not private_key:
print("Set WALLET_KEY environment variable")
exit(1)
profile = fetch_with_payment("/api/ens/resolve/vitalik.eth", private_key)
print("Profile:", json.dumps(profile, indent=2))
Install dependencies:
pip install requests eth-account
Step 4: React Payment Hook
Build a reusable hook for React apps:
// hooks/useX402.ts
import { useState, useCallback } from 'react';
import { useWalletClient } from 'wagmi';
import { parseUnits } from 'viem';
const BASE_URL = 'https://api.web3identity.com';
interface PaymentState {
loading: boolean;
error: string | null;
lastPayment: { amount: string; endpoint: string } | null;
}
interface X402Options {
maxPaymentUSD?: number;
autoConfirm?: boolean;
onPaymentRequired?: (price: number) => Promise<boolean>;
}
export function useX402(options: X402Options = {}) {
const { data: walletClient } = useWalletClient();
const [state, setState] = useState<PaymentState>({
loading: false,
error: null,
lastPayment: null,
});
const { maxPaymentUSD = 0.10, autoConfirm = false, onPaymentRequired } = options;
const createPaymentHeader = useCallback(async (paymentInfo: any) => {
if (!walletClient) throw new Error('Wallet not connected');
const nonce = '0x' + [...crypto.getRandomValues(new Uint8Array(32))]
.map(b => b.toString(16).padStart(2, '0')).join('');
const validBefore = BigInt(Math.floor(Date.now() / 1000) + 300);
const value = parseUnits(paymentInfo.priceUSD.toString(), 6);
const message = {
from: walletClient.account.address,
to: paymentInfo.recipient,
value,
validAfter: 0n,
validBefore,
nonce,
};
const signature = await walletClient.signTypedData({
domain: {
name: 'USD Coin',
version: '2',
chainId: 8453,
verifyingContract: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
},
types: {
TransferWithAuthorization: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
],
},
primaryType: 'TransferWithAuthorization',
message,
});
return btoa(JSON.stringify({
...message,
value: value.toString(),
validAfter: '0',
validBefore: validBefore.toString(),
signature,
}));
}, [walletClient]);
const fetchWithPayment = useCallback(async <T = any>(endpoint: string): Promise<T> => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const url = `${BASE_URL}${endpoint}`;
let response = await fetch(url);
if (response.status === 200) {
setState(prev => ({ ...prev, loading: false }));
return response.json();
}
if (response.status === 402) {
const paymentInfo = await response.json();
// Check max payment
if (paymentInfo.priceUSD > maxPaymentUSD) {
throw new Error(`Payment $${paymentInfo.priceUSD} exceeds max $${maxPaymentUSD}`);
}
// Confirm payment
if (!autoConfirm) {
const confirmed = onPaymentRequired
? await onPaymentRequired(paymentInfo.priceUSD)
: window.confirm(`Pay $${paymentInfo.priceUSD} USDC?`);
if (!confirmed) {
throw new Error('Payment cancelled');
}
}
// Sign and send payment
const paymentHeader = await createPaymentHeader(paymentInfo);
response = await fetch(url, {
headers: {
'x-payment': paymentHeader,
'payment-signature': paymentHeader,
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Payment failed');
}
setState(prev => ({
...prev,
loading: false,
lastPayment: { amount: `$${paymentInfo.priceUSD}`, endpoint },
}));
return response.json();
}
throw new Error(`Unexpected status: ${response.status}`);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
setState(prev => ({ ...prev, loading: false, error: message }));
throw error;
}
}, [createPaymentHeader, maxPaymentUSD, autoConfirm, onPaymentRequired]);
return { ...state, fetchWithPayment };
}
Usage in Component
import { useX402 } from './hooks/useX402';
function ENSLookup() {
const { fetchWithPayment, loading, error, lastPayment } = useX402({
maxPaymentUSD: 0.05,
onPaymentRequired: async (price) => {
return window.confirm(`This will cost $${price}. Continue?`);
},
});
const [result, setResult] = useState(null);
const handleLookup = async (name: string) => {
try {
const data = await fetchWithPayment(`/api/ens/resolve/${name}`);
setResult(data);
} catch (err) {
// Error is in hook state
}
};
return (
<div>
<button onClick={() => handleLookup('vitalik.eth')} disabled={loading}>
{loading ? 'Loading...' : 'Lookup vitalik.eth'}
</button>
{error && <p className="error">{error}</p>}
{lastPayment && <p className="success">Paid {lastPayment.amount}</p>}
{result && <pre>{JSON.stringify(result, null, 2)}</pre>}
</div>
);
}
Step 5: Budget Control System
Prevent overspending with a budget tracker:
// budget.ts
interface BudgetConfig {
dailyLimitUSD: number;
storageKey?: string;
}
interface BudgetState {
spent: number;
resetAt: number;
}
export class PaymentBudget {
private state: BudgetState;
private config: BudgetConfig;
constructor(config: BudgetConfig) {
this.config = config;
this.state = this.loadState();
this.checkReset();
}
canSpend(amount: number): boolean {
this.checkReset();
return this.state.spent + amount <= this.config.dailyLimitUSD;
}
recordSpend(amount: number): void {
this.state.spent += amount;
this.saveState();
}
getRemaining(): number {
this.checkReset();
return Math.max(0, this.config.dailyLimitUSD - this.state.spent);
}
getSpent(): number {
return this.state.spent;
}
private checkReset(): void {
if (Date.now() > this.state.resetAt) {
this.state = {
spent: 0,
resetAt: this.getNextMidnight(),
};
this.saveState();
}
}
private getNextMidnight(): number {
const d = new Date();
d.setHours(24, 0, 0, 0);
return d.getTime();
}
private loadState(): BudgetState {
const key = this.config.storageKey || 'x402-budget';
try {
const saved = localStorage.getItem(key);
if (saved) return JSON.parse(saved);
} catch {}
return { spent: 0, resetAt: this.getNextMidnight() };
}
private saveState(): void {
const key = this.config.storageKey || 'x402-budget';
localStorage.setItem(key, JSON.stringify(this.state));
}
}
// Usage with the hook
const budget = new PaymentBudget({ dailyLimitUSD: 1.00 });
function App() {
const { fetchWithPayment } = useX402({
onPaymentRequired: async (price) => {
if (!budget.canSpend(price)) {
alert(`Budget exceeded! Remaining: $${budget.getRemaining().toFixed(2)}`);
return false;
}
const ok = window.confirm(
`Pay $${price}? (Budget remaining: $${budget.getRemaining().toFixed(2)})`
);
if (ok) budget.recordSpend(price);
return ok;
},
});
// ...
}
Step 6: Testing Your Integration
Test Endpoints
| Endpoint | Price | Good for testing |
|---|---|---|
/api/ens/resolve/vitalik.eth | $0.01 | Basic flow |
/api/price/ETH | $0.001 | Cheapest option |
/api/farcaster/dwr.eth | $0.01 | Different data type |
Test Script
#!/bin/bash
# test-x402.sh - Test x402 payment flow
BASE_URL="https://api.web3identity.com"
echo "=== Testing Free Tier ==="
curl -s -w "\nStatus: %{http_code}\n" \
"$BASE_URL/api/ens/resolve/vitalik.eth" | head -20
echo ""
echo "=== Testing Usage Endpoint ==="
curl -s "$BASE_URL/api/usage" | jq
echo ""
echo "=== Testing Health ==="
curl -s "$BASE_URL/api/health" | jq
Verify Payment Settlement
After a successful payment, check your USDC balance changed on Base:
import { createPublicClient, http, formatUnits } from 'viem';
import { base } from 'viem/chains';
const client = createPublicClient({
chain: base,
transport: http(),
});
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
async function checkBalance(address) {
const balance = await client.readContract({
address: USDC_ADDRESS,
abi: [{
name: 'balanceOf',
type: 'function',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ type: 'uint256' }],
}],
functionName: 'balanceOf',
args: [address],
});
console.log('USDC Balance:', formatUnits(balance, 6));
}
Complete Example: CLI Tool
A full CLI tool with payment support:
#!/usr/bin/env node
// x402-cli.mjs
import { createWalletClient, http, parseUnits } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
import { program } from 'commander';
const BASE_URL = 'https://api.web3identity.com';
async function createPayment(wallet, info) {
const nonce = '0x' + [...crypto.getRandomValues(new Uint8Array(32))]
.map(b => b.toString(16).padStart(2, '0')).join('');
const signature = await wallet.signTypedData({
domain: { name: 'USD Coin', version: '2', chainId: 8453,
verifyingContract: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
types: {
TransferWithAuthorization: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
],
},
primaryType: 'TransferWithAuthorization',
message: {
from: wallet.account.address,
to: info.recipient,
value: parseUnits(info.priceUSD.toString(), 6),
validAfter: 0n,
validBefore: BigInt(Math.floor(Date.now() / 1000) + 300),
nonce,
},
});
return btoa(JSON.stringify({ signature }));
}
async function fetch402(endpoint, wallet) {
let res = await fetch(`${BASE_URL}${endpoint}`);
if (res.status === 402) {
const info = await res.json();
console.error(`Payment: $${info.priceUSD} USDC`);
const payment = await createPayment(wallet, info);
res = await fetch(`${BASE_URL}${endpoint}`, {
headers: { 'x-payment': payment, 'payment-signature': payment },
});
}
return res.json();
}
program
.command('ens <name>')
.description('Resolve ENS name')
.action(async (name) => {
const account = privateKeyToAccount(process.env.WALLET_KEY);
const wallet = createWalletClient({ account, chain: base, transport: http() });
console.log(JSON.stringify(await fetch402(`/api/ens/resolve/${name}`, wallet), null, 2));
});
program
.command('price <symbol>')
.description('Get token price')
.action(async (symbol) => {
const account = privateKeyToAccount(process.env.WALLET_KEY);
const wallet = createWalletClient({ account, chain: base, transport: http() });
console.log(JSON.stringify(await fetch402(`/api/price/${symbol}`, wallet), null, 2));
});
program.parse();
Usage:
chmod +x x402-cli.mjs
WALLET_KEY=0x... ./x402-cli.mjs ens vitalik.eth
WALLET_KEY=0x... ./x402-cli.mjs price ETH
Troubleshooting
| Error | Cause | Solution |
|---|---|---|
INSUFFICIENT_FUNDS | Not enough USDC | Add USDC on Base |
INVALID_SIGNATURE | Wrong message format | Check EIP-712 types |
PAYMENT_EXPIRED | 5-minute window passed | Sign new authorization |
ALREADY_USED | Nonce reused | Generate fresh nonce |
Next Steps
- Add SIWE authentication for 2x rate limits
- Build an ENS Profile Card UI
- Explore the SDK for simpler integration