Version: Current
Historical release snapshot. These docs are frozen. Read current docs.
Keys and grants
A virtual key is the machine credential of the platform. It is issued once, shown once, stored as a hash, and revoked by deleting one row.
What a key carries
Section titled “What a key carries”| Field | Meaning |
|---|---|
name |
operator-facing label; not unique |
scopes |
chat, embeddings, admin |
models |
allowlist of catalog model ids; empty means every model |
daily_usd |
daily spend cap in USD |
monthly_usd |
monthly spend cap in USD |
rpm |
requests per minute |
tpm |
tokens per minute |
disabled |
pause without revoking |
The model allowlist is checked before the exact-cache lookup, so a cached premium answer is never replayed to a key that may not call that model. Rate and spend windows live in memory; a restart resets the current minute, day and month windows.
Secret handling
Section titled “Secret handling”- Only a SHA-256 hash is stored in the
virtual_keystable. - The plaintext is returned once by
POST /v1/router/keysor by the consumer create flow, and never again. - Lists show ids, limits and spend. They never show a key or a hash.
- A key id (
key-…) is not a secret. It is safe to record in a runbook; thevk_…value is not.
Never paste a key value into chat, a ticket, a commit or a documentation page. Reference the file that holds it. Key material lives in the service’s environment file or in the consumer’s own browser session, nowhere else.
Create a key as an operator
Section titled “Create a key as an operator”Over loopback with an admin key:
curl -s http://127.0.0.1:4020/v1/router/keys \ -H "Authorization: Bearer $ACUMEN_ADMIN_KEY" \ -H 'Content-Type: application/json' \ -d '{"name":"ci","scopes":["chat"],"models":["cheap"], "daily_usd":5,"monthly_usd":50,"rpm":60,"tpm":200000}'The response contains the plaintext once. Capture it into the consumer’s secret store immediately; do not echo it into a shell log.
Updating is partial: send only the fields that change.
curl -s -X PATCH http://127.0.0.1:4020/v1/router/keys \ -H "Authorization: Bearer $ACUMEN_ADMIN_KEY" \ -H 'Content-Type: application/json' \ -d '{"id":"key-…","daily_usd":10}'Revoking is DELETE /v1/router/keys?id=key-…. The next request with that key is
401 invalid virtual key with no restart.
Create a key as a person
Section titled “Create a key as a person”A signed-in person creates a key at /app/grants/new. That handler writes both
rows (the api grant and its virtual_keys row) and deletes the key if the
grant write fails, so the two cannot drift.
The ledger invariant
Section titled “The ledger invariant”Every credential that can act for an account appears in grants:
kind=api: a virtual key, with the same id as itsvirtual_keysrow.kind=session: a browser sign-in, idgrt_plus the session id.kind=delegation: reserved; no handler writes it yet.
Grants carry label, scopes, models, caps, origin, created_by, state
and timestamps. State is active, disabled, revoked, or the computed
expired. grant_events is the append-only history shown on the grant page.
Which scope reaches what
Section titled “Which scope reaches what”| Scope | Doors |
|---|---|
chat |
/v1/chat/completions, /v1/messages, /v1/responses, /v1/search, /v1/models |
embeddings |
/v1/embeddings |
admin |
/health, /readyz, /console*, /metrics, /debug/pprof/*, /v1/router/*, key CRUD |
A chat key that reaches an operator path gets 403 with
X-Acumen-Denied-By: scope.
Failure modes a caller will see
Section titled “Failure modes a caller will see”| Condition | Status | Header |
|---|---|---|
| missing or unknown key | 401 | X-Acumen-Denied-By: key |
| scope not held | 403 | scope |
| model not on the allowlist | 403 | model |
| RPM exceeded | 429 with Retry-After |
rate |
| TPM exceeded | 429, retry after the minute rolls | rate |
| daily or monthly cap crossed | 402 | budget |