Dexter
Dexter
Docs
Pay for APIs

Verified against @dexterai/x402 5.3.1 · 2026-07-07

Pay per call from a Node agent

By the end of this page you will have a Node script that holds its own Solana keypair, pays a 402-gated endpoint in USDC, and reads back the settled receipt. No browser, no wallet extension. The whole journey is install, make a wallet, fund it, pay a call, read the receipt.

The one thing that trips up a first run is funding. Read that step in full: you fund USDC, and you do not need SOL.

What you need

  • Node 18 or newer. The examples use ES modules and top-level await, so run them as .mjs files or set "type": "module" in your package.json.
  • An endpoint that returns HTTP 402 with x402 payment terms. If you don't have one, build it from Get paid — sell with tabs.
  • USDC on Solana to fund the wallet. Covered below.

Install

npm install @dexterai/x402 @solana/web3.js @solana/spl-token bs58 viem
  • @dexterai/x402 is the client.
  • @solana/web3.js generates the keypair; bs58 encodes its secret key.
  • @solana/spl-token is used only by the balance check in step 2.
  • viem is a hard requirement of @dexterai/x402/client — the entry point imports it at load time even on a Solana-only path, so importing the client without viem installed throws Cannot find package 'viem'. Installing it is enough; you never import it yourself for Solana.

Step 1 — Make a wallet

createKeypairWallet turns a Solana secret key into a wallet the client can sign with. Generate one keypair once, print its address and secret, and save the secret to an environment variable. The address is where you send funds.

generate-wallet.ts
import { Keypair } from '@solana/web3.js';
import bs58 from 'bs58';
 
const kp = Keypair.generate();
 
console.log('address:', kp.publicKey.toBase58());
console.log('secret (set SOLANA_PRIVATE_KEY to this):', bs58.encode(kp.secretKey));

Run it once and put the secret in your environment, not in source control:

node generate-wallet.mjs
export SOLANA_PRIVATE_KEY="<the base58 string it printed>"

createKeypairWallet accepts that base58 string, or the 64-byte JSON array that solana-keygen writes to an id.json, or a raw Uint8Array. All three produce the same wallet.

Step 2 — Fund it with USDC

Send USDC on Solana to the address from step 1. Any exchange or wallet that supports Solana USDC withdrawals (Coinbase, Kraken, Phantom, Backpack) works — withdraw USDC on the Solana network to the address. That first transfer also creates the wallet's USDC token account for you.

You fund USDC only. You do not need SOL in this wallet. When you pay through Dexter's facilitator, the facilitator is the transaction fee payer on Solana — it signs and pays the network fee, your wallet only signs the transfer. A wallet with USDC and zero SOL pays fine. Funding SOL but no USDC is the common first-run mistake: payAndFetch then returns { ok: false, reason: 'insufficient_funds' }.

Confirm the USDC landed before you try to pay. This helper reads the wallet's USDC balance. It returns 0 only when the token account does not exist yet; a real RPC error is re-thrown rather than being reported as an empty balance.

check-balance.ts
import { Connection, PublicKey } from '@solana/web3.js';
import {
  getAssociatedTokenAddress,
  getAccount,
  TokenAccountNotFoundError,
} from '@solana/spl-token';
import { USDC_MINT } from '@dexterai/x402/client';
 
const RPC = process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com';
const mint = new PublicKey(USDC_MINT);
 
async function usdcBalance(address: string): Promise<number> {
  const conn = new Connection(RPC, 'confirmed');
  const ata = await getAssociatedTokenAddress(mint, new PublicKey(address));
  try {
    const acct = await getAccount(conn, ata);
    return Number(acct.amount) / 1e6; // USDC has 6 decimals
  } catch (err) {
    if (err instanceof TokenAccountNotFoundError) return 0; // never funded
    throw err; // a throttled or failed RPC must not read as $0
  }
}
 
console.log('USDC balance:', await usdcBalance(process.env.WALLET_ADDRESS!));

USDC_MINT is exported by the SDK so you don't hardcode the mint address. Public RPC endpoints rate-limit balance reads; for anything beyond a one-off check, set SOLANA_RPC_URL to your own RPC.

Step 3 — Pay a call and read the receipt

payAndFetch(url, init, wallets, opts) probes the endpoint once. If it answers 402, the client signs a payment with your wallet, retries, and returns a PayResult. It never throws for an expected failure — you narrow the result instead.

pay.ts
import {
  payAndFetch,
  createKeypairWallet,
  getPaymentReceipt,
} from '@dexterai/x402/client';
 
const wallet = await createKeypairWallet(process.env.SOLANA_PRIVATE_KEY!);
 
const result = await payAndFetch(
  'https://your-api.example.com/paid/route',
  { method: 'GET' },
  { solana: wallet },
  { maxAmountAtomic: '10000' }, // cap this call at 0.01 USDC (10000 atomic units)
);
 
if (result.ok && result.paid) {
  if (!result.response) {
    console.error('paid, but the seller never answered before the deadline');
  } else {
    const data = await result.response.json();
    console.log('data:', data);
    console.log(
      `paid ${result.amountPaid} atomic on ${result.network.bare}`,
      result.txSignature ? `tx ${result.txSignature}` : '',
    );
 
    const receipt = getPaymentReceipt(result.response);
    if (receipt?.amountAtomic && typeof receipt.assetDecimals === 'number') {
      const usd = Number(receipt.amountAtomic) / 10 ** receipt.assetDecimals;
      console.log(`receipt: ${usd} USDC → ${receipt.payer} (${receipt.transaction})`);
    }
  }
} else if (result.ok && !result.paid) {
  console.log('endpoint was free — no payment sent');
  console.log(await result.response.json());
} else {
  console.error(`payment failed: ${result.reason}`, result.detail ?? '');
}

Reading the result

PayResult is a discriminated union. Narrow on ok, then on paid, before you touch any payment field:

BranchFields you can readMeaning
ok: true, paid: trueresponse, amountPaid, network, txSignature?The endpoint charged and you paid.
ok: true, paid: falseresponseThe endpoint returned a non-402 directly; no payment was made.
ok: falsereason, detail?An expected failure. Nothing threw.

Two subtleties the types force you to handle:

  • On the paid: true branch, response can still be undefined. That means the payment settled on-chain but the seller did not answer before the deadline. You paid; you have no body. The example checks for it.
  • getPaymentReceipt(response) reads the settled receipt (amountAtomic, assetDecimals, payer, transaction, network) off any paid response. The amount is not always echoed by the facilitator, so treat every receipt field as optional and guard before use.

Capping spend

maxAmountAtomic bounds a single call. '10000' is 0.01 USDC (USDC has 6 decimals, so atomic units are millionths). If the 402 asks for more than the cap, payAndFetch returns { ok: false, reason: 'budget_exceeded' } and no money moves.

For an agent that makes many calls under one total budget, per-request cap, and hourly limit, use createBudgetAccount instead of calling payAndFetch directly. See Budgets and spend caps.

When it fails

Every failure is a typed reason, and two of them decide whether a retry is safe:

  • timeout — no payment was sent. Safe to retry.
  • payment_unconfirmed — the payment authorization went out and could not be confirmed. Do not blind-retry; a retry can pay again.

The full table, and the safe-retry rules, are in Handling errors.

Reliability

payAndFetch accepts a solanaRpcUrl in its options. The Solana signing path reads the mint and a recent blockhash from RPC; a slow or throttled public endpoint shows up as a timeout. Pass your own RPC for anything running unattended:

await payAndFetch(url, { method: 'GET' }, { solana: wallet }, {
  maxAmountAtomic: '10000',
  solanaRpcUrl: process.env.SOLANA_RPC_URL,
});

Base and other EVM chains

The same wallet slot pattern works for EVM. Build the wallet with createEvmKeypairWallet(process.env.EVM_PRIVATE_KEY!) (from a hex private key) and pass it in the evm slot: payAndFetch(url, init, { evm }, opts). You can pass both solana and evm at once and the client settles on whichever network the endpoint's 402 asks for. Fund that wallet with USDC on the target chain the same way. createEvmKeypairWallet is the reason viem is installed.

On this page