Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

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.

How-to guideLast 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, with each refusal recorded rather than silently dropped.

Prerequisites

  • Governance is armed — governance.enabled: true, or any entry under governance.limits.
  • A decision log is configured (ledger.decision_log.path, which defaults to ~/.tokentriage/decisions.jsonl and needs no setup), so budget_usd (a LedgerSettled limit) has rows to fold. A hard-enforced budget_usd limit will not arm without one. The trust-ladder mode: is not part of this: the log is written under ledger, shadow and route alike.
  • 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.