Total budget (cross-token)
The problem: per-token caps multiply
Section titled “The problem: per-token caps multiply”maxTotal is a lifetime cap per (network, asset) pair.
That is exact and oracle-free, but it has a sharp edge once a buyer holds several tokens on several
chains: the same cap applies independently to each pair.
// ⚠️ This does NOT mean "$20 total".policy: { maxTotal: '20.00', tokens: ['USDC', 'USDT'] }// across base + solana + xrpl → up to $20 for base-USDC, ANOTHER $20 for base-USDT,// ANOTHER $20 for solana-USDC … = up to $20 × every (chain × token) pair.When you mean “spend at most $20, across everything”, reach for maxTotalPerDenom.
maxTotalPerDenom — one number across everything
Section titled “maxTotalPerDenom — one number across everything”A denomination is a unit of account you cap as a whole: e.g. USD across every USD
stablecoin, on every chain.
import { PipRailClient } from '@piprail/sdk'
const client = new PipRailClient({ chain: 'base', wallet: { key: process.env.AGENT_KEY! }, policy: { maxTotalPerDenom: { USD: '20.00' } }, // $20 total — full stop})The SDK sums the human value of every token of that denomination — USDC, USDT, and any other
USD stablecoin — and refuses the payment that would push the running total past the cap, with a
typed PaymentDeclinedError (reasonCode: 'BUDGET',
policyCode: 'MAX_TOTAL_DENOM'). It coexists with the per-asset maxTotal; the stricter cap wins.
Which tokens count as which unit
Section titled “Which tokens count as which unit”The built-in labels cover every stablecoin PipRail ships:
| Denomination | Tokens |
|---|---|
USD | USDC · USDT · USD1 · FDUSD · U · RLUSD |
EUR | EURC |
Extend or override them with denomFor (your assertion of equivalence — by symbol or asset id):
policy: { maxTotalPerDenom: { USD: '20.00' }, denomFor: { PYUSD: 'USD', '0xMyCustomStable…': 'USD' },}Staged examples
Section titled “Staged examples”① One chain, one token
Section titled “① One chain, one token”const client = new PipRailClient({ chain: 'base', wallet, policy: { maxTotalPerDenom: { USD: '5.00' } },})// USDC payments stop once $5 total has been spent.② One chain, many stablecoins (the cross-token sum)
Section titled “② One chain, many stablecoins (the cross-token sum)”const client = new PipRailClient({ chain: 'base', wallet, policy: { maxTotalPerDenom: { USD: '5.00' } },})// 0.05 USDC + 0.05 USDT = $0.10 toward the SAME $5 cap. They are not tracked separately.③ Many chains, ONE budget
Section titled “③ Many chains, ONE budget”Use MultiChainPayer.fromWallets: it gives every chain’s client a
shared ledger, so the grand total spans all of them.
import { MultiChainPayer } from '@piprail/sdk'
const payer = MultiChainPayer.fromWallets({ wallets: { base: { key: process.env.EVM_KEY! }, solana: { key: process.env.SOLANA_SECRET! }, xrpl: { key: process.env.XRPL_SEED! }, }, policy: { maxTotalPerDenom: { USD: '20.00' } }, // $20 across base + solana + xrpl, combined})
await payer.get('https://api.example.com/paid') // pays the first funded chain that can settlepayer.budget().byDenom[0] // → { denom: 'USD', spentFormatted: '…', remainingFormatted: '…', … }④ USD and EUR side by side
Section titled “④ USD and EUR side by side”policy: { maxTotalPerDenom: { USD: '20.00', EUR: '5.00' } }// Two independent grand totals — USDC/USDT roll into USD, EURC into EUR.⑤ Cap the number of payments
Section titled “⑤ Cap the number of payments”Counts need no oracle, so they span every chain and token — including native coins:
policy: { maxPayments: 100, // lifetime cap on the NUMBER of payments maxPaymentsPerWindow: 10, // …and at most 10 within the rolling window windowSeconds: 3600, // the window width (shared with windowTotal)}A breach is reasonCode: 'BUDGET' for maxPayments, reasonCode: 'OUTSIDE_WINDOW' for
maxPaymentsPerWindow.
Reading the grand total
Section titled “Reading the grand total”client.budget() exposes the leash. The denomination rows are
present from the start (before any spend) because the cap is a single declared number, so an
agent can preview its headroom up front:
const b = client.budget()b.byDenom // [{ denom: 'USD', spentFormatted: '0', capFormatted: '20', remainingFormatted: '20', fraction: 0 }]b.counts // { settled: 0, lifetimeCap: 100, lifetimeRemaining: 100, … }Survive a restart
Section titled “Survive a restart”By default these totals are in-memory (the session is the process). Add a durable spend store and the grand total + counts resume after a crash or redeploy.
In the MCP server
Section titled “In the MCP server”PIPRAIL_MAX_TOTAL_DENOM="USD:20.00,EUR:5.00" # cross-token grand totalPIPRAIL_MAX_PAYMENTS=100 # lifetime payment countPIPRAIL_MAX_PAYMENTS_PER_WINDOW=10 # + PIPRAIL_WINDOW_SECONDSPIPRAIL_WINDOW_SECONDS=3600In multi-chain mode (PIPRAIL_CHAINS) the clients share one ledger automatically, so the grand
total spans every funded chain. See MCP configuration.