Handling x402 Payments
Learn how to implement x402 payment flows in your application to access paid API endpoints.
What is x402?โ
x402 is a protocol for HTTP-native payments. When you hit a paid endpoint:
- Server returns
402 Payment Requiredwith payment details - Client pays via blockchain (USDC on Base)
- Client retries with payment proof in header
- Server validates payment and returns data
Why x402?โ
| Feature | x402 | Traditional API Keys |
|---|---|---|
| Signup required | โ No | โ Yes |
| Credit card needed | โ No | โ Yes |
| Pay per request | โ Yes | โ Subscription |
| Programmable | โ Fully | โ Limited |
| Agent-friendly | โ Ideal | โ Manual |
Prerequisitesโ
- A wallet with USDC on Base network
- Understanding of Ethereum transactions
- Node.js 18+ or modern browser
Step 1: Understand the Flowโ
โโโโโโโโโโโ GET /api/ens/resolve/vitalik.eth โโโโโโโโโโโ
โ Client โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโถ โ Server โ
โโโโโโโโโโโ โโโโโโโโโโโ
โ โ
โ 402 Payment Required โ
โ x-payment-request: {...} โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ [Client signs payment tx] โ
โ โ
โ GET /api/ens/resolve/vitalik.eth โ
โ x-payment: <signed_payment> โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโถ โ
โ โ
โ 200 OK + data โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
Step 2: Install Dependenciesโ
npm install viem @x402/client
Or use the Web3 Identity SDK which handles this automatically:
npm install @web3identity/sdk
Step 3: Basic Implementationโ
Option A: Using the SDK (Recommended)โ
import { Web3IdentityClient } from '@web3identity/sdk';
import { createWalletClient, http } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
// Create wallet for payments
const account = privateKeyToAccount('0x...');
const walletClient = createWalletClient({
account,
chain: base,
transport: http()
});
// Initialize client with payment capability
const client = new Web3IdentityClient({
wallet: walletClient,
autoPayment: true, // Automatically pay for 402 responses
maxPayment: 0.10, // Max $0.10 per request
});
// Use normally - payments handled automatically
const profile = await client.getProfile('vitalik.eth');
Option B: Manual Implementationโ
import { createWalletClient, http, parseUnits } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
const API_BASE = 'https://api.web3identity.com';
interface PaymentRequest {
payTo: string;
maxAmount: string;
asset: string;
network: string;
nonce: string;
}
async function fetchWithPayment(
url: string,
wallet: any
): Promise<Response> {
// Initial request
let response = await fetch(url);
// If payment required
if (response.status === 402) {
const paymentRequest = response.headers.get('x-payment-request');
if (!paymentRequest) {
throw new Error('No payment request header');
}
const request: PaymentRequest = JSON.parse(paymentRequest);
// Sign and send payment
const payment = await createPayment(wallet, request);
// Retry with payment
response = await fetch(url, {
headers: {
'x-payment': payment,
'payment-signature': payment, // Required by middleware
}
});
}
return response;
}
async function createPayment(
wallet: any,
request: PaymentRequest
): Promise<string> {
// Create payment authorization
const message = {
payTo: request.payTo,
maxAmount: request.maxAmount,
asset: request.asset,
network: request.network,
nonce: request.nonce,
validUntil: Math.floor(Date.now() / 1000) + 300, // 5 minutes
};
// Sign the payment authorization
const signature = await wallet.signTypedData({
domain: {
name: 'x402',
version: '1',
chainId: 8453, // Base
},
types: {
Payment: [
{ name: 'payTo', type: 'address' },
{ name: 'maxAmount', type: 'uint256' },
{ name: 'asset', type: 'address' },
{ name: 'nonce', type: 'string' },
{ name: 'validUntil', type: 'uint256' },
],
},
primaryType: 'Payment',
message,
});
// Encode payment header
return btoa(JSON.stringify({ ...message, signature }));
}
Step 4: React Hook Implementationโ
import { useState, useCallback } from 'react';
import { useWalletClient } from 'wagmi';
interface PaymentState {
pending: boolean;
error: string | null;
lastPayment: {
amount: string;
txHash: string;
} | null;
}
export function useX402Payment() {
const { data: walletClient } = useWalletClient();
const [state, setState] = useState<PaymentState>({
pending: false,
error: null,
lastPayment: null,
});
const fetchWithPayment = useCallback(async (url: string) => {
if (!walletClient) {
throw new Error('Wallet not connected');
}
setState(prev => ({ ...prev, pending: true, error: null }));
try {
// Initial fetch
let response = await fetch(url);
if (response.status === 402) {
const paymentHeader = response.headers.get('x-payment-request');
if (!paymentHeader) {
throw new Error('Missing payment request');
}
const paymentRequest = JSON.parse(paymentHeader);
// Confirm with user (optional)
const confirmed = window.confirm(
`This request costs ${paymentRequest.maxAmount} USDC. Continue?`
);
if (!confirmed) {
throw new Error('Payment cancelled by user');
}
// Create and sign payment
const payment = await createPayment(walletClient, paymentRequest);
// Retry with payment
response = await fetch(url, {
headers: {
'x-payment': payment,
'payment-signature': payment,
},
});
setState(prev => ({
...prev,
lastPayment: {
amount: paymentRequest.maxAmount,
txHash: 'pending',
},
}));
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
setState(prev => ({ ...prev, pending: false }));
return response.json();
} catch (error) {
const message = error instanceof Error ? error.message : 'Payment failed';
setState(prev => ({ ...prev, pending: false, error: message }));
throw error;
}
}, [walletClient]);
return { ...state, fetchWithPayment };
}
Step 5: Payment Confirmation UIโ
import React from 'react';
import { useX402Payment } from '../hooks/useX402Payment';
interface PaymentConfirmProps {
endpoint: string;
cost: string;
onConfirm: () => void;
onCancel: () => void;
}
export function PaymentConfirm({
endpoint,
cost,
onConfirm,
onCancel,
}: PaymentConfirmProps) {
return (
<div className="payment-modal">
<div className="modal-content">
<h3>Payment Required</h3>
<div className="payment-details">
<div className="detail">
<span className="label">Endpoint</span>
<code className="value">{endpoint}</code>
</div>
<div className="detail">
<span className="label">Cost</span>
<span className="value">${cost} USDC</span>
</div>
<div className="detail">
<span className="label">Network</span>
<span className="value">Base</span>
</div>
</div>
<p className="notice">
This payment will be signed by your wallet.
No transaction will be sent until confirmed.
</p>
<div className="actions">
<button onClick={onCancel} className="btn-secondary">
Cancel
</button>
<button onClick={onConfirm} className="btn-primary">
Pay ${cost}
</button>
</div>
</div>
</div>
);
}
Step 6: Budget Managementโ
Implement spending controls:
class PaymentBudget {
private spent: number = 0;
private limit: number;
private resetTime: number;
constructor(dailyLimitUSD: number) {
this.limit = dailyLimitUSD;
this.resetTime = this.getNextMidnight();
this.loadState();
}
canSpend(amount: number): boolean {
this.checkReset();
return this.spent + amount <= this.limit;
}
recordSpend(amount: number): void {
this.spent += amount;
this.saveState();
}
getRemaining(): number {
this.checkReset();
return this.limit - this.spent;
}
private checkReset(): void {
if (Date.now() > this.resetTime) {
this.spent = 0;
this.resetTime = this.getNextMidnight();
this.saveState();
}
}
private getNextMidnight(): number {
const tomorrow = new Date();
tomorrow.setHours(24, 0, 0, 0);
return tomorrow.getTime();
}
private saveState(): void {
localStorage.setItem('x402-budget', JSON.stringify({
spent: this.spent,
resetTime: this.resetTime,
}));
}
private loadState(): void {
const saved = localStorage.getItem('x402-budget');
if (saved) {
const state = JSON.parse(saved);
this.spent = state.spent;
this.resetTime = state.resetTime;
}
}
}
// Usage
const budget = new PaymentBudget(1.00); // $1/day limit
async function fetchWithBudget(url: string, cost: number) {
if (!budget.canSpend(cost)) {
throw new Error(`Daily budget exceeded. Remaining: $${budget.getRemaining()}`);
}
const data = await fetchWithPayment(url);
budget.recordSpend(cost);
return data;
}
Step 7: Server-Side Implementationโ
For Node.js backends that proxy API requests:
import { createWalletClient, http } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`);
const wallet = createWalletClient({
account,
chain: base,
transport: http(),
});
export async function proxyAPIRequest(endpoint: string) {
const url = `https://api.web3identity.com${endpoint}`;
let response = await fetch(url);
if (response.status === 402) {
const paymentRequest = JSON.parse(
response.headers.get('x-payment-request') || '{}'
);
// Create payment
const payment = await createServerPayment(wallet, paymentRequest);
// Retry with payment
response = await fetch(url, {
headers: {
'x-payment': payment,
'payment-signature': payment,
},
});
}
return response.json();
}
Cost Referenceโ
| Endpoint | Cost |
|---|---|
/api/ens/{name} | $0.01 |
/api/ens/avatar/{name} | $0.001 |
/api/farcaster/{id} | $0.01 |
/api/price/{symbol} | $0.001 |
/api/price/batch | $0.002 |
/api/bulk/ens | $0.005/name |
Full pricing: /overview/pricing
Error Handlingโ
async function handlePaymentError(error: any) {
if (error.code === 'INSUFFICIENT_FUNDS') {
// Prompt user to add USDC
showModal('Add USDC to your wallet on Base network');
} else if (error.code === 'USER_REJECTED') {
// User cancelled
console.log('Payment cancelled');
} else if (error.code === 'PAYMENT_EXPIRED') {
// Retry the request
return retryRequest();
} else {
// Unknown error
console.error('Payment error:', error);
throw error;
}
}
Testingโ
Use testnet for development:
const client = new Web3IdentityClient({
baseUrl: 'https://api-testnet.web3identity.com',
wallet: walletClient,
// Uses Base Sepolia testnet USDC
});
Security Best Practicesโ
- Never expose private keys in client-side code
- Set spending limits per session/day
- Confirm large payments with user
- Log all payments for accounting
- Use hardware wallets for production
Next Stepsโ
- Explore ENS Profile Viewer tutorial
- Add Farcaster integration
- Review pricing for cost planning