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.mjsfiles or set"type": "module"in yourpackage.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
@dexterai/x402is the client.@solana/web3.jsgenerates the keypair;bs58encodes its secret key.@solana/spl-tokenis used only by the balance check in step 2.viemis 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 withoutvieminstalled throwsCannot 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.
Run it once and put the secret in your environment, not in source control:
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.
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.
Reading the result
PayResult is a discriminated union. Narrow on ok, then on paid, before you touch any payment field:
| Branch | Fields you can read | Meaning |
|---|---|---|
ok: true, paid: true | response, amountPaid, network, txSignature? | The endpoint charged and you paid. |
ok: true, paid: false | response | The endpoint returned a non-402 directly; no payment was made. |
ok: false | reason, detail? | An expected failure. Nothing threw. |
Two subtleties the types force you to handle:
- On the
paid: truebranch,responsecan still beundefined. 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:
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.