Skip to main content

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:

  1. Server returns 402 Payment Required with payment details
  2. Client pays via blockchain (USDC on Base)
  3. Client retries with payment proof in header
  4. Server validates payment and returns data

Why x402?โ€‹

Featurex402Traditional 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โ€‹

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โ€‹

EndpointCost
/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โ€‹

  1. Never expose private keys in client-side code
  2. Set spending limits per session/day
  3. Confirm large payments with user
  4. Log all payments for accounting
  5. Use hardware wallets for production

Next Stepsโ€‹