A 30M-token quota looks generous until five people share it. At 2,000 tokens per call and 200 calls per person per day, the quota lasts 15 working days. The first failure is not the model or the server; it is the shared counter that cannot tell you which feature consumed 80% of the budget. That is why you should not build a quota tracker that increments a single number. Build a ledger that attributes every token to a project, feature, and caller before the team starts treating the free tier as unlimited.
I am evaluating the MonkeyCode open-source project's free model access and free server option, which includes a 30M-token quota. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The quota and free-server terms are operator-supplied and can change; verify current limits before building around them.
The current AI news cycle is full of watermarking debates and agent gatekeepers, but the mechanical failure many teams hit is quieter: nobody knows which feature burned the shared quota. The token total does not answer that. A ledger does.
The shared counter hides the cost of each feature
A single integer that decreases on every API call is easy to implement and useless to debug. It tells you how much is left, not what consumed it. When the quota hits zero, the team is left arguing about which feature was responsible. That argument is expensive: it happens after the outage, under time pressure, and with no data.
A free tier is not a blank check. It is a finite shared resource with an invisible price: every under-tested feature, retry loop, or exploratory script draws from the same pool. Without attribution, the most aggressive consumer wins by default, and the most careful teammate gets blamed when the meter stops.
The fix is not a better dashboard. It is a schema that records every token against a project, a feature, and a caller. Once those columns exist, the team can answer cost questions before the quota is gone.
A ledger is a schema, not a dashboard
Start with a SQLite table. In a local or development environment, SQLite is enough. In a production gateway, replace it with a log sink like ClickHouse or a managed analytics store.
CREATE TABLE token_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
occurred_at TEXT NOT NULL,
project TEXT NOT NULL,
feature TEXT NOT NULL,
caller TEXT NOT NULL,
model TEXT NOT NULL,
prompt_tokens INTEGER NOT NULL,
completion_tokens INTEGER NOT NULL,
total_tokens INTEGER NOT NULL,
status_code INTEGER NOT NULL,
latency_ms INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_token_usage_project_time
ON token_usage(project, occurred_at);
CREATE INDEX idx_token_usage_feature_time
ON token_usage(feature, occurred_at);
The key design choice is to store project, feature, and caller as explicit columns. Do not store a single label string and parse it later. Parsing a comma-separated column in a hot path leads to the same blame game you were trying to avoid.
After each API call, insert one row. The snippet below wraps a generic chat completion function so every call is recorded without forcing the caller to remember bookkeeping.
import sqlite3
import time
import json
import os
LEDGER_DB = os.environ.get('LEDGER_DB', 'token_usage.db')
def record_token_usage(record: dict) -> None:
conn = sqlite3.connect(LEDGER_DB)
conn.execute(
"""
INSERT INTO token_usage (
occurred_at, project, feature, caller, model,
prompt_tokens, completion_tokens, total_tokens,
status_code, latency_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
record.get('project', 'unknown'),
record.get('feature', 'unknown'),
record.get('caller', 'unknown'),
record.get('model', 'unknown'),
record.get('prompt_tokens', 0),
record.get('completion_tokens', 0),
record.get('total_tokens', 0),
record.get('status_code', 0),
record.get('latency_ms', 0),
)
)
conn.commit()
conn.close()
# Example of a wrapped provider call
def chat_completion(project: str, feature: str, caller: str, messages: list, max_tokens: int = 256):
started = time.perf_counter()
# Replace this with the actual provider request from the MonkeyCode free server
# The response body should include usage.prompt_tokens, usage.completion_tokens, usage.total_tokens
response = { # illustrative stub
'status_code': 200,
'usage': {'prompt_tokens': 120, 'completion_tokens': 80, 'total_tokens': 200}
}
latency_ms = int((time.perf_counter() - started) * 1000)
record_token_usage({
'project': project,
'feature': feature,
'caller': caller,
'model': 'issued-model',
'prompt_tokens': response['usage']['prompt_tokens'],
'completion_tokens': response['usage']['completion_tokens'],
'total_tokens': response['usage']['total_tokens'],
'status_code': response['status_code'],
'latency_ms': latency_ms,
})
return response
Do not wait for the quota to run out before adding this. Add the schema and the record_token_usage call in the first pull request that introduces the free route. If you add it later, the early consumption data is already lost.
Policy questions the ledger can answer
Once the rows exist, a few SQL queries turn the shared counter into a burn-down report.
-- Which project consumed the most tokens in the last 7 days?
SELECT project, SUM(total_tokens) AS tokens
FROM token_usage
WHERE occurred_at >= datetime('now', '-7 days')
GROUP BY project
ORDER BY tokens DESC;
-- Which feature had the highest error rate in the last 24 hours?
SELECT feature,
SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END) AS errors,
COUNT(*) AS calls,
ROUND(100.0 * SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END) / COUNT(*), 2) AS error_pct
FROM token_usage
WHERE occurred_at >= datetime('now', '-1 day')
GROUP BY feature
ORDER BY error_pct DESC;
-- Daily burn rate for the last 14 days
SELECT date(occurred_at) AS day, SUM(total_tokens) AS tokens
FROM token_usage
WHERE occurred_at >= datetime('now', '-14 days')
GROUP BY day
ORDER BY day;
The first query answers the blame question. The second catches a failing feature that is burning tokens on retries. The third lets you forecast when the quota will run out: divide the remaining tokens by the average daily burn.
The table below summarizes four common allocation policies you can enforce with the ledger data. Pick one before the quota becomes scarce.
| Allocation policy | How it works | Best for | Failure mode if ignored |
|---|---|---|---|
| Equal per project | Cap each project at a fixed share of the quota | Teams with several independent prototypes | One project starves the rest |
| Priority reserved | Keep a reserve for high-value features; let the rest share the remainder | A single flagship feature plus experiments | The flagship hits zero first |
| Feature flag limit | Pause low-priority features at a threshold | Teams that ship behind flags | No kill switch exists at the threshold |
| Time-window budget | Allow a fixed token amount per day, roll over unused | Batch jobs or CI runs | A weekend spike exhausts the window |
The policy is not the point. The point is that the ledger gives you the data to choose a policy instead of guessing.
Limits and who should not use this
The free server is likely a shared, best-effort slot. A clean ledger does not make the provider stable. Quotas and free-server availability can change, so do not treat the ledger as committed capacity. Do not send private data. Do not grant write authority based on token attribution alone; correctness and permissions need their own checks.
Skip this approach if you need p99 below two seconds for user-facing workloads, if the route handles regulated data, or if your team cannot tolerate provider terms changing. Use a committed endpoint or a self-hosted model instead. The free server is most useful for read-only shadow traffic, contract tests, and failure-state discovery before you pay for committed capacity.
If your team is sharing a free token quota, stop tracking it as one number. Add a project and feature column to the first few calls, run the weekly query, and see which feature consumes the most. That observation is more actionable than the headline quota.
Top comments (0)