A free tier is not a budget until something refuses a request on your behalf. That sentence ended a pairing session last week, after four questions and two dead ends produced a small pre-flight gate that now runs before every prompt our team sends to a hosted coding model. The gate is under forty lines of standard-library Python, it has a table-driven test, and it would still be worth keeping if every hosted tool disappeared tomorrow.
The setup, in numbers rather than adjectives
The session started from a concrete constraint: a free tier for AI coding work, an open-source client to drive it, and a free server option for anything that needed to stay running. The operator of MonkeyCode states that the project is open source, that free model access is available, that a free server option exists, and that the free allocation includes 10 million tokens. Those figures are operator-supplied and worth re-reading on the project page before planning against them, because free-tier terms change without notice.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The senior in the session had one rule for the afternoon: no request leaves the machine until we can say what it costs and what happens when it fails. She did not ask for a benchmark, a pricing page, or a comparison chart. She asked four questions, and the answers turned out to be an architecture.
Four questions asked before the keyboard was touched
- Where does the number live? If the remaining budget exists only in a dashboard tab, it does not exist in a script, a CI job, or a teammate's head.
- What refuses the request? A warning that logs a line is not a control; a non-zero exit code is a control because shells, make targets, and pipelines already respect it.
- What is the estimate allowed to be wrong about? Any heuristic will misjudge code and JSON differently from prose, so the gate needs a stated direction of error, not an accuracy claim.
- What happens to local state when the host is free? Anything that is not on the local disk must be assumed to be temporary until a contract says otherwise.
Those four answers fit on an index card, which is exactly why the first two attempts were wrong.
Dead end one: counting characters and calling it accounting
The first draft divided the prompt length by four, printed a friendly number, and moved on. It looked reasonable in a demo and failed the second question immediately, because nothing in the script could stop a request; the number was decoration. The senior's objection was narrower than expected: character heuristics under-count dense code and over-count English prose, so a team that trusts the number as truth will build on sand.
The fix was to stop describing the estimate as a measurement and start describing it as a reservation. The script reserves a conservative amount before the call, records it in a ledger file on disk, and lets a later reconciliation step correct the entry once the provider reports real usage. Direction of error is stated in the docstring instead of implied by a decimal point.
Dead end two: treating the free server as a staging environment
The second draft pushed the ledger and the prompt queue onto the free server, which felt tidy and was the worst idea of the afternoon. Free hosts are excellent for experiments and terrible for silent assumptions about persistence, backups, or resource ceilings, and none of those terms had been read. The senior rewrote the plan in one line: the ledger stays on the developer's machine or in the repository, and the server holds only what can be rebuilt from that ledger in a minute.
That single constraint also removed a class of security questions. No credentials, no customer text, and no long-lived secrets were placed on the hosted side, so a surprise reset would cost a restart rather than an incident review. Readers should verify the data-handling and persistence terms of any free host themselves; this article does not claim to know them.
The decision kept: reserve before send, reconcile after
The artifact that survived is a gate that returns a decision and an exit code, and a ledger that can be inspected with a text editor. It is deliberately boring, because boring code gets run.
#!/usr/bin/env python3
"""budget_gate.py -- refuse a request the free tier cannot comfortably pay for.
Stdlib only. The token estimate is a heuristic for allocation discipline,
not for billing reconciliation.
"""
import json
import os
import sys
import time
CHARS_PER_TOKEN = 4 # conservative for prose, risky for dense code
LEDGER = os.environ.get("BUDGET_LEDGER", "budget_ledger.json")
def estimate_tokens(text: str) -> int:
return max(1, len(text) // CHARS_PER_TOKEN)
def load_ledger(path: str = LEDGER) -> dict:
if not os.path.exists(path):
return {"spent": 0, "entries": []}
with open(path, encoding="utf-8") as fh:
return json.load(fh)
def save_ledger(data: dict, path: str = LEDGER) -> None:
tmp = f"{path}.tmp"
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2, sort_keys=True)
os.replace(tmp, path)
def verdict(prompt: str, ceiling: int, reserve: int = 0, path: str = LEDGER) -> dict:
est = estimate_tokens(prompt)
data = load_ledger(path)
remaining = ceiling - data["spent"]
decision = "allow" if est + reserve <= remaining else "refuse"
return {"estimate": est, "remaining": remaining, "decision": decision}
def commit(amount: int, label: str, path: str = LEDGER) -> None:
data = load_ledger(path)
data["spent"] += amount
data["entries"].append({"label": label, "amount": amount, "at": int(time.time())})
save_ledger(data, path)
def reconcile(reserved: int, actual: int, label: str, path: str = LEDGER) -> None:
"""Correct the ledger once the provider reports real usage."""
commit(actual - reserved, f"{label}:reconcile", path)
if __name__ == "__main__":
ceiling = int(sys.argv[2]) if len(sys.argv) > 2 else 10_000_000
with open(sys.argv[1], encoding="utf-8") as fh:
prompt = fh.read()
result = verdict(prompt, ceiling)
print(json.dumps(result))
sys.exit(0 if result["decision"] == "allow" else 1)
Wiring it into a shell workflow takes one line, and the exit code does the enforcement:
python3 budget_gate.py prompt.txt 10000000 || { echo "budget gate refused the request"; exit 1; }
Step-by-step: reproducing the afternoon in about thirty minutes
- Save the script above as
budget_gate.pyand create an emptybudget_ledger.jsonin the repository root. - Write the prompt to a file instead of pasting it into a shell, so the gate measures exactly what will be sent.
- Run the gate, then run it again after a request, and confirm the ledger grows by the reserved amount rather than the actual amount.
- Add reconciliation once the provider exposes usage numbers, and record the difference in the ledger instead of overwriting it.
- Commit the ledger only if the labels are free of customer text; otherwise keep it local and commit the script alone.
The test is table-driven because the interesting cases are arithmetic, not integration:
import os
import tempfile
import unittest
import budget_gate as bg
class GateTest(unittest.TestCase):
def setUp(self):
fd, self.path = tempfile.mkstemp(suffix=".json")
os.close(fd)
os.unlink(self.path)
self.ceiling = 1000
def test_decision_table(self):
cases = [
("empty ledger", "", 0, "allow"),
("a third spent", "x" * 2000, 300, "allow"),
("half spent", "x" * 2000, 500, "refuse"),
("near ceiling", "x" * 2000, 990, "refuse"),
]
for name, prompt, spent, expected in cases:
if os.path.exists(self.path):
os.unlink(self.path)
if spent:
bg.commit(spent, "seed", self.path)
got = bg.verdict(prompt, self.ceiling, reserve=100, path=self.path)["decision"]
self.assertEqual(got, expected, name)
The fourth row is the one that earns its place: a nearly exhausted ledger must refuse even a small request, because the reserve represents the follow-up call that a refusal protects.
Routing table: what belongs on the free tier at all
| Task shape | Route | Reason | Guard |
|---|---|---|---|
| Explaining unfamiliar code | Free hosted model | Cheap, reversible, read-only | Prompt files contain no secrets |
| Writing a first draft of tests | Free hosted model | Reviewable by a human in minutes | Gate plus mandatory test run |
| Long refactors across modules | Senior reviewer first | Estimation error compounds | One file per request |
| Anything with customer text | Local only | Data boundary unknown | Explicit approval |
| Scheduled jobs or queues | Free server, state off-host | Host may reset | Rebuild from local ledger |
Limitations and who should not use this
The heuristic is not a measurement, and teams that already receive per-request usage numbers should reconcile on every call rather than reserving. Anyone running regulated workloads, handling personal data, or needing uptime guarantees should not put the free server in a critical path, regardless of how good the experiment looked. This approach also does nothing to improve model output quality; it only prevents a quiet budget exhaustion that turns into a broken pipeline on a Friday.
For teams that want to try the workflow against a hosted free tier, the operator's stated 10 million free tokens and free server option are the entry point, and the terms are worth reading before anyone depends on them.
Top comments (0)