Define metered resources, attach quota and rate-limit policies to subjects, and atomically check-and-consume usage — with reservations, an append-only ledger, rollup reporting and threshold alerts.
Coding agents: fetch
https://tokengate.rodmena.co.uk/llms.txt
for the complete integration contract in plain text — auth, request shapes,
the reserve/commit flow, and every error code — then work from
/openapi.json for exact schemas.
The same guide is served over MCP at https://tokengate.rodmena.co.uk/mcp/
(streamable HTTP), and the Python SDK is pip install tokengate.
One call evaluates every applicable policy for a subject. All items commit or none do — no partial consumption.
strict counts inside a PostgreSQL transaction (money-like
budgets); fast counts in Redis with an exactly-once ledger
write-behind.
Reserve an estimate before an expensive call, commit the actual after. Uncommitted holds count against quota and expire automatically.
Append-only usage ledger and admin audit log, rollup reporting, and HMAC-signed threshold-alert webhooks.
Every request carries a tenant-scoped API key as a bearer token. Keys are stored only as a SHA-256 hash and shown exactly once, at mint time.
Authorization: Bearer tg_<key_id>_<secret>
Keys carry capability scopes: consume (data plane),
catalog:*, assignments:*, overrides:*,
alerts:*, webhooks:*, keys:*,
reporting:read, tenant:read, ops:jobs.
admin:read and admin:write are supersets — and
admin:write is a tenant superuser: it can mint and
revoke keys and rewrite every policy in the tenant. Ask for the narrowest scope
that covers your integration; the full list is in
the agent guide. Ask your TokenGate operator for a key —
this page cannot mint one.
curl -X POST https://tokengate.rodmena.co.uk/v1/consume \
-H "Authorization: Bearer $TOKENGATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "user_42",
"items": [{"resource": "llm.tokens", "amount": 1200}],
"idempotency_key": "req-8f2c1a"
}'
A success returns 200 with the post-state of every policy that
applied:
{
"allowed": true,
"event_id": "01KYB...",
"degraded": false,
"results": [
{
"resource": "llm.tokens", "policy_name": "monthly-budget",
"kind": "quota", "mode": "strict", "allowed": true,
"limit": 100000, "used": 1200, "remaining": 98800,
"window": {"id": "mo1:1780...", "reset_at": "2026-08-01T00:00:00+00:00"}
}
]
}
pip install tokengate
The official typed client — sync and async, automatic idempotency keys, typed exceptions for every denial, and a reserve/commit metering context manager. On PyPI, Apache-2.0, Python 3.11+:
from tokengate import TokenGate, QuotaExceeded
tg = TokenGate("https://tokengate.rodmena.co.uk", api_key="tg_...")
tg.consume("user_42", {"llm.tokens": 1200})
with tg.meter("user_42", {"llm.tokens": 4000}) as m: # reserve → commit
m.record("llm.tokens", 3271)
A success status always means permission — if a 2xx ever said otherwise the
client raises ProtocolViolation rather than returning. Plain HTTP
works everywhere the SDK does; nothing about the protocol is SDK-specific.
When the true cost is only known afterwards, hold an estimate first. The hold counts against quota until you commit the actual, release it, or its TTL expires.
# 1. hold an estimate
POST /v1/reserve {"subject": "user_42", "ttl_seconds": 120,
"items": [{"resource": "llm.tokens", "amount": 4000}]}
# -> {"reservation_id":"01KYB...","status":"held", ...}
# 2a. settle with the real amount | 2b. or give the hold back
POST /v1/reservations/{id}/commit {"actuals":[{"resource":"llm.tokens","amount":3271}]}
POST /v1/reservations/{id}/release {}
| Method & path | Scope | Purpose |
|---|---|---|
POST /v1/consume | consume | Atomic check-and-consume |
POST /v1/check |
consume | Dry run — always 200; read allowed |
POST /v1/refund | consume | Return usage to the quota |
POST /v1/reserve | consume | Hold an estimate |
POST /v1/reservations/{id}/commit |
consume | Settle a hold with actuals |
POST /v1/reservations/{id}/release |
consume | Drop a hold |
GET /v1/subjects/{subject}/usage |
consume | Current usage per policy |
GET /v1/subjects/{subject}/entitlements |
consume | Policies in effect |
POST /v1/resources, /v1/policies, /v1/plans |
catalog:write | Define the catalog |
PUT /v1/subjects/{subject}/assignment |
assignments:write | Attach a plan to a subject |
GET /v1/ledger |
reporting:read | Append-only usage, paged {entries, next_cursor} |
GET /v1/usage/summary |
reporting:read | Rollup aggregates — lags recent activity |
GET /v1/audit |
reporting:read | Admin audit trail, paged {entries, next_cursor} |
The full set, with schemas, is in the interactive docs.
Every failure is application/problem+json (RFC 7807) with a
stable code. Retryable responses carry Retry-After.
| Status | Code | Meaning |
|---|---|---|
| 429 | quota_exceeded |
A quota would be exceeded. Honor Retry-After;
reset_at says when the window rolls. |
| 429 | rate_limited |
A token-bucket rate limit is empty. Retry after the header value. |
| 409 | idempotency_key_reuse |
Same key, different payload. Use a fresh key. |
| 409 | idempotency_in_flight |
An identical request is mid-flight. Retry shortly. |
| 422 | unknown_resource |
The resource is not defined for this tenant. |
| 503 | backend_unavailable |
A backend is down and policy says deny. Retry; never treat as "allowed". |
Always send an idempotency key on consume,
reserve, commit, and refund. A retry with
the same key returns the original outcome instead of consuming twice; the
response carries X-TokenGate-Replayed: true.
A batch is all-or-nothing. If any item's policy would be exceeded, nothing is consumed and the response names the most-constraining policy.
Failures never oversubscribe. Under a partial outage
TokenGate may deny early or serve flagged degraded: true per
policy configuration, but it will not silently let usage through uncounted.
/v1/check never returns 429. It reports a
denial as 200 with "allowed": false and a
blocking object — branch on the field, not the status. Only the
mutating endpoints answer 429.
Read balances from live sources.
/v1/subjects/{subject}/usage and /v1/ledger are
current; /v1/usage/summary is built by a periodic rollup job and
returns nothing for activity that just happened.
Rate limits report no used. A
rate_limit policy returns "used": null with
remaining tokens left — only quota policies carry a cumulative
total.