Quickstart for Buyers
Pay for paywalled APIs and services using any token with the AnySpend x402 Client SDK
Overview
This guide shows you how to use the AnySpend x402 Client SDK to pay for paywalled resources using compatible tokens with EIP-2612 or EIP-3009 support. The SDK handles the entire payment flow automatically, including signature generation and retry logic.
You can use this same client to pay for any x402-enabled service, not just those using AnySpend.
Prerequisites
Before you begin, make sure you have:
- Node.js 18+ or compatible JavaScript runtime
- A crypto wallet with compatible tokens (USDC, DAI, or other EIP-2612/EIP-3009 tokens)
- A wallet library like viem or ethers.js
- Access to an x402-enabled service (like a paywalled API)
Important: Only tokens with EIP-2612 (Permit) or EIP-3009 (TransferWithAuthorization) support are compatible with AnySpend x402 for gasless payments. Use the Token Compatibility Checker to verify your token is supported.
Installation
Install the x402 client SDK:
npm install anyspend-x402-client viemyarn add anyspend-x402-client viempnpm add anyspend-x402-client viemThe SDK uses viem for wallet operations and signature generation.
Basic usage
1. Create a wallet client
First, set up your wallet client using viem:
typescriptimport { createWalletClient, custom } from 'viem'; import { base } from 'viem/chains'; // For browser with MetaMask or other wallet extension const walletClient = createWalletClient({ chain: base, transport: custom(window.ethereum) }); // Get the user's address const [address] = await walletClient.getAddresses();
2. Initialize the x402 client
Create an instance of the x402 client:
typescriptimport { X402Client } from 'anyspend-x402-client'; const x402Client = new X402Client({ walletClient, // Your viem wallet client preferredToken: '0x...', // Optional: default token to pay with autoRetry: true // Auto-handle 402 responses (default: true) });
3. Make a paid request
Use the client to access paywalled resources:
typescripttry { const response = await x402Client.request( 'https://api.example.com/premium-data', { method: 'GET', } ); console.log('Data:', response.data); console.log('Payment TX:', response.paymentResponse?.txHash); } catch (error) { console.error('Payment failed:', error); }
What happens:
- Client makes initial request
- API returns
402 Payment Requiredwith payment details - X402Client automatically signs the payment authorization
- Client retries the request with payment
- API verifies and settles the payment
- Client receives the requested data
Advanced usage
Specify payment token
There are two ways to specify which token you want to pay with:
Option 1: using X402Client
Pay with a specific token by setting preferredToken:
typescriptconst response = await x402Client.request( 'https://ai-agent-api.com/inference', { method: 'POST', body: { prompt: 'Generate an image of a cat' }, preferredToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC on Base } );
Option 2: using wrapFetchWithPayment (advanced)
For more control, use the lower-level wrapFetchWithPayment function:
typescriptimport { wrapFetchWithPayment } from 'anyspend-x402-client'; const tokenAddress = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base const selectedChain = 8453; // Base const paymentPreferences = { preferredToken: tokenAddress, preferredNetwork: 'base' as const, // or use getNetworkName(selectedChain) }; // Wrap fetch with automatic payment handling const fetchWithPayment = wrapFetchWithPayment( fetch, walletClient, '1000000', // Max payment value (1 USDC in 6 decimals) undefined, // Use default payment requirements selector undefined, // Use default config paymentPreferences, // Specify your preferred token and network ); // Use the wrapped fetch like normal fetch const response = await fetchWithPayment('https://api.example.com/premium-data'); const data = await response.json();
Benefits of wrapFetchWithPayment:
- Works with any existing
fetch-based code - More granular control over payment behavior
- Can specify max payment value
- Custom payment requirements selector
- Supports multi-network signers
If the resource server supports AnySpend middleware, you'll pay the equivalent amount in your preferred token instead of USDC.
Using HTTP headers for payment preferences
New in X402: You can now specify payment preferences using HTTP headers, which is particularly useful when making direct HTTP requests or when integrating with existing HTTP clients.
When making requests to X402-enabled services, you can specify your preferred payment token and network using these HTTP headers:
bashcurl -X GET https://api.example.com/premium-data \ -H "X-PREFERRED-TOKEN: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" \ -H "X-PREFERRED-NETWORK: base-mainnet"
Common Token Addresses:
- USDC on Base:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 - USDT on Base:
0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2
Network Identifiers:
base-mainnet- Base Mainnetethereum-mainnet- Ethereum Mainnetarbitrum-mainnet- Arbitrum Oneoptimism-mainnet- Optimism Mainnetpolygon-mainnet- Polygon PoS
These headers work with any HTTP client and are automatically respected by X402-enabled resource servers using AnySpend middleware. If no headers are provided, the server will default to USDC.
Using headers with X402Client
The X402Client automatically adds these headers based on your configuration:
typescriptconst x402Client = new X402Client({ walletClient, preferredToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC network: 'base-mainnet' }); // Headers are automatically added to all requests const response = await x402Client.request('https://api.example.com/data');
Using headers with fetch
You can also add these headers manually when using the native fetch API or other HTTP clients:
typescriptconst response = await fetch('https://api.example.com/premium-data', { headers: { 'X-PREFERRED-TOKEN': '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', 'X-PREFERRED-NETWORK': 'base-mainnet' } }); // Then handle the 402 response if needed if (response.status === 402) { // Process payment using X402Client or wrapFetchWithPayment }
POST requests with body
Make paid POST requests:
typescriptconst response = await x402Client.request( 'https://api.example.com/compute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: { model: 'gpt-4', messages: [{ role: 'user', content: 'Hello!' }] } } );
Manual payment flow
For more control, handle the payment flow manually:
typescript// 1. Make initial request const initialResponse = await fetch('https://api.example.com/premium-data'); if (initialResponse.status === 402) { // 2. Parse payment requirements const paymentRequired = await initialResponse.json(); const requirements = paymentRequired.requirements[0]; console.log('Payment required:', { amount: requirements.amount, token: requirements.asset, recipient: requirements.recipient }); // 3. Sign payment authorization const paymentHeader = await x402Client.signPayment(requirements); // 4. Retry with payment const paidResponse = await fetch('https://api.example.com/premium-data', { headers: { 'X-PAYMENT': paymentHeader } }); const data = await paidResponse.json(); const paymentResponse = paidResponse.headers.get('X-PAYMENT-RESPONSE'); console.log('Payment successful!', JSON.parse(atob(paymentResponse))); }
Configuration options
X402ClientOptions
typescriptinterface X402ClientOptions { walletClient: WalletClient; // viem wallet client (required) network?: string; // Default network (e.g., 'base-mainnet') preferredToken?: string; // Preferred payment token address autoRetry?: boolean; // Auto-retry on 402 (default: true) timeout?: number; // Request timeout in ms (default: 30000) }
Request options
typescriptinterface RequestOptions { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; headers?: Record<string, string>; body?: any; // Request body (auto-serialized to JSON) preferredToken?: string; // Override default preferred token }
wrapFetchWithPayment Parameters
For advanced use cases, wrapFetchWithPayment provides lower-level control:
typescriptfunction wrapFetchWithPayment( fetchFn: typeof fetch, // Fetch function to wrap signer: Signer | MultiNetworkSigner, // Wallet client for signing maxPaymentValue: string, // Max payment in smallest unit (e.g., '1000000' for 1 USDC) selectPaymentRequirements?: Function, // Custom requirements selector config?: { timeout?: number; // Request timeout in ms maxRetries?: number; // Max payment retry attempts }, paymentPreferences?: { preferredToken?: string; // Token address to pay with preferredNetwork?: string; // Network identifier (e.g., 'base', 'ethereum') } ): typeof fetch
Example with all parameters:
typescriptimport { wrapFetchWithPayment } from 'anyspend-x402-client'; const fetchWithPayment = wrapFetchWithPayment( fetch, walletClient, '5000000', // Max 5 USDC undefined, // Default requirements selector { timeout: 60000, // 60 second timeout maxRetries: 3 // Retry up to 3 times }, { preferredToken: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb', // DAI preferredNetwork: 'base' } );
Supported tokens
Only tokens with EIP-2612 or EIP-3009 support are compatible with AnySpend x402 for gasless payments.
What are EIP-2612 and EIP-3009?
- EIP-2612 (Permit): Allows gasless token approvals via off-chain signatures
- EIP-3009 (TransferWithAuthorization): Enables gasless transfers via off-chain signatures (used by USDC)
Common compatible tokens
- USDC - EIP-3009 support on all chains
- B3 - EIP-2612 permit support
- Many other tokens - Check compatibility using the tool below
See Network Support for detailed token addresses per chain.
Examples
React component (using X402Client)
tsximport { X402Client } from 'anyspend-x402-client'; import { useWalletClient } from 'wagmi'; import { useState } from 'react'; export function PremiumDataFetcher() { const { data: walletClient } = useWalletClient(); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const fetchPremiumData = async () => { if (!walletClient) return; setLoading(true); try { const client = new X402Client({ walletClient, preferredToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC on Base }); const response = await client.request( 'https://api.example.com/premium-data' ); setData(response.data); console.log('Paid with TX:', response.paymentResponse?.txHash); } catch (error) { console.error('Payment failed:', error); } finally { setLoading(false); } }; return ( <div> <button onClick={fetchPremiumData} disabled={loading}> {loading ? 'Paying...' : 'Get Premium Data (Costs 1 USDC)'} </button> {data && <pre>{JSON.stringify(data, null, 2)}</pre>} </div> ); }
React component (using wrapFetchWithPayment)
For applications that need more control or use existing fetch-based code:
tsximport { wrapFetchWithPayment } from 'anyspend-x402-client'; import { useWalletClient, useChainId } from 'wagmi'; import { useState } from 'react'; // Helper to get network name from chain ID const getNetworkName = (chainId: number): string => { const networks: Record<number, string> = { 8453: 'base', 1: 'ethereum', 42161: 'arbitrum', 10: 'optimism', 137: 'polygon', }; return networks[chainId] || 'base'; }; export function PremiumDataFetcherAdvanced() { const { data: walletClient } = useWalletClient(); const selectedChain = useChainId(); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [log, setLog] = useState<string[]>([]); // Token addresses by chain const tokenAddresses: Record<number, string> = { 8453: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base 1: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum }; const fetchPremiumData = async () => { if (!walletClient) return; setLoading(true); setLog([]); try { const tokenAddress = tokenAddresses[selectedChain]; const paymentPreferences = { preferredToken: tokenAddress, preferredNetwork: getNetworkName(selectedChain) as any, }; setLog(prev => [...prev, `💡 Using preferred token: ${tokenAddress} on ${getNetworkName(selectedChain)}` ]); // Wrap fetch with automatic payment handling const fetchWithPayment = wrapFetchWithPayment( fetch, walletClient as any, '1000000', // Max 1 USDC undefined, // Use default payment requirements selector undefined, // Use default config paymentPreferences, ); setLog(prev => [...prev, '🔄 Making request...']); const response = await fetchWithPayment('https://api.example.com/premium-data'); const result = await response.json(); setData(result); setLog(prev => [...prev, '✅ Payment successful!']); } catch (error) { console.error('Payment failed:', error); setLog(prev => [...prev, `❌ Error: ${error.message}`]); } finally { setLoading(false); } }; return ( <div> <button onClick={fetchPremiumData} disabled={loading}> {loading ? 'Processing Payment...' : 'Get Premium Data'} </button> {log.length > 0 && ( <div style={{ marginTop: '1rem', fontFamily: 'monospace' }}> {log.map((entry, i) => <div key={i}>{entry}</div>)} </div> )} {data && <pre>{JSON.stringify(data, null, 2)}</pre>} </div> ); }
Node.js script
typescriptimport { X402Client } from 'anyspend-x402-client'; import { createWalletClient, http } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { base } from 'viem/chains'; // Load private key from environment const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const walletClient = createWalletClient({ account, chain: base, transport: http() }); const x402Client = new X402Client({ walletClient, preferredToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC on Base }); async function main() { const response = await x402Client.request( 'https://api.example.com/ai-inference', { method: 'POST', body: { prompt: 'Analyze this data...' } } ); console.log('Result:', response.data); console.log('Payment TX:', response.paymentResponse?.txHash); } main();
Error handling
The SDK throws typed errors for common failure scenarios:
typescriptimport { X402Error } from 'anyspend-x402-client'; try { const response = await x402Client.request('https://api.example.com/data'); } catch (error) { if (error instanceof X402Error) { switch (error.code) { case 'INSUFFICIENT_BALANCE': console.error('Not enough tokens in wallet'); break; case 'SIGNATURE_REJECTED': console.error('User rejected the signature request'); break; case 'PAYMENT_EXPIRED': console.error('Payment deadline expired, try again'); break; case 'SETTLEMENT_FAILED': console.error('Payment verification failed:', error.message); break; default: console.error('Payment error:', error.message); } } }
Testing
Test your integration against a test resource server:
typescriptconst testClient = new X402Client({ walletClient, network: 'base-sepolia', // Use testnet preferredToken: '0x036CbD53842c5426634e7929541eC2318f3dCF7e' // USDC on Sepolia }); const response = await testClient.request( 'https://test-api.anyspend.com/echo' );
Get testnet USDC from Coinbase Faucet.
Best practices
Never hardcode private keys in your application:
typescriptconst account = privateKeyToAccount( process.env.PRIVATE_KEY as `0x${string}` );
Before initiating payment, show users:
- The exact amount they'll pay
- The token being used
- The resource server domain
- An option to cancel
Users can reject wallet signature requests. Always handle this gracefully:
typescripttry { await x402Client.request('...'); } catch (error) { if (error.code === 'SIGNATURE_REJECTED') { // User cancelled - don't show error UI return; } }
Create one X402Client instance and reuse it:
typescript// Good: Create once const client = new X402Client({ walletClient }); // Bad: Creating new instance for each request await new X402Client({ walletClient }).request('...');
What's next
Troubleshooting
The signature verification failed. Check that:
- Your wallet is connected to the correct network
- The token address matches the network
- Your wallet has enough tokens for the payment
You don't have enough of the payment token. Either:
- Add more tokens to your wallet
- Try paying with a different token
Make sure your wallet is on the same network as the payment token:
typescript// Base network const walletClient = createWalletClient({ chain: base, // Must match the payment network transport: custom(window.ethereum) });
Getting help
- Discord: Join our Discord community
- GitHub: github.com/b3-fun/anyspend-x402
- Examples: See full examples in the GitHub repository