Client Integration
The x402 client SDK makes it easy to add automatic payments to any application.
Installation
npm install @x402/core @x402/fetch @x402/evm @agentokratia/x402-escrow viem
Node.js / Agent (Balance-Aware)
For autonomous agents and server-side scripts, use balance-aware token selection:
import { wrapFetchWithPayment , x402Client } from '@x402/fetch' ;
import { ExactEvmScheme } from '@x402/evm/exact/client' ;
import { createWalletClient , createPublicClient , http } from 'viem' ;
import { privateKeyToAccount } from 'viem/accounts' ;
import { base } from 'viem/chains' ;
import {
EscrowScheme ,
createBalanceSelector ,
preferTokenPolicy ,
} from '@agentokratia/x402-escrow/client' ;
const WETH = '0x4200000000000000000000000000000000000006' ;
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' ;
const account = privateKeyToAccount ( process . env . PRIVATE_KEY as `0x ${ string } ` );
const walletClient = createWalletClient ({
account , chain: base , transport: http (),
});
const publicClient = createPublicClient ({
chain: base , transport: http (),
});
// Balance-aware: auto-picks USDC (gasless) or WETH/DAI (swap)
const client = new x402Client (
createBalanceSelector ( publicClient , account . address )
)
. register ( 'eip155:8453' , new ExactEvmScheme ( account ))
. register ( 'eip155:8453' , new EscrowScheme ( walletClient ))
. registerPolicy ( preferTokenPolicy ([ WETH , USDC ]));
const paidFetch = wrapFetchWithPayment ( fetch , client );
// 402 responses are handled automatically
const response = await paidFetch ( 'https://api.example.com/premium' );
const data = await response . json ();
Balance-aware selection: The createBalanceSelector checks on-chain balances and picks the first token the wallet can afford. Combined with preferTokenPolicy, you control the priority order.
Browser (wagmi)
For React applications with wagmi:
import { useWalletClient , usePublicClient , useAccount } from 'wagmi' ;
import { wrapFetchWithPayment , x402Client } from '@x402/fetch' ;
import { ExactEvmScheme } from '@x402/evm/exact/client' ;
import {
EscrowScheme ,
createBalanceSelector ,
} from '@agentokratia/x402-escrow/client' ;
function PaymentComponent () {
const { data : walletClient } = useWalletClient ();
const publicClient = usePublicClient ();
const { address } = useAccount ();
const callPaidApi = async () => {
if ( ! walletClient || ! publicClient || ! address ) return ;
const signer = {
address ,
signTypedData : ( msg ) => walletClient . signTypedData ({
account: walletClient . account ,
... msg
}),
};
const client = new x402Client (
createBalanceSelector ( publicClient , address )
)
. register ( 'eip155:8453' , new ExactEvmScheme ( signer ))
. register ( 'eip155:8453' , new EscrowScheme ( walletClient ));
const paidFetch = wrapFetchWithPayment ( fetch , client );
const response = await paidFetch ( 'https://api.example.com/premium' );
return response . json ();
};
return < button onClick ={ callPaidApi }> Call Paid API </ button > ;
}
Getting a WalletClient
The escrow client requires a viem WalletClient. Here’s how to get one:
Option A: With wagmi (React)
import { useWalletClient } from 'wagmi' ;
function MyComponent () {
const { data : walletClient } = useWalletClient ();
// walletClient is available when wallet is connected
}
Option B: With viem directly (Browser)
import { createWalletClient , custom } from 'viem' ;
import { base } from 'viem/chains' ;
// Browser with injected wallet (MetaMask, etc.)
const walletClient = createWalletClient ({
chain: base ,
transport: custom ( window . ethereum ),
});
// Request account access
const [ address ] = await walletClient . requestAddresses ();
Option C: With private key (scripts/testing only)
import { createWalletClient , http } from 'viem' ;
import { privateKeyToAccount } from 'viem/accounts' ;
import { baseSepolia } from 'viem/chains' ;
const walletClient = createWalletClient ({
account: privateKeyToAccount ( process . env . PRIVATE_KEY as `0x ${ string } ` ),
chain: baseSepolia ,
transport: http (),
});
Never use private keys in browser code. Option C is for server-side scripts and testing only.
Token Selection Policies
Control which token the client prefers:
import { preferTokenPolicy } from '@agentokratia/x402-escrow/client' ;
const WETH = '0x4200000000000000000000000000000000000006' ;
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' ;
const DAI = '0x50c5725949a6f0c72e6c4a641f24049a917db0cb' ;
// Prefer WETH first, then USDC, then DAI
const client = new x402Client (
createBalanceSelector ( publicClient , account . address )
)
. register ( 'eip155:8453' , new ExactEvmScheme ( account ))
. register ( 'eip155:8453' , new EscrowScheme ( walletClient ))
. registerPolicy ( preferTokenPolicy ([ WETH , USDC , DAI ]));
Manual Balance Checking
For custom balance logic:
import { checkBalance } from '@agentokratia/x402-escrow/client' ;
const canAfford = await checkBalance (
publicClient ,
account . address ,
USDC ,
BigInt ( '1000000' ) // $1 USDC
);
if ( canAfford ) {
// Proceed with payment
}
API Exports
Client Exports
Export Description EscrowSchemeClient scheme for x402Client (takes WalletClient) createBalanceSelectorAsync selector that checks on-chain balances preferTokenPolicySync policy that reorders by token preference checkBalanceUtility for custom balance checks signERC3009Sign ERC-3009 authorization signPermit2TransferFromSign Permit2 transfer computePaymentNonceDerive deterministic nonce from payment params PERMIT2_ADDRESSUniversal Permit2 contract address decompressCalldataDecompress gzipped aggregator calldata
Supported Input Tokens (Base Mainnet)
Token Address USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913WETH 0x4200000000000000000000000000000000000006DAI 0x50c5725949a6f0c72e6c4a641f24049a917db0cbUSDT 0xfde4c96c8593536e31f229ea8f37b2ada2699bb2
Error Handling
try {
const response = await paidFetch ( url );
if ( ! response . ok ) {
const error = await response . json ();
console . error ( 'Payment failed:' , error );
}
} catch ( err ) {
if ( err . message . includes ( 'User rejected' )) {
// User cancelled wallet signature
console . log ( 'Transaction cancelled by user' );
} else {
// Network or other error
console . error ( 'Error:' , err );
}
}
TypeScript Types
import type {
EscrowSchemeOptions ,
} from '@agentokratia/x402-escrow/client' ;
React Example
import { useCallback , useState } from 'react' ;
import { useWalletClient , usePublicClient , useAccount } from 'wagmi' ;
import { wrapFetchWithPayment , x402Client } from '@x402/fetch' ;
import { ExactEvmScheme } from '@x402/evm/exact/client' ;
import {
EscrowScheme ,
createBalanceSelector ,
} from '@agentokratia/x402-escrow/client' ;
function PaidApiButton () {
const { data : walletClient } = useWalletClient ();
const publicClient = usePublicClient ();
const { address } = useAccount ();
const [ result , setResult ] = useState < string | null >( null );
const [ loading , setLoading ] = useState ( false );
const callApi = useCallback ( async () => {
if ( ! walletClient || ! publicClient || ! address ) return ;
setLoading ( true );
try {
const signer = {
address ,
signTypedData : ( msg ) => walletClient . signTypedData ({
account: walletClient . account ,
... msg
}),
};
const client = new x402Client (
createBalanceSelector ( publicClient , address )
)
. register ( 'eip155:8453' , new ExactEvmScheme ( signer ))
. register ( 'eip155:8453' , new EscrowScheme ( walletClient ));
const paidFetch = wrapFetchWithPayment ( fetch , client );
const response = await paidFetch ( 'https://api.example.com/premium' );
const data = await response . json ();
setResult ( JSON . stringify ( data , null , 2 ));
} catch ( err ) {
setResult ( `Error: ${ err . message } ` );
} finally {
setLoading ( false );
}
}, [ walletClient , publicClient , address ]);
return (
< div >
< button onClick = { callApi } disabled = { loading || ! walletClient } >
{ loading ? 'Processing...' : 'Call Paid API' }
</ button >
{ result && < pre > { result } </ pre > }
</ div >
);
}
For production, consider memoizing the x402 client setup to avoid recreating it on every call.
Troubleshooting
When users click “Reject” in their wallet: try {
const response = await paidFetch ( url );
} catch ( err ) {
if ( err . message . includes ( 'User rejected' )) {
// User cancelled - show friendly message
alert ( 'Transaction cancelled' );
}
}
Ensure your wallet is on the correct network (Base Mainnet or Base Sepolia): import { base , baseSepolia } from 'viem/chains' ;
// Check chain before making request
if ( walletClient . chain . id !== base . id ) {
await walletClient . switchChain ({ id: base . id });
}
Insufficient token balance
The wallet needs enough tokens for the payment. The createBalanceSelector automatically picks tokens you can afford: import { formatUnits } from 'viem' ;
import { erc20Abi } from 'viem' ;
const balance = await publicClient . readContract ({
address: USDC_ADDRESS ,
abi: erc20Abi ,
functionName: 'balanceOf' ,
args: [ walletClient . account . address ],
});
console . log ( `USDC Balance: $ ${ formatUnits ( balance , 6 ) } ` );