Docs/Use casesv0.0.1-preview

Spend controls and agent budgets

Budgets, allowances, and rate limits as ledger state — enforced atomically at write time, replenished by TTL, safe for humans and AI agents alike.

On this page
  1. The budget account
  2. A ledger-wide transaction cap
  3. Pre-authorized spends
  4. Replenishing windows
  5. Related

A spending limit enforced in application code is a suggestion — every retry, race, and forgotten code path is a way around it. Enforced as ledger state, it’s arithmetic: the budget is an account, each spend debits it, and the engine’s limit check is atomic with the write. This pattern has become newly urgent as AI agents get spending authority: an agent’s budget needs exactly what a ledger already does — hard caps, deduplicated retries, and a complete audit trail.

The budget account

Fund a budget account per user, team, card, or agent; floor it at zero. Every spend is a transfer out of it — and the two properties that make the cap real are enforced by the engine:

B = c.create_ledger(LedgerInstance(ledger_id=0, currency=840)).ledger_id
TREASURY, AGENT_BUDGET, VENDOR = 9000, 9001, 9002   # integer account ids
FINANCE, AGENT = 1, 7                               # source ids
MONTHLY_CAP = 50_000                                # this month's allowance, minor units

c.create_account(AccountCreate(id=TREASURY, ledger_id=B, currency=840,
                               limit_low=-10**15, limit_high=10**15))
c.create_account(AccountCreate(id=AGENT_BUDGET, ledger_id=B, currency=840,
                               limit_low=0, limit_high=MONTHLY_CAP))
c.create_account(AccountCreate(id=VENDOR, ledger_id=B, currency=840,
                               limit_low=0, limit_high=10**15))

# fund the month's budget from treasury
c.transfer(TransferCreate(source_id=FINANCE, ledger_id=B, currency=840,
        debit_account_id=TREASURY, credit_account_id=AGENT_BUDGET,
        amount=MONTHLY_CAP, txn_id="refill-2026-09"))

event_id = "evt-0001"                               # the event that triggered the spend
c.transfer(TransferCreate(source_id=AGENT, ledger_id=B, currency=840,
        debit_account_id=AGENT_BUDGET, credit_account_id=VENDOR,
        amount=1_900, txn_id=f"call-{event_id}"))
  • Overspend is a decline, not a race. A spend that would take the budget below zero fails with ERR_LIMIT atomically — two concurrent spends cannot both pass a check and jointly overdraw, because admission is serialized per account.
  • A retried spend is one spend. Key txn_id to the triggering event and redelivery returns OK_DUPLICATE — an agent retrying a timed-out purchase cannot pay twice.

Every attempt — including the declines — is a durable, queryable record: “what did this agent try to spend, and what was refused” is a history query, which is precisely the audit trail agent operations lack.

A ledger-wide transaction cap

The budget above bounds cumulative spend per account. A different control bounds any single transaction: the ledger’s max_txn_amount property. Set it at ledger create or change it at runtime, and every transfer, hold, or tiered leg above the cap is declined durably with ERR_AMOUNT_EXCEEDS_MAX (42) and the engine’s own reason code 0xFFFF002A — a hard structural limit no caller can exceed, useful as the outermost guardrail under per-account budgets (“no agent moves more than $10,000 in one transaction, ever”). Raising the cap admits new identities; already-declined ones replay their original decline.

Pre-authorized spends

For a spend with a confirmation step (agent proposes, human approves; or purchase pending fulfillment), hold the amount instead of posting it. Held funds count against the cap immediately — with 600 posted and 500 held, a further 200 spend is declined — and a reservation TTL auto-expires proposals nobody approves.

Replenishing windows

Two engine mechanics compose into renewable allowances:

  • Top-up on a schedule: a periodic transfer from treasury refills the budget; idempotency (txn_id="refill-2026-09") makes the cron safely re-runnable.
  • Token bucket via TTL holds: charge each action as a hold against a capacity account with a TTL equal to your window. Capacity is consumed while holds are live and returns automatically as the engine expires them — a rate limit with no sweeper of your own. Expiry resolves at touch and on the reaper’s pass (reserve_expiry_interval_ns, default 100 ms), so capacity returns shortly after each deadline rather than at the exact instant; it’s a throughput governor, not a millisecond-precise limiter.

Wallets · Escrow and expiring holds · Accounts and limits · Idempotency