Your free allowance does not fail loudly. It fails at 3pm on a Tuesday.
Here is my position after watching this pattern repeat: an allowance is a deadline, not a cushion. Treat it like a deploy budget, or it will ambush you.
This post is an anti-pattern catalog. For each one: symptoms, root cause, replacement pattern. Then a small gate you can run before every call.
Why a free allowance behaves like a deadline
Three properties matter more than the size of the allowance.
- It is consumed by failures, not just successes. Retries, timeouts, and bad prompts all bill.
- It is shared across environments. Your laptop, CI, and the demo box dip into one pool.
- It is opaque until you measure it. Dashboards lag. Your logs do not.
Benchmark and AI-testing posts are everywhere this week. Counting is the part that breaks.
The gate: one file, three exit codes
The idea is boring on purpose. Before you send a model call, ask a file how much room is left.
# budget_gate.py — stdlib only, Python 3.10+ from __future__ import annotations import json import sys from dataclasses import dataclass from pathlib import Path @dataclass(frozen=True) class Plan: tier: str # full | lean | cache_only | stop max_tokens: int allow_retry: bool reason: str def plan_for(spent: int, allowance: int, *, reserve: int = 0) -> Plan: if allowance <= 0: raise ValueError("allowance must be positive") left = max(allowance - spent - reserve, 0) frac = left / allowance if frac >= 0.50: return Plan("full", left, True, f"{frac:.0%} of allowance left") if frac >= 0.20: return Plan("lean", left // 2, True, f"{frac:.0%} left, trim prompts") if frac >= 0.05: return Plan("cache_only", min(left, 2_000), False, f"{frac:.0%} left, replay only") return Plan("stop", 0, False, f"{frac:.0%} left, escalate") EXIT = {"full": 0, "lean": 0, "cache_only": 2, "stop": 3} def main(argv: list[str]) -> int: if len(argv) != 2: print("usage: python budget_gate.py ledger.json", file=sys.stderr) return 64 ledger = json.loads(Path(argv[1]).read_text(encoding="utf-8")) plan = plan_for( spent=ledger["spent_tokens"], allowance=ledger["allowance_tokens"], reserve=ledger.get("reserve_tokens", 0), ) print(json.dumps(plan.__dict__)) return EXIT[plan.tier] if __name__ == "__main__": raise SystemExit(main(sys.argv)) Exit 0 means go. Exit 2 means cache-only. Exit 3 means no fresh calls at all.
The ledger it reads
One file. Appended by whatever wrapper already calls your model.
{ "allowance_tokens": 1000000, "spent_tokens": 730000, "reserve_tokens": 50000, "updated_at": "2026-11-08T09:00:00Z" } reserve_tokens is the part you refuse to spend. Mine would cover the demo you promised someone.
Wire it into CI
set +e python budget_gate.py ledger.json > plan.json code=$? cat plan.json case "$code" in 0) echo "fresh calls allowed" ;; 2) echo "cache-only path" ;; 3) echo "stop: no fresh calls" && exit 1 ;; esac Notice what changed? Budget is now a gate, not a chart.
Tests that prove the gate
Boundaries are where this breaks. So test the boundaries.
# test_budget_gate.py — pytest import pytest from budget_gate import plan_for def test_half_left_is_full(): assert plan_for(spent=5_000, allowance=10_000).tier == "full" def test_just_under_half_is_lean(): assert plan_for(spent=5_001, allowance=10_000).tier == "lean" def test_below_twenty_is_cache_only(): assert plan_for(spent=8_500, allowance=10_000).tier == "cache_only" def test_below_five_is_stop(): assert plan_for(spent=9_600, allowance=10_000).tier == "stop" def test_reserve_shrinks_the_tier(): assert plan_for(spent=4_000, allowance=10_000, reserve=2_000).tier == "lean" def test_zero_allowance_is_rejected(): with pytest.raises(ValueError): plan_for(spent=0, allowance=0) I ran these six against the file above with the fake ledger. They pass. No live account involved.
The catalog: five anti-patterns
1. The Infinite Retry
Symptoms. A 429 storm. Wall-clock time triples. Nobody checks token spend.
Root cause. Retries are treated as free because they return errors.
Replacement. Give retries their own budget line, separate from tokens. Cap at one retry in lean, zero in cache_only.
2. The Fat Prompt on Every Call
Symptoms. Identical system prompts, resent forever. Cost per call never drops.
Root cause. No request fingerprint, so nothing is reused.
Replacement. Hash the prompt prefix and log the hash. Read your hit rate before you touch temperature.
3. The Allowance Assumption
Symptoms. A plan built on the current allowance. A quiet change. A broken sprint.
Root cause. A free allowance is treated as a contract with an SLA.
Replacement. Store the allowance as data with an expires_at field. Fail the gate when it passes.
4. The Blind Fallback
Symptoms. Free path errors. Code silently swaps in a paid key. Invoice surprise.
Root cause. Fallback is coded as resilience, not as a spending decision.
Replacement. Make fallback require an explicit flag. Log every switch with the reason.
5. The Green Dashboard
Symptoms. CI is green at 6% remaining. The next PR cannot run at all.
Root cause. Budget is visualized but never enforced.
Replacement. Exit code 3 fails the job. A warning is not a control.
Decision table
| Tier | Trigger | Fresh calls | Retries | Prompt size |
|---|---|---|---|---|
full | ≥50% left | yes | yes | normal |
lean | 20–50% | yes, capped | max 1 | trimmed |
cache_only | 5–20% | replay only | no | minimal |
stop | <5% | no | no | escalate |
Print this table in your README. People follow tables faster than prose.
Where MonkeyCode fits
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The MonkeyCode project is the context I had in mind: free model access plus a free server option. Both are operator-supplied claims, and the exact token allowance is time-sensitive. Verify it yourself before you build a plan on it.
The practical part: a gate like this is one stdlib file. It runs happily on a small free server instance, right next to the code that makes the calls. Keep the ledger on that box. Then the gate and the spend live in the same place.
Limitations and who should skip this
Be honest about the edges.
- It is advisory, not transactional. Two concurrent workers can both pass the gate and both overspend. Use a lock or accept the drift.
- The ledger can lie. If any caller bypasses the wrapper, the number is fiction.
- It adds a step to every call. For a weekend script, that step is pure overhead.
- Skip it if you have committed paid capacity. The gate mostly produces noise there.
- Skip it if you need a permanence guarantee. No free tier gives you one.
Who should use it? Anyone whose CI, demo box, and laptop share one allowance. That is most of us.
Run it before the next deploy
Do this today, in this order.
- Write
ledger.jsonwith your real allowance and today's spend. - Run
python budget_gate.py ledger.json. - Look at the tier. Are you surprised? That surprise is the whole point.
Then wire exit code 3 into CI and stop guessing.
What is your current spend as a fraction of the allowance? If you cannot answer in one second, the gate is missing.
Clone the file, point it at your ledger, and check which tier you land in before your next deploy.

