This walkthrough takes you from a clean checkout to seeing your Claude Code
seat's tokens flowing through tokenctl top, attributed to the right leaf of
your org → team → dev tree.
Target time: under 10 minutes, no Docker, no cluster, no message queue.
- Go 1.24+
- A Claude or OpenAI API key you can point at a proxy (read-only is fine — the proxy just forwards)
- Linux or macOS (Windows binaries are out of scope for v0.1)
git clone https://github.com/SuperMarioYL/tokenctl
cd tokenctl
go build -o tokenctl ./cmd/tokenctl
./tokenctl versionYou should see something like:
tokenctl 0.1.0-dev (commit unknown, linux/amd64, go1.24.0)
./tokenctl init --org acme -c ./tokenctl.yamlThis writes a 3-team, 6-dev tree with placeholder API keys. Inspect it:
less ./tokenctl.yamlYou will see the wallet block, two upstream providers (claude + openai), the
recursive tree: block, and api_keys: mapping placeholder keys to leaves.
Open tokenctl.yaml, find the api_keys: block, and replace
replace-me-alice with the real Bearer token you want to govern — typically
the ANTHROPIC_API_KEY your Claude Code seat is configured with, or a
synthetic per-dev token you mint upstream and then bind here.
Static keys + mTLS is the only auth model in v0.1. SSO / SCIM / per-user identity is on the hosted-tier roadmap.
./tokenctl up -c ./tokenctl.yamlYou should see:
tokenctl 0.1.0-dev — proxy on :8080, metrics on :9090/metrics, store ./tokenctl.db
export the following so your clients route through the proxy:
export ANTHROPIC_BASE_URL=http://localhost:8080
export OPENAI_BASE_URL=http://localhost:8080/v1
ctrl-c to stop.
Leave this terminal running.
In a second terminal:
export ANTHROPIC_BASE_URL=http://localhost:8080
export ANTHROPIC_AUTH_TOKEN=<the-key-you-bound-in-tokenctl.yaml>
# Claude Code, or any client that respects ANTHROPIC_BASE_URL:
claude "write a short poem about cgroups"In a third terminal:
./tokenctl top -c ./tokenctl.yamlYou will see a live table:
tokenctl top 2026-06-05T03:42:11Z in-flight=1 throttles=0 denies=0
wallet: [██······························] (4.2k / 20.00M = 0%)
────────────────────────────────────────────────────────────────────────────────
GROUP WEIGHT USAGE BUDGET STATE
acme 100 4.2k 20.00M ok (0%)
acme.team-platform 50 4.2k 10.00M ok (0%)
acme.team-platform.alice 50 4.2k 5.00M ok (0%)
acme.team-platform.bob 50 0 5.00M ok (0%)
...
The usage column ticks up in real time as the streamed SSE response from
Claude reaches the proxy. Each token is attributed to the leaf bound to the
Bearer token, and every ancestor's consumed counter is updated too.
Lower a leaf's budget to something you'll burn in a single call:
- name: alice
weight: 50
budget: { tokens: 5000, window: 1h, soft_throttle_at: 0.8 }Restart tokenctl up, send a long request, and watch the leaf flip from
ok → throttle (8x%) → DENY (10x%). Requests above 100% return
HTTP 429 with two response headers:
X-TokenCtl-Reason: budget_exceeded
X-TokenCtl-Group: acme.team-platform.alice
The proxy logs the deny to BoltDB's audit bucket and to Prometheus
(tokenctl_denies_total).
http://localhost:9090/metrics exposes per-group counters:
tokenctl_consumed_tokens{group="acme.team-platform.alice"}tokenctl_budget_tokens{group="..."}tokenctl_in_flight{group="..."}tokenctl_denies_total{reason="budget_exceeded",group="..."}tokenctl_throttles_total{reason="soft_throttle",group="..."}
Wire it into your existing Grafana — there's nothing new to learn.
Ctrl-C on the tokenctl up terminal. The arbiter goroutine drains, the
counter flusher commits one last time, BoltDB is closed. The audit log in
tokenctl.db persists across restarts — feed it into your SIEM if needed.
- Read README.md for the cgroups analogy and the architecture diagram.
- Review
configs/tokenctl.example.yamlfor every knob, including thewallet:shared cap across Claude + OpenAI + Bedrock on a single bill. - For commercial use (multi-region HA, SSO, 90-day audit retention, hosted control plane), see the 付费 / Pricing section of the README.