DEV Community

Sam Chen
Sam Chen

Posted on

Five Ways a Free Token Allowance Dies Quietly — and the Budget Gate That Stops It

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))
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

  1. Write ledger.json with your real allowance and today's spend.
  2. Run python budget_gate.py ledger.json.
  3. 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.

Top comments (0)