Skip to main content

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

ExportDescription
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)

TokenAddress
USDC0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
WETH0x4200000000000000000000000000000000000006
DAI0x50c5725949a6f0c72e6c4a641f24049a917db0cb
USDT0xfde4c96c8593536e31f229ea8f37b2ada2699bb2

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 });
}
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)}`);