Everyone on my timeline spent the week arguing about AI fatigue, and I get it. The part that wears me out isn't the models — it's that almost nobody hands you a way to check the numbers yourself. So I gave myself 48 hours to answer one boring question: if a tool advertises free model access, what would I need to measure before I put a loop on it?
This is not a review of a model, and it is not a benchmark. It's the ledger I built, the two things that broke, and the checklist I now run before I let any agent loop spend someone else's free tier. I used the open-source MonkeyCode project as the test subject because it offers free model access plus a free server option, which is exactly the combination that makes accounting hard. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
One honesty note up front: the free allotment is described in the project's outreach as 10 million tokens, and there is a free server option. I could not independently verify either quota from primary documentation inside a 48-hour window, so treat both as claims to confirm on the project page, not as constants you can plan a budget around.
Hour 0–6: The only thing I actually trusted was my own log
My first instinct was to read the provider's usage dashboard, and my second instinct was to distrust it. Dashboards aggregate; loops retry; a retry that fails after the completion was generated can still be billed somewhere, and you will never see it in a per-day bar chart. So the ledger lives locally, and it records what each call reported about itself.
# meter.py — append one row per model call to a local JSONL ledger.
# Nothing here is provider-specific; it just refuses to guess.
import json, time, pathlib
LEDGER = pathlib.Path("usage.jsonl")
def record(provider: str, model: str, usage: dict, note: str = "") -> None:
row = {
"ts": time.time(),
"provider": provider,
"model": model,
"prompt_tokens": usage.get("prompt_tokens"),
"completion_tokens": usage.get("completion_tokens"),
"total_tokens": usage.get("total_tokens"),
"note": note,
}
with LEDGER.open("a") as f:
f.write(json.dumps(row) + "\n")
The usage object comes from the response body, not from my estimate. That distinction matters more than it sounds: if the field is missing, I want a None in the ledger screaming at me, not a plausible-looking number my own code invented. Wrap the call site once, pass note="retry-1" on retries, and the ledger becomes a record of intent as well as spend.
Then a ten-line summary, because a JSONL file you never read is just disk usage:
# summary.py — p50 cost per call plus a naive burn-rate projection.
import json, statistics, pathlib, time
rows = [json.loads(l) for l in pathlib.Path("usage.jsonl").read_text().splitlines() if l]
totals = [r["total_tokens"] for r in rows if r["total_tokens"]]
span_h = (rows[-1]["ts"] - rows[0]["ts"]) / 3600 if len(rows) > 1 else 0
print(f"calls={len(rows)} p50={statistics.median(totals)} tokens")
print(f"nulls={sum(1 for r in rows if not r['total_tokens'])} span={span_h:.2f}h")
print(f"burn={sum(totals)/span_h:.0f} tokens/h" if span_h else "span too short")
Hour 6–18: Two things broke, and neither was the model
Break one: my retry counter and my ledger disagreed
I ran a loop that retried on 429 and on read timeouts, and the ledger showed 41 calls while the client had issued 58. The gap was all retries that never got a response — so nothing was appended, and my "burn rate" was fiction. The fix was to log the attempt before sending and the outcome after, rather than logging only on success.
- Attempt row:
{ts, model, note:"attempt"}written before the request. - Outcome row:
{ts, usage, note:"ok"|"429"|"timeout"}written after. - Reconciliation:
attempts - outcomesis your in-flight and failed count, visible instead of implied.
Break two: the free server changed my failure model, not my CPU
Moving a worker onto a remote free server felt like a speed decision, and it was really a lifecycle decision. Local runs fail fast and fail loudly; remote runs fail as latency, as a connection that closes mid-stream, or as a service that answers a health check but stalls on the first real completion. Here is the smoke test I now run before any loop, and it checks truncation, not just status:
# Check the project docs for the current base URL and model id; both move.
curl -sS -o /tmp/out.json \
-w 'http=%{http_code} ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"model":"'"$MODEL"'","messages":[{"role":"user","content":"reply with the single word: ok"}],"max_tokens":8}' \
"$BASE_URL/v1/chat/completions"
python -c "import json;d=json.load(open('/tmp/out.json'));print(d['choices'][0]['message']['content']);print(d.get('usage'))"
If usage is absent from that response, your ledger will fill with None under load, and you will be back to trusting a dashboard. That is a five-second check worth doing before a 6-hour job, wouldn't you say?
Hour 18–36: The decision table I now fill in before renting anything
Free is a pricing fact, not an architecture. The question is which part of your workflow tolerates a lifecycle you don't control.
| Workload | Duration | Latency tolerance | Where I put it |
|---|---|---|---|
| Prompt/response shaping, unit tests | seconds | low | Local, always |
| Batch summarization of a corpus | 1–6 h | medium | Free server, with ledger + retry cap |
| Interactive UI path in prod | continuous | very low | Neither untested tier |
| Anything with regulated data | any | any | Read the data policy first |
Two rules survive the table. First, cap retries and total attempts in code, not in your head — a max_attempts=3 plus a wall-clock deadline has saved me twice. Second, keep a --dry-run flag that walks the loop and logs prompts without sending them, so a misconfigured prompt can waste your afternoon instead of your allotment.
Hour 36–48: What I would repeat, and who should walk away
I would repeat the ledger, the attempt/outcome split, the truncation smoke test, and the dry run. Those four cost me maybe ninety minutes and turned "I think it's cheap" into a number I could defend in a code review. I would skip the dashboard archaeology entirely.
What I would not claim: that a free tier is fast, stable, or permanent. Token counts also differ by tokenizer, so a 10-million-token claim means very little until you know which tokenizer is counting and whether retries are billed. If you need guaranteed uptime, sub-second interactive latency, or a contractual data-handling statement, this whole approach is the wrong tool and you should not use it — go buy a plan with an SLA.
If you want to reproduce the ledger, the project is open source, and the smoke test above is the cheapest way in; swap in your own endpoint and model id, then let your log tell you whether the free tier fits your loop.
Top comments (0)