Cap Claude Code API Spend with a Local Budget Gateway
Claude Code on a metered Anthropic API key can retry a failed tool call, re-send the same prompt in a loop, or grind through a task far longer than you expected. Anthropic's billing alerts fire hours late, so the first time you notice is on the invoice. This guide puts a small local gateway — Stoke — between Claude Code and the Anthropic API so requests over your cap are refused before Anthropic is contacted, and repeated identical prompts are stopped by a loop breaker.
How this works
Stoke is a single Rust binary (~5.5 MB) that listens on 127.0.0.1:8787 and speaks the Anthropic Messages API on POST /v1/messages. You point ANTHROPIC_BASE_URL at it. On every request Stoke runs its enforcement — auth, budget cap, rate limit, loop breaker — and only then forwards the request to Anthropic. Nothing that trips a check ever reaches Anthropic, so it never costs a token.
Be clear about the scope: this forwards to Anthropic. It does not run Claude Code on local models. Stoke is a policy-enforcing passthrough, not a translator — your prompts still go to Claude, you still pay Anthropic's per-token price, and the gateway's job is to refuse the calls you don't want to pay for.
Install Stoke
curl -sSf https://stokegate.com/install | sh
This pulls a checksum-verified static binary for macOS (arm64/x64) or Linux (x64/arm64) and installs two binaries: stoke (the server) and stoke-cli.
Configure the gateway
Create stoke.toml. You need one [[providers]] block of type = "anthropic" and one [[keys]] block that sets the dollar cap:
[server]
host = "127.0.0.1"
port = 8787
[[providers]]
name = "anthropic"
type = "anthropic"
base_url = "https://api.anthropic.com"
api_key_env = "ANTHROPIC_API_KEY"
tier = "cloud"
[[keys]]
key = "cc-cap-key"
budget_usd = 20.0
rate_limit_rpm = 60
# Stoke ships no prices and guesses none. Declare what the model costs.
# Copy the numbers from Anthropic's pricing page for the model you use.
[pricing.models."claude-haiku-4"]
input_per_1m = 0.80
output_per_1m = 4.00
Two separate keys are in play, and keeping them straight is the whole trick:
- The gateway key (
cc-cap-keyabove) is a string you invent. Claude Code sends it to Stoke, and Stoke meters it — the budget and rate limit apply to this key. - The real Anthropic key (
sk-ant-...) is read server-side from theANTHROPIC_API_KEYenvironment variable viaapi_key_env. Stoke attaches it asx-api-keyon the way out to Anthropic. It never lives in Claude Code's config.
When the metered spend for cc-cap-key reaches budget_usd, the next request is refused with HTTP 429 and a body like Budget exceeded: $20.0130/$20.0000 for key cc-cap-k (Stoke identifies the key by its first eight characters). rate_limit_rpm caps requests per rolling 60-second window.
The [pricing] block is not optional bookkeeping — it is what makes the cap real. A dollar cap can only stop spend the gateway can measure, so Stoke refuses to serve a model on a metered provider until you tell it what that model costs:
HTTP 403
no price configured for model 'claude-opus-4-8' on a 'cloud' provider, so its
spend cannot be metered and `budget_usd` could not enforce a cap.
That refusal is deliberate. The alternative — quietly metering an unrecognised model at $0 — leaves you with a budget cap that never trips and a dashboard that says you spent nothing. Declare the price, or set [pricing] unpriced = "free" if you knowingly want that model served without a dollar ceiling. Local and LAN providers (tier = "local" or "remote") need no prices: their compute is not billed per token. Check what Stoke knows with stoke-cli pricing.
Start Stoke
Stoke is fail-closed: with no STOKE_API_KEYS set (and no STOKE_DEV=1), every request is rejected with 401. The gateway key from stoke.toml must also appear in STOKE_API_KEYS. From the directory containing stoke.toml:
export ANTHROPIC_API_KEY="sk-ant-...your real key..."
export STOKE_API_KEYS="cc-cap-key"
stoke-cli serve
The real key lives only in this process's environment. Anyone hitting the gateway needs the gateway key, not the Anthropic key.
Point Claude Code at Stoke
Claude Code authenticates to a base URL with a Bearer token when you set ANTHROPIC_AUTH_TOKEN (it sends Authorization: Bearer <token>). Stoke authenticates callers by exactly that header — so use ANTHROPIC_AUTH_TOKEN, not ANTHROPIC_API_KEY (which Claude Code would send as x-api-key, a header Stoke's auth ignores, giving you a 401). In the shell where you run Claude Code:
export ANTHROPIC_BASE_URL="http://127.0.0.1:8787"
export ANTHROPIC_AUTH_TOKEN="cc-cap-key"
unset ANTHROPIC_API_KEY
claude
unset ANTHROPIC_API_KEY keeps the client shell free of the real key — Stoke holds it. Claude Code now sends every request to http://127.0.0.1:8787/v1/messages with Authorization: Bearer cc-cap-key, and Stoke enforces before forwarding to Anthropic.
Verify it worked
Health check needs no auth:
curl -sS http://127.0.0.1:8787/health
# {"status":"ok","service":"stoke"}
Confirm fail-closed — a request with no Bearer token is rejected before it touches Anthropic:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8787/v1/messages \
-H "content-type: application/json" \
-d '{"model":"claude-haiku-4","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
# 401
Now a real forwarded request, proving passthrough:
curl -sS http://127.0.0.1:8787/v1/messages \
-H "Authorization: Bearer cc-cap-key" \
-H "content-type: application/json" \
-d '{
"model": "claude-haiku-4",
"max_tokens": 32,
"messages": [{"role": "user", "content": "Say hi in three words."}]
}'
You get back a normal Anthropic Messages response. Trip the loop breaker by sending the same prompt five times inside 60 seconds — the threshold is a global constant (5 similar requests in 60s → blocked for 120s):
for i in 1 2 3 4 5; do
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8787/v1/messages \
-H "Authorization: Bearer cc-cap-key" \
-H "content-type: application/json" \
-d '{"model":"claude-haiku-4","max_tokens":16,"messages":[{"role":"user","content":"retry me"}]}'
done
# 200
# 200
# 200
# 200
# 429
The fifth call's body reads: Loop detected: key cc-cap-k sent 5 similar requests within 60s. Blocked for 120s. Check your agent's retry logic — it may be stuck. That is a stuck-retry storm — the most common way agents burn credits — stopped at the gateway.
Inspect the per-key ledger any time. spend_usd accrues from every priced call, streamed or not. estimated_usd is the share of it Stoke had to guess because a provider reported no usage. reserved_usd is money held for requests still running — not spent yet, but the cap counts it. Your exact numbers will differ:
curl -sS http://127.0.0.1:8787/v1/budget -H "Authorization: Bearer cc-cap-key"
# {"auth_enabled":true,"keys":[{"key":"cc-cap-k","spend_usd":0.0007,
# "limit_usd":20.0,"recent_requests":6,"estimated_usd":0.0,
# "reserved_usd":0.0}], ... }
What this doesn't do
Read this section before you trust the dollar figure.
- It forwards to Anthropic; it does not run Claude Code on local models. Your traffic still goes to Claude and still bills Anthropic. Stoke controls whether a call goes out, not where it runs.
- Subscriptions can't be dollar-capped. If you use Claude Code on a Claude Max or Pro subscription rather than a metered API key, there is no per-request dollar price to cap. Stoke can still rate-limit and loop-kill that traffic — it just can't put a USD ceiling on it. Hard
budget_usdcaps apply only to metered API keys. - A stream is billed after it finishes, not while it runs. Streamed responses accrue spend — Stoke reads the usage Anthropic reports as the stream passes and charges the key when the stream ends, including when Claude Code disconnects mid-response. Because a stream in flight has not been charged yet, Stoke holds the most it could cost against your cap before dispatching it, and refuses a request whose hold would not fit.
/v1/budgetshows the outstanding holds asreserved_usd. Claude Code always sendsmax_tokens, which makes the hold exact rather than assumed. - A hold assumes the worst case, so a busy key can be refused early. The hold reserves
max_tokensworth of output even if the answer turns out to be three words. Run several agents on one capped key and some will get a 429 while the key still has room in practice. That is the cost of a hard stop rather than a ceiling; the alternative is discovering the overshoot on the invoice. Give each agent its own key if you want them to fail independently. - An estimate is not a measurement. If a metered provider reports no usage at all, Stoke bills an estimate rather than $0, and reports that share as
estimated_usdin/v1/budget. Treat it as a guess: it is derived from the prompt's length and the number of frames the provider streamed. - You supply the prices, and they can go stale. Stoke meters a response against the
[pricing.models]numbers in your config. It ships none and infers none, so a model you haven't priced is refused rather than served for free — but nothing tells you when a provider changes its rates. Check what the gateway believes:
stoke-cli pricing
- Loop thresholds are global, not per-key, and this is pre-release software (MIT).
Next steps
Get the binary, drop a [[keys]] cap in front of your metered key, and let the loop breaker do the rest — source, config reference, and issues are at github.com/Ozperium/stoke.
Put a control point in front of your agents
Stoke is a single Rust binary: hard budget caps, a runaway-loop kill switch, and local-first routing — enforced before a provider is ever called. Open source, MIT.
Get Stoke on GitHub