View Markdown

Issue a key and give it a budget

On this page

What you get at the end: a key a team can put in their OPENAI_API_KEY slot, that stops working when the money runs out, without you having to watch it.

The examples create an organization budget and a virtual key. Replace the example names and caps with your intended policy.

First, the shape of the thing#

An organisation holds the money; a key spends it. You do not put a budget on a key and hope every key gets one. You put it on the organisation, and every key underneath it draws from the same pot. That is why one team overspending shows up as one bill rather than a hunt through twelve keys.

A key can carry its own smaller cap as well, which is how you hand a contractor a key that cannot spend more than fifty dollars of your organisation's budget. The organisation's cap still applies above it; the tighter of the two wins.

Log in#

This walkthrough uses an admin session: keep its cookie jar and send the CSRF token on mutations. HTTP Basic is also supported for scripts; see admin authentication.

bash
J=$(mktemp)
curl -s -c "$J" -X POST localhost:8080/admin/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"YOUR_ADMIN_PASSWORD"}'

X=$(grep XSRF-TOKEN "$J" | awk '{print $7}')

Every write below sends both: -b "$J" for the session and -H "X-XSRF-TOKEN: $X" for the token. Without the token you get 403 Access denied, which reads like a permissions problem and is not.

Create the organisation#

bash
ORG=$(curl -s -b "$J" -H "X-XSRF-TOKEN: $X" \
  -X POST localhost:8080/admin/organizations \
  -H 'Content-Type: application/json' \
  -d '{"name":"acme"}' | jq -r .id)

Give it a monthly budget#

A budget is a limit rule that counts money over a calendar window. Twenty-five dollars a month, resetting at midnight UTC on the first:

bash
curl -s -b "$J" -H "X-XSRF-TOKEN: $X" \
  -X POST "localhost:8080/admin/organizations/$ORG/rules" \
  -H 'Content-Type: application/json' \
  -d '{"amount":"cost","budget":25,
       "window":{"kind":"calendar","count":1,"unit":"month",
                 "anchor_zone":"UTC","anchor_reset_time":"00:00","anchor_day":1},
       "action":"block"}'

Choose an enforcement action#

block refuses the request when the cap is reached. If you would rather be told than stopped, warn counts and logs without ever refusing, which is the honest way to find out what a team actually spends before you cap them. degrade sends them to a cheaper model instead of refusing, which keeps the service up at lower quality.

Issue the key#

bash
curl -s -b "$J" -H "X-XSRF-TOKEN: $X" \
  -X POST "localhost:8080/admin/organizations/$ORG/keys" \
  -H 'Content-Type: application/json' \
  -d '{"name":"acme-prod","kind":"service"}'

Choose a window#

kind is required and the choice matters. A service key belongs to the organisation and keeps working when the person who created it leaves. A personal key is bounded by its creator's account and stops the moment that account is disabled. Omit it and the request is refused with a message explaining exactly this, which is deliberate: a key that outlives its owner by accident is a security problem, and a key that dies with a departure by surprise is an outage.

The response contains the key once:

json
{ "id": "...", "key": "pointsman_test_77658dca...", "key_prefix": "pointsman_test_77658dca",
  "budget_usd": null, "rate_limit_rpm": null, "scope_id": "..." }

Understand inherited budgets#

budget_usd: null does not mean unlimited. It means this key has no cap of its own, and draws on the organisation's. The prefix is what you will see in logs and on the dashboard; the full key is shown only here, because only a hash of it is stored.

Use it#

bash
curl -s localhost:8080/v1/chat/completions \
  -H "Authorization: Bearer pointsman_test_77658dca..." \
  -H 'Content-Type: application/json' \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'

Check that the budget is really enforced#

Send a request and look at the response headers. If a rule applied to you, the gateway says how much room is left:

text
X-RateLimit-Remaining-Requests: 999

An absent header means no rule of that kind matched, which is not the same as unlimited - it means nothing you configured governs that metric for that key. If you set a budget and see no budget headroom reported, the rule is attached somewhere the request does not pass through, and that is worth fixing before you trust it.

What happens if Redis goes down#

Rate-limit checks can fail open when the counter store is unreachable. Spend-cap admission depends on pointsman.budget.failure-mode:

Mode Enforcing spend cap applies No enforcing spend cap
deny (default) Refuse 503 budget_unverifiable; do not query the ledger Continue without counter-store enforcement
allow (opt-in) Check readable caps against committed ledger spend; skip and count caps the ledger cannot answer Continue

The ledger records the identity chain. Rules counted by authored tree scopes, platform ceilings, models or sessions cannot be reconstructed from that chain; each unanswerable read increments pointsman.budget.oracle.unanswerable. In allow mode, concurrent ledger reads are bounded by POINTSMAN_BUDGET_LEDGER_FALLBACK_MAX_CONCURRENT.

Ledger estimates do not provide atomic reservations, so allow can overspend. Neither mode bypasses a lost-trust fence. See counter-store failure behavior.

What happens if Redis restarts or loses data#

After any Redis restart, failover, reload or data loss, a request governed by a blocking, degrading or throttling cost rule gets a 503 with the code budget_recovering. That happens under both failure modes, until the balance is re-established. A clean restart that lost nothing refuses too, because nothing yet proves what it kept. Rate limits, warn-only budgets and keys with no budget are unaffected.

To reopen a tenant's budgets, review retained counters, journal evidence and provider usage, including any database-backup gap. A ledger total or backup-time counter snapshot alone may be incomplete. Follow the budget-trust recovery procedure before attesting:

bash
curl -u admin:... https://gateway.example/admin/budget/trust
bash
# Replace expected_generation: 1 with the generation from the inventory you reviewed.
curl -u admin:... -H 'Content-Type: application/json' https://gateway.example/admin/budget/trust/<tag>/attest --data '{"expected_generation":1,"evidence_ref":"reviewed spend including backup gap and unresolved attempts","balances":{"{<tag>}:limitrule:<rule>:<bucket>":100000}}'

The first command lists each tenant slot (its tag) with its state. The second opens a new generation where each named counter starts at the balance you give, in micros. Every attestation is recorded in the admin audit log. A counter whose current period began before the attestation and was not named in it stays refused. A new period starts at zero. The first boot after upgrading adopts the counters already in Redis, once, so an upgrade does not refuse anything. See KI-7 for what this phase does not yet do, including automatic recovery.

See multi-instance.md for the full failure posture.

Full wire shapes for every door this guide touches: the admin API reference.

Pointsman v1 candidate documentation Source: 0e09f5e7

Search guides, reference pages and architecture.

Documentation

Open full-size image