48-Hour Field Notes: I Built a Token Ledger Before Trusting Any Free Tier
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 hour
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.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.