Home / Blog / how-agent-budget-enforcement-works

How agent budget enforcement actually works (and why pre-charge matters)

A budget check that runs after the call has already gone through is not a budget. The pre-charge pattern is the difference between a real spending cap and an alert.

Published 2026-05-19 · Spawnpay

The naive pattern

Most attempts at AI agent budgets look like this:

const result = await callLLM(prompt);
spend += result.cost;
if (spend > budget) alert();

The call already went through. The money is already spent. The alert tells you what already happened.

For a one-off run this is fine. For an agent in a loop, it is catastrophic. A bug that causes the agent to call its tool 10,000 times runs at full speed until you notice the alert. By then, the bill exists.

The pre-charge pattern

A real budget enforcement primitive looks like this:

const wrapped = paywall(
  { price: 0.01, vendor: 'SP_x', budget: { daily: 5.00, perCall: 0.50 } },
  callLLM
);

const result = await wrapped(prompt);

What wrapped does, in order:

1. Check the cumulative daily spend in a local rolling window. If the next call would exceed the daily budget, throw BUDGET_DAILY immediately. The upstream call is never made. 2. Check the per-call max. If price exceeds perCall, throw BUDGET_PER_CALL. Same — upstream never invoked. 3. Only if both checks pass: pre-charge the Spawnpay wallet for price. If the wallet is empty or the charge fails, throw INSUFFICIENT_FUNDS. Still upstream never invoked. 4. Now make the upstream call. If it succeeds, the charge stays. If it fails (4xx/5xx from the upstream), refund the charge.

The agent that hits a budget limit gets a structured error it can catch and route around — fall back to a cheaper model, wait an hour, log and exit, whatever. The agent that hits the limit does not run away with your money first.

Where the state lives

Budget state is local to the SDK caller, persisted to ~/.spawnpay/budget-state.json. 24h rolling window, keyed by (vendor, apiKey-prefix). Auto-pruned on every write so the file stays small.

This means:

Warn-at threshold

The third option:

{ daily: 5.00, perCall: 0.50, warnAt: 0.8, onBudgetWarn: ({ used, daily }) => alert() }

Fires onBudgetWarn once per rollover when usage crosses 80% of daily. Useful for in-process logging without spamming a notifier.

When this is not enough

If you have multiple processes / containers sharing a single Spawnpay key, the local SDK state diverges. For that case, the right pattern is one Spawnpay key per process, with each process having its own pre-funded balance ceiling. Topping up the process's balance is the budget primitive.

For multi-tenant SaaS where each end-user has their own budget, the right pattern is one Spawnpay key per end-user. Spawnpay's spk_ keys are cheap (free to mint), and rotating them is a single API call.

What to do next

1. npm install spawnpay-paywall (Node) or pip install spawnpay-paywall (Python) 2. Wrap your upstream call with paywall({ price, vendor, budget: { daily, perCall } }, fn) 3. Verify the pre-charge behavior locally: set daily: 0.01, make 11 calls at $0.001, the 11th throws BUDGET_DAILY.

Full SDK reference: https://github.com/Robocular/spawnpay/tree/main/packages/paywall

60-SEC QUICKSTART → PLAYGROUND → USE CASES → TOP UP USDC →