A developer signed up for a free AI coding tier. Monday morning, the quota looked generous. Thursday afternoon, it was gone. Nobody logged a single request. The vendor dashboard showed usage. Nobody could say what the quota actually bought.
This is the real cost of free tiers. They stay opaque until you measure them.
MonkeyCode offers a free model tier and a free server. The quota is 10 million tokens. The project is open source.
Disclosure: This article was prepared as part of MonkeyCode’s product outreach.
This article builds a reproducible harness. It measures where the free tier performs. It finds where it breaks. The harness works with any OpenAI-compatible API. MonkeyCode is the example here.
Why a token ledger matters
Quotas hide variance. A 10 million token pool sounds large. Agentic loops multiply token use. Context grows with every tool call. One long refactor can cost more than fifty small fixes.
Without a ledger, the pool empties silently. With a ledger, every token has a purpose. The same logic applies to the free server. Uptime looks fine from a single request. A probe loop reveals the real story.
Vendor dashboards report aggregate usage. They rarely report pass rate. They never report what the quota bought. A badge measures marketing. A ledger measures work.
The experiment design
Five tasks. Increasing difficulty. Each task asks the model to produce code. A sandbox executes the code. The harness records four metrics.
- Pass rate: the fraction of tasks that run clean on the first try.
- Token burn: total tokens consumed per task.
- Latency: time from request to full response.
- Error type: timeout, auth failure, rate limit, or malformed JSON.
Five tasks is a deliberate number. A single task proves nothing. Five tasks cover the common failure modes. String manipulation, control flow, networking, data, and parsing. Each layer stresses a different part of the model.
The suite runs in about fifteen minutes. It costs a small fraction of the quota. The output is a JSONL ledger. Every line is one complete record.
Step 1: Define the task set
Save this file as tasks.mjs.
export const TASKS = [
{
id: "reverse-string",
prompt: "Write a Python function that reverses a string without slicing.",
file: "reverse.py",
check: "python3 reverse.py"
},
{
id: "fizzbuzz",
prompt: "Write a Python script that prints FizzBuzz from 1 to 100.",
file: "fizzbuzz.py",
check: "python3 fizzbuzz.py"
},
{
id: "http-json",
prompt: "Write a Node.js HTTP server that returns JSON on GET /health.",
file: "server.mjs",
check: "node server.mjs & pid=$!; sleep 1; curl -s localhost:3000/health; kill $pid"
},
{
id: "sql-top5",
prompt: "Write a SQL query for the top 5 customers by total order value.",
file: "query.sql",
check: "sqlite3 test.db < query.sql"
},
{
id: "regex-dates",
prompt: "Write a Python script that extracts ISO dates from a log file.",
file: "dates.py",
check: "python3 dates.py < sample.log"
}
];
Enter fullscreen mode Exit fullscreen mode
The tasks are boring on purpose. Boring tasks isolate model behavior. They remove human cleverness from the equation.
Each task has a check command. The check runs in a fresh directory. It has a 15 second timeout. A hanging script fails fast.
Step 2: Run the model and record everything
Save this file as run.mjs.
import { TASKS } from "./tasks.mjs";
import { execSync } from "node:child_process";
import { writeFileSync, appendFileSync, mkdirSync } from "node:fs";
const API_URL = process.env.MC_API_URL;
const API_KEY = process.env.MC_API_KEY;
const MODEL = process.env.MC_MODEL;
for (const task of TASKS) {
const started = Date.now();
const res = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: MODEL,
messages: [
{ role: "system", content: "Return only code. No fences. No prose." },
{ role: "user", content: task.prompt }
]
})
});
const body = await res.json();
const code = body.choices?.[0]?.message?.content ?? "";
const tokens = body.usage?.total_tokens ?? 0;
const latency = Date.now() - started;
let passed = false;
let error = "";
try {
mkdirSync(`out/${task.id}`, { recursive: true });
writeFileSync(`out/${task.id}/${task.file}`, code);
execSync(`cd out/${task.id} && ${task.check}`, { timeout: 15000 });
passed = true;
} catch (e) {
error = e.message.split("\n")[0];
}
const record = { task: task.id, passed, tokens, latency, error };
appendFileSync("ledger.jsonl", JSON.stringify(record) + "\n");
console.log(JSON.stringify(record));
}
Enter fullscreen mode Exit fullscreen mode
Set three environment variables. The values come from your MonkeyCode account settings. Check the current docs for the endpoint and model name. Quotas and names change.
export MC_API_URL="https://api.monkeycode.example/v1/chat/completions"
export MC_API_KEY="your-key-here"
export MC_MODEL="the-free-model-name"
Enter fullscreen mode Exit fullscreen mode
Run the suite.
node run.mjs
Enter fullscreen mode Exit fullscreen mode
Every task appends one JSON line to ledger.jsonl. The console prints the same record. Nothing is lost.
Step 3: Probe the free server
The free model is half the claim. The free server is the other half. This script measures the server from the outside.
Save this file as probe.mjs.
const url = process.env.MC_SERVER_URL;
const started = Date.now();
const res = await fetch(url);
const latency = Date.now() - started;
console.log(JSON.stringify({
status: res.status,
latency,
at: new Date().toISOString()
}));
Enter fullscreen mode Exit fullscreen mode
Run fifty probes. Space them two seconds apart.
export MC_SERVER_URL="https://your-free-server.example"
for i in $(seq 1 50); do node probe.mjs >> server-ledger.jsonl; sleep 2; done
Enter fullscreen mode Exit fullscreen mode
The loop takes under two minutes. It reveals cold starts, rate limits, and timeouts.
Reading the ledger
Example records. Real values will differ per run.
{"task":"reverse-string","passed":true,"tokens":842,"latency":4120,"error":""}
{"task":"fizzbuzz","passed":true,"tokens":1204,"latency":5380,"error":""}
{"task":"http-json","passed":false,"tokens":8931,"latency":22140,"error":"ECONNREFUSED"}
Enter fullscreen mode Exit fullscreen mode
The third record shows the pattern to watch. The model produced code. The code failed to start. Token burn was ten times the first task. Failure is not free. It is billed in tokens.
Do the arithmetic on your own ledger. A task that burns 5K tokens supports 2,000 runs against a 10M quota. A task that burns 50K tokens supports only 200 runs. The difference is a factor of ten. That factor decides whether the free tier lasts a week or a year.
The server ledger tells a different story. Plot the latency column. Look for spikes every N requests. A regular spike pattern suggests a cold start. Random timeouts suggest throttling.
Use these thresholds as a starting point. Adjust them to the target workflow.
Metric Warning threshold What it means Pass rate Below 0.6 The model needs heavy prompt engineering Token burn Above 50K per task The 10M quota will not survive real work Latency Above 60 seconds Interactive coding becomes painful Error type Repeated rate limits The free tier throttles before the quota endsDecision matrix
Situation Free tier verdict Prototyping and one-off scripts Sufficient Batch code generation with large contexts Watch token burn Production API behind the free server Not sufficient CI pipelines with hard deadlines Not sufficientThe free tier is a tool. It is not a contract. Check the service terms before depending on it.
How to extend the harness
Add a retry wrapper. Rate limits are common on free tiers. Record the first attempt. Do not hide the retry in the log.
// callModel wraps the fetch logic from run.mjs
async function callWithRetry(task, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
return await callModel(task);
} catch (e) {
if (i === attempts - 1) throw e;
await new Promise(r => setTimeout(r, 2000 * (i + 1)));
}
}
}
Enter fullscreen mode Exit fullscreen mode
Run the suite twice. Once in the morning. Once at peak hours. Compare the latency columns. Free tiers often degrade under load. The ledger makes that degradation visible.
Add a temperature field to the request body. Set it to zero for deterministic checks. Set it higher for exploratory tasks. Record the temperature in the ledger. Reproducibility requires fixed settings.
Add your own tasks. Use code from your real work. The harness only needs a prompt, a file name, and a check command.
Limitations of this method
Five tasks are a sample. They are not a benchmark. One run hides variance. Model behavior shifts between releases. Quota terms can change without notice.
Treat the ledger as a signal. Do not treat it as a certification.
Who should skip this approach
Teams with production workloads need guarantees. Regulated environments need audit trails. Anyone who needs an SLA should pay for one.
A free tier is for learning, prototyping, and low-stakes automation. That is a real job. It is not every job.
The conclusion
The harness answers three questions. It shows whether the model produces runnable code. It shows how many tokens a task costs. It shows whether the server survives a probe loop.
A dashboard shows usage. A ledger shows value. Run the harness against MonkeyCode’s free tier. The ledger will tell you the truth.