Cap spend per virtual key
Give a tenant or virtual key a hard monthly USD budget that fails safely — mint the key, shadow the limit, preview the would-refuses, then promote to enforce.
Last updated
On this page
Put a hard monthly USD ceiling on a single tenant (or one virtual key) so that when it burns through its budget, further requests are refused — honestly, with the refusal recorded, and without ever silently blocking on a healthy path.
Prerequisites
- Governance is enabled and
mode: ledgeris set, sobudget_usd(a LedgerSettled limit) can fold the decision log to settle spend. - A decision log is configured (
ledger.decision_log.path) — a hard-enforcedbudget_usdlimit refuses to arm without one. - Your tenancy tree is declared under
governance.tenants(or created via the management API); the node you cap must exist.
1. Mint a virtual key for the tenant
Mint a ttv_-prefixed key bound to the owner node you want to cap. The secret is printed once — only its SHA-256 is stored.
$ tokentriage keys mint \
--owner org:acme/team:ml/user:dev1 \
--label "dev1 laptop" \
--expiry 720h
You should see (illustrative):
kid: vk_7Q3f9m2x
expires: 2026-08-28T14:02:11Z
This secret is shown ONCE and cannot be recovered — copy it now:
ttv_9f3c…c1a0
The caller presents it as Authorization: Bearer ttv_….
2. Attach the budget in shadow first
Add a budget_usd limit attached to that tenant, in mode: shadow. Shadow runs a real evaluation and records a would_refuse on the ledger row, but the engine structurally cannot refuse in shadow — so you gather evidence with zero blast radius.
Attach the limit to the user node (or use attach: { level: key } to template one budget per key):
governance:
enabled: true
limits:
- id: dev1_monthly_budget
attach: { node: org:acme/team:ml/user:dev1 }
type: budget_usd
amount_usd: 200 # dollars-decimal; stored on the nanodollar grid
period: month # day | week | month
posture:
mode: shadow # evaluated, records would_refuse, never refuses
Reload the proxy and let real traffic flow through the key. Every request that would have breached the cap is stamped would_refuse: [dev1_monthly_budget] on its decision-log row.
3. Read the would-refuses with govern preview
Dry-run the proposed limit set over your logged traffic. Preview folds the ledger through the same B.8 accounting the live proxy uses, so the budget_usd numbers are exact and hand-count-verifiable.
$ tokentriage govern preview -c tokentriage.yaml
You should see (illustrative):
governance preview — governance_dry_run
shadow mode (mode: shadow) is the primary pre-enablement instrument; this is the labeled dry-run over ledger history + live counters (§7.5)
ledger: ~/.tokentriage/decisions.jsonl
rows replayed: 4128
EXACT — LedgerSettled recomputation (hand-count-verifiable, exact):
[budget_usd] dev1_monthly_budget @ org:acme/team:ml/user:dev1 burn $214.930000 / limit $200.000000 (frac 1.0747, rows 4128, unpriced_excluded 0) ⟵ WOULD REFUSE NOW
APPROXIMATE — rate-class (label: rate_windows_approximated):
(none)
WOULD BE REFUSED RIGHT NOW (exact over the recorded ledger; the LIVE gate may lag
by its disclosed overrun bound — in-flight/streaming rows not yet folded):
[budget_usd] dev1_monthly_budget @ org:acme/team:ml/user:dev1 burn $214.930000 / limit $200.000000
The ⟵ WOULD REFUSE NOW marker and the WOULD BE REFUSED RIGHT NOW block tell you exactly which tenants the cap would bite today. If the numbers look right, promote it. (Add --json for the machine-readable frozen shape, or --log <path> to preview a specific log.)
4. Promote to enforce — soft, then hard
Climb the ladder one rung at a time. First enforce: soft (mode: on) — it fires the alert thresholds but still never refuses, so you confirm the alerting wiring on live traffic:
posture:
mode: "on"
enforce: soft # alert-only; fires thresholds, never refuses
thresholds: [0.5, 0.8, 0.95]
Once you trust the alerts, flip to enforce: hard with fail: open:
posture:
mode: "on"
enforce: hard # refuses on breach
fail: open # prefer availability; disclose a suspended accountant
thresholds: [0.5, 0.8, 0.95]
Reload. A breaching request now gets a 429 with error.refused_by.limit_type: budget_usd and unit: usd, and the same refusal is mirrored onto exactly one decision-log row (row == wire).
You should see (illustrative), on a breaching request:
HTTP/1.1 429 Too Many Requests
x-tt-refused-by: dev1_monthly_budget
x-tt-refusal-type: budget_exceeded
Why this is safe
Because you shadowed before enforcing and previewed the exact ledger-settled numbers, you never enforce blind: the cap you switch on is the cap you already watched decide.