A recurring scene in team chats: one engineer posts a screenshot of a free tier answering a genuinely hard question, another posts a monthly cost report, and both are correct. The screenshot captures a single exchange on a single afternoon, while the report aggregates retries, resent context, and prompts nobody saved. What follows is usually an argument about trusting a vendor, when it could be an argument about a file in the repository.
Free AI coding tiers have become ordinary infrastructure, and the conversation around them has drifted toward vibes — visible in the recent wave of 'AI fatigue' posts as much as in launch threads. The useful correction is not more opinion; it is a smaller, timestamped claim that someone else can re-run. Four myths keep surviving contact with that standard, and each one falls to a probe that takes about half an hour to assemble.
Myth 1: open source means the model runs next to you
Most open-source AI coding projects are clients, gateways, or orchestration layers, while the weights sit behind an HTTP boundary you do not control. 'Open source' tells you which part you can audit and patch, and it says nothing about where inference happens or what happens when the network drops. That distinction matters for air-gapped review, for data-handling sign-off, and for anyone who assumes graceful local fallback.
You can settle the question with an egress trace instead of a README argument. Run strace -f -e trace=connect -o /tmp/egress.txt python3 ask.py ping and inspect the resolved addresses with grep -o 'sin_addr=inet_addr("[^"]*")' /tmp/egress.txt | sort -u. While a slow request is still in flight, ss -tunp | grep python3 shows the live sockets and remote ports, which is usually enough to answer the only question a reviewer cares about.
The corrected mental model is a thin local client plus a remote service that carries its own terms, quotas, and retention policy. You can self-host the client in minutes and you cannot self-host the model in minutes, so keep the two separate in your architecture notes.
Myth 2: a free server is a workstation you can leave running
Free compute is usually ephemeral by design: the container restarts, the disk resets, and the CPU is shared with whoever else is on the host that minute. None of that makes it useless, but it does make it a poor home for a long-running daemon or for state you cannot rebuild from the repository. The failure mode is quiet, because the first session feels identical to a paid box.
Fingerprint the environment before and after instead of trusting your memory of it:
fp() {
{ uname -a; python3 -V; nproc
python3 -m pip freeze 2>/dev/null | sort | sha256sum
env | grep -viE 'token|key|secret' | sort | sha256sum
}
}
fp > /tmp/fp_t0.txt # capture at session start
# ... a few hours later, in a fresh shell ...
fp | diff -u /tmp/fp_t0.txt - || echo 'environment drifted'
Keep volatile lines out of the fingerprint if you only care about structure, since memory and disk figures move on their own and produce noise. A drift report that fires on every run teaches you to ignore it, which is worse than having no report at all.
Myth 3: the usage field tells you what you spent
Provider usage fields are useful and they are not an invoice, because they count a request shape you may not recognise. A system prompt gets prepended, a failed call gets retried, and an agent loop resends the whole conversation on every turn, so prompt tokens can exceed your visible message by a wide margin. The gap is not fraud; it is the difference between what you typed and what was sent.
The pair of scripts below reconciles the two views. ask.py writes one JSONL record per exchange, and token_audit.py compares the provider's counts against a local count of the exact strings you transmitted.
#!/usr/bin/env python3
# ask.py - template; endpoint and model id come from your own dashboard
import datetime, json, os, time, urllib.request, uuid
BASE = os.environ['BASE_URL'].rstrip('/') # OpenAI-compatible base URL
KEY = os.environ['API_KEY']
MODEL = os.environ['MODEL_ID']
def ask(prompt: str, out: str = 'runs.jsonl') -> dict:
payload = {'model': MODEL, 'temperature': 0,
'messages': [{'role': 'user', 'content': prompt}]}
req = urllib.request.Request(
f'{BASE}/chat/completions',
data=json.dumps(payload).encode(),
headers={'Content-Type': 'application/json',
'Authorization': f'Bearer {KEY}'})
t0 = time.time()
with urllib.request.urlopen(req, timeout=120) as r:
resp = json.loads(r.read())
rec = {'ts': datetime.datetime.now(datetime.timezone.utc).isoformat(),
'run_id': str(uuid.uuid4()),
'latency_ms': round((time.time() - t0) * 1000),
'request': payload, 'response': resp}
with open(out, 'a') as f:
f.write(json.dumps(rec) + '\n')
return resp
if __name__ == '__main__':
print(ask('Reply with the single word: ok')['choices'][0]['message']['content'])
#!/usr/bin/env python3
# token_audit.py - reconcile reported usage against local counts
import hashlib, json, statistics, sys
from pathlib import Path
def count_tokens(text: str) -> int:
try:
import tiktoken
return len(tiktoken.get_encoding('cl100k_base').encode(text))
except ImportError:
return max(1, len(text) // 4) # coarse estimate, labelled as such
def main(path: str) -> None:
overheads, rows = [], []
for i, line in enumerate(Path(path).read_text().splitlines(), 1):
if not line.strip():
continue
rec = json.loads(line)
sent = '\n'.join(m.get('content', '') for m in
rec.get('request', {}).get('messages', []))
choice = (rec.get('response', {}).get('choices') or [{}])[0]
reply = choice.get('message', {}).get('content') or ''
usage = rec.get('response', {}).get('usage') or {}
local_in, local_out = count_tokens(sent), count_tokens(reply)
rep_in, rep_out = usage.get('prompt_tokens'), usage.get('completion_tokens')
oh = None if rep_in is None else rep_in - local_in
if oh is not None:
overheads.append(oh)
rows.append((i, hashlib.sha256(reply.encode()).hexdigest()[:10],
local_in, rep_in, oh, local_out, rep_out))
print('ln, reply_sha, local_in, reported_in, overhead, local_out, reported_out')
for r in rows:
print(r)
if overheads:
print(f'median prompt overhead: {statistics.median(overheads):+.0f} tokens'
f' over {len(overheads)} runs')
if __name__ == '__main__':
main(sys.argv[1] if len(sys.argv) > 1 else 'runs.jsonl')
When usage is missing entirely, the script reports that instead of guessing, and you fall back to the local estimate with a visible caveat. That honesty is the actual deliverable: a number with a method beats a number with a confident tone.
Myth 4: one good run means the tier works
A single success proves the path existed once, which is roughly what a screenshot proves. Free tiers change shape between morning and evening, model identifiers get repointed, and rate limits appear exactly when a deadline does. Time-spaced repetition is the cheapest defence, and it costs three prompts.
The plan I follow is deliberately boring. Send the same temperature-zero prompt three times, at least six hours apart, and keep every record written by ask.py. Then run python3 token_audit.py runs.jsonl and compare the reply hashes, the latencies, and the median prompt overhead across all three records. A stable hash is a bonus rather than an expectation, and a shifting overhead tells you the wrapper or the system prompt changed underneath you.
Choosing where a job should run
The table is the part I reuse most, because it converts an argument into a routing decision.
| Job | Laptop plus free model access | Free server | Paid or self-hosted |
|---|---|---|---|
| One-off question or syntax check | fine | unnecessary | waste |
| Repo-wide mechanical edit | fine, on a branch | fine, after pushing | when the run must survive a restart |
| Multi-hour agent loop | risky | risky | yes |
| Anything touching customer data | only after review | only after review | depends on the contract |
| Benchmarking model quality | no | no | pinned version, pinned hardware |
The rightmost column is not an argument for spending money; it is where reproducibility becomes a requirement instead of a preference. Most exploratory work never gets there, which is why the free columns genuinely earn their place.
Limitations and who should skip this
The probe only sees what your client can see, so server-side accounting, caching discounts, and queueing stay invisible to it. The token count without tiktoken installed is coarse enough to be wrong by double digits on unusual text, and it measures volume rather than response quality. A free server that wipes its disk will also wipe the runs.jsonl you forgot to commit, so push early and push often.
Skip this approach if you need a contractual uptime guarantee, if your data cannot leave a controlled network, or if the real question is whether one model beats another. Quality comparisons need a pinned model version, a fixed evaluation set, and more patience than a thirty-minute probe can supply.
Where MonkeyCode fits in this workflow
MonkeyCode is an open-source AI coding project offering free model access and a free server option, and both availability claims belong to the operator rather than to this article's measurements. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Inside the workflow above they serve a narrow purpose: free model access lets ask.py produce the first three records without a billing conversation, and the free server gives you a second location to fingerprint, which is precisely the drift check from Myth 2.
Treat any quoted allowance as a dated claim rather than a permanent property, because free-tier numbers printed in older posts go stale faster than the code does. Check the project's current documentation before you plan a week of work around it, and write the date next to the number in your notes. If you run the three probes and keep the output, you will hold a review comment worth more than any screenshot, which is the only endorsement I am comfortable making here.
If you do try it, the useful thing to share back is not 'it worked' but the overhead column from token_audit.py.
Top comments (0)