Dexter
Dexter
Docs
Pay for APIs

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

Budgets & spend caps

By the end of this page you will have a budget account: a payment-aware fetch that stops an agent from spending past a total limit, a per-request cap, an hourly cap, and a domain allowlist you set.

What a budget account is

createBudgetAccount wraps the x402 client with spending controls. It returns an object whose fetch behaves like normal fetch, except it pays x402 challenges automatically and refuses to pay past your limits. Every payment is recorded in an in-memory ledger you can read back.

The controls are:

ControlConfig fieldEnforces
Total budgetbudget.totalCumulative spend across the account's whole life
Per-request capbudget.perRequestThe most a single request may pay (optional)
Hourly capbudget.perHourThe most spent in any rolling hour (optional)
Domain allowlistallowedDomainsOnly these hosts may be paid; if omitted, any host may be paid

Amounts are USD strings, for example '50.00'.

Working example

agent.ts
import { createBudgetAccount } from '@dexterai/x402/client';
 
const agent = createBudgetAccount({
  walletPrivateKey: process.env.SOLANA_PRIVATE_KEY!,
  budget: {
    total: '50.00',
    perRequest: '1.00',
    perHour: '10.00',
  },
  allowedDomains: ['api.example.com'],
});
 
const res = await agent.fetch('https://api.example.com/data');
const data = await res.json();
 
console.log(agent.spent);       // '$0.05'
console.log(agent.remaining);   // '$49.95'
console.log(agent.payments);    // 1

walletPrivateKey is a Solana key — a base58 string or a 64-byte JSON array. For Base/EVM payments, pass evmPrivateKey instead (a hex key; requires viem installed). The config also inherits the wrapFetch options — facilitatorUrl, preferredNetwork, rpcUrls, maxAmountAtomic, accessPass — so a budget account is a superset of a plain payment fetch.

The wallet must already hold funds. See Pay per call — Node agent for funding a keypair wallet.

Reading spend back

The account exposes live counters:

PropertyTypeWhat it holds
spentstringTotal spent, formatted ('$12.34')
remainingstringBudget left, formatted ('$37.66')
paymentsnumberCount of payments made
spentAmountnumberTotal spent as a raw number
remainingAmountnumberBudget left as a raw number
hourlySpendnumberSpent in the last rolling hour
ledgerreadonly PaymentRecord[]Every payment: amount, domain, network, timestamp
reset()() => voidClears all spend history and counters
for (const entry of agent.ledger) {
  console.log(entry.amount, entry.domain, entry.network, entry.timestamp);
}

What happens at a cap

When a request would cross a cap, the wrapped fetch throws before any payment is signed — it does not silently under-deliver or partially pay. The thrown value is an X402Error with a code you can branch on:

  • amount_exceeds_max — the total, per-request, or hourly cap would be crossed.
  • payment_rejected — the request host is not in allowedDomains.
import { createBudgetAccount, X402Error } from '@dexterai/x402/client';
 
const agent = createBudgetAccount({
  walletPrivateKey: process.env.SOLANA_PRIVATE_KEY!,
  budget: { total: '5.00', perRequest: '0.50' },
  allowedDomains: ['api.example.com'],
});
 
try {
  const res = await agent.fetch('https://api.example.com/data');
  await res.json();
} catch (err) {
  if (err instanceof X402Error && err.code === 'amount_exceeds_max') {
    console.error('cap reached:', err.message, '| spent so far', agent.spent);
  } else if (err instanceof X402Error && err.code === 'payment_rejected') {
    console.error('domain blocked:', err.message);
  } else {
    throw err;
  }
}

The ledger is in-memory and per-process. It counts only payments this account instance made; it is not shared across processes and does not survive a restart or reset(). A budget account bounds spend within one running agent, not across a fleet.

On this page