Cap spend per virtual key
Give a tenant a hard monthly USD budget — mint the key, shadow the limit, preview the would-refuses, then enforce.
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, with each refusal recorded rather than silently dropped.
Prerequisites
- Governance is armed —
governance.enabled: true, or any entry undergovernance.limits. - A decision log is configured (
ledger.decision_log.path, which defaults to~/.tokentriage/decisions.jsonland needs no setup), sobudget_usd(a LedgerSettled limit) has rows to fold. A hard-enforcedbudget_usdlimit will not arm without one. The trust-laddermode:is not part of this: the log is written underledger,shadowandroutealike. - 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; what is stored is a SHA-256 hash of it plus a four-character stub for matching.
$ tokentriage keys mint \
--owner org:acme/team:ml/user:dev1 \
--label "dev1 laptop" \
--expiry 720h
You should see:
kid: vk_FEKZdMs2UQRi
expires: 2026-11-07T20:41:14Z
This secret is shown ONCE and cannot be recovered — copy it now:
ttv_EeNpxgdtQYWP…
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 full evaluation and records a would_refuse on the ledger row, and the engine cannot turn a request away while in shadow, so you gather evidence with no blast radius.
Attach the limit to the user node. Pin it by name rather than reaching for attach: { level: key }: that template has no key-level nodes to materialize against in this build, so it resolves to nothing and enforces nothing.
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, stops nothing
Reload the proxy and let live 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 accounting the live proxy uses, so the budget_usd numbers are recomputable by hand.
$ tokentriage govern preview -c tokentriage.yaml
The run below is from a sandbox whose upstream is a stub, so the dollar figures are cents of a cent and the cap was set low enough to trip on a handful of requests. It is cut back to the two parts this step turns on, with # … where lines were dropped; turn governance on prints a report of the same command in full. The shape is what to read:
# …
EXACT — LedgerSettled recomputation (hand-count-verifiable, exact):
[budget_usd] dev1_monthly_budget @ org:acme/team:ml/user:dev1 burn $0.000038 / limit $0.000020 (frac 1.8900, rows 3, unpriced_excluded 0) ⟵ WOULD REFUSE NOW
# …
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 $0.000038 / limit $0.000020
The ⟵ WOULD REFUSE NOW marker and the WOULD BE REFUSED RIGHT NOW block name the tenants the cap would bite today, and frac is burn over limit. Preview folds the ledger through the same accounting the live proxy uses, so a budget_usd figure here is recomputable by hand from the rows. If the numbers look right, promote it. Add --json for the machine-readable shape, or --log <path> to preview a specific log. The -c flag is required despite advertising a default, and the command errors rather than printing an empty report if the log does not exist yet.
4. Promote to enforce — soft, then hard
Climb the ladder one rung at a time. First enforce: soft (mode: on), where the limit is evaluated in full and a breach lets the request through anyway:
posture:
mode: "on"
enforce: soft # a breach lets the request through
thresholds: [0.5, 0.8, 0.95]
The thresholds fire on fraction crossings from the accountant, whatever the mode, so this is the rung where you confirm the alerting wiring is reaching you without risking traffic. Note that a soft breach writes nothing to the decision row: would_refuse is recorded by mode: shadow only. If you want the breach itself to alert, put 1 in thresholds.
Once you trust the alerts, flip to enforce: hard with fail: open:
posture:
mode: "on"
enforce: hard # stops the request 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 carrying error.refused_by.limit_type: budget_usd and unit: usd, and one function stamps that same value onto the response and onto a single decision-log row, so the two agree.
It comes back like this, from the same sandbox:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Ratelimit: "org_monthly_budget";r=0;t=1912510
Retry-After: 1912510
X-Tt-Refusal-Type: budget_exceeded
X-Tt-Refused-By: org_monthly_budget
X-Tt-Request-Id: tt_i3f2naenug5aanctu6pcjrofcy
Retry-After is the seconds until the calendar period rolls, which for a monthly budget stopped early in the month is most of a month. Read the body for observed against limit, and expect observed to sit above the cap: a budget_usd counter is folded from ledger rows after the fact, so it discovers the breach slightly late and says so. How a limit counts has the mechanism.
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.