Skip to content

Total budget (cross-token)

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.

The built-in labels cover every stablecoin PipRail ships:

DenominationTokens
USDUSDC · USDT · USD1 · FDUSD · U · RLUSD
EUREURC

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' },
}
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.

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 settle
payer.budget().byDenom[0] // → { denom: 'USD', spentFormatted: '…', remainingFormatted: '…', … }
policy: { maxTotalPerDenom: { USD: '20.00', EUR: '5.00' } }
// Two independent grand totals — USDC/USDT roll into USD, EURC into EUR.

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.

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, … }

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.

Terminal window
PIPRAIL_MAX_TOTAL_DENOM="USD:20.00,EUR:5.00" # cross-token grand total
PIPRAIL_MAX_PAYMENTS=100 # lifetime payment count
PIPRAIL_MAX_PAYMENTS_PER_WINDOW=10 # + PIPRAIL_WINDOW_SECONDS
PIPRAIL_WINDOW_SECONDS=3600

In multi-chain mode (PIPRAIL_CHAINS) the clients share one ledger automatically, so the grand total spans every funded chain. See MCP configuration.