A batch agent job usually fails at the least convenient moment: the run has already written half its outputs, and the remote model endpoint returns a quota error. The scenario below is illustrative rather than a reported incident, but the shape is common enough to plan for. A repository maintenance job queues forty small prompts, sends them to a free remote endpoint overnight, and wakes up with eighteen completed files and no record of how many tokens were consumed. The local model on the same machine never ran, because the routing config said remote and the config was written last month.
The interesting part is not the quota exhaustion itself; it is the missing accounting. When a free tier is the budget, spending decisions have to happen before dispatch, not after the bill arrives. This article builds a small token ledger with a reserve, commit, and release cycle, then uses it to decide between a local path and a free remote path.
Where a config flag breaks down
A static provider: remote setting is a placement decision without an instrument attached to it. Three failure modes appear almost immediately in any multi-job workflow. First, concurrent workers each read the remaining budget, each decide there is room, and together overshoot the cap. Second, retries double-count silently, so a flaky endpoint consumes twice the planned tokens for a single logical job. Third, a job carrying a secret field takes the same code path as a public one, because the flag knows about providers and nothing about payloads.
A ledger fixes the first two problems structurally and gives the third problem a place to live. The idea is borrowed from payments: reserve funds before the transaction, settle after it, and release the hold when the transaction fails. The unit here is tokens, the ledger is a SQLite table, and the transaction is one model call.
Step 1 — Classify jobs before you count tokens
Token counting is the least reliable part of the design, so it should not be the first decision. Classification is cheaper and more stable: does the payload contain a secret, can the job tolerate local quality, and is there a network path at all. The routing table below captures the policy that the code will enforce.
| Condition | Path | Reason recorded |
|---|---|---|
| Payload carries a secret and policy is local-only | local | secret-policy |
| Host reports no network path | local | offline |
| Reservation rejected by the ledger | local | budget-exhausted |
| Reservation accepted | remote | budget-reserved |
The table is intentionally boring, because routing rules that cannot be explained in four rows tend to be rewritten every sprint. Note that the policy never asks whether the remote model is better; quality comparisons belong in an evaluation harness, not in a spending gate.
Step 2 — Reserve, then commit, then release
The ledger needs two tables and three operations. A budget row holds the cap, the amount spent, and the amount currently reserved. A job row records each reservation so that a crashed worker can be reconciled later. Writes use BEGIN IMMEDIATE so concurrent workers serialize on the same row instead of racing.
# ledger.py — reference implementation, standard library only
import sqlite3, time
from dataclasses import dataclass
SCHEMA = """
CREATE TABLE IF NOT EXISTS budget (
scope TEXT PRIMARY KEY,
cap INTEGER NOT NULL,
spent INTEGER NOT NULL DEFAULT 0,
reserved INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS job (
job_id TEXT PRIMARY KEY,
scope TEXT NOT NULL,
est INTEGER NOT NULL,
actual INTEGER,
state TEXT NOT NULL,
ts REAL NOT NULL
);
"""
class _Tx:
def __init__(self, db): self.db = db
def __enter__(self):
self.db.execute("BEGIN IMMEDIATE")
return self.db
def __exit__(self, exc_type, exc, tb):
self.db.execute("ROLLBACK" if exc_type else "COMMIT")
return False
class Ledger:
def __init__(self, path="budget.db"):
self.db = sqlite3.connect(path, isolation_level=None)
self.db.executescript(SCHEMA)
def open_scope(self, scope, cap):
self.db.execute(
"INSERT OR IGNORE INTO budget(scope, cap) VALUES (?, ?)", (scope, cap))
def reserve(self, job_id, scope, est):
with _Tx(self.db) as db:
cur = db.execute(
"UPDATE budget SET reserved = reserved + :est "
"WHERE scope = :scope AND spent + reserved + :est <= cap",
{"est": est, "scope": scope})
if cur.rowcount == 0:
return False
db.execute(
"INSERT INTO job(job_id, scope, est, state, ts) VALUES (?,?,?,?,?)",
(job_id, scope, est, "reserved", time.time()))
return True
def commit(self, job_id, actual):
row = self.db.execute(
"SELECT scope, est FROM job WHERE job_id=? AND state='reserved'",
(job_id,)).fetchone()
if row is None:
raise KeyError(f"no open reservation for {job_id}")
scope, est = row
with _Tx(self.db) as db:
db.execute(
"UPDATE budget SET reserved = reserved - ?, spent = spent + ? "
"WHERE scope = ?", (est, actual, scope))
db.execute(
"UPDATE job SET actual=?, state='committed', ts=? WHERE job_id=?",
(actual, time.time(), job_id))
def release(self, job_id):
row = self.db.execute(
"SELECT scope, est FROM job WHERE job_id=? AND state='reserved'",
(job_id,)).fetchone()
if row is None:
return
scope, est = row
with _Tx(self.db) as db:
db.execute(
"UPDATE budget SET reserved = reserved - ? WHERE scope = ?",
(est, scope))
db.execute(
"UPDATE job SET state='released', ts=? WHERE job_id=?",
(time.time(), job_id))
The important line is the WHERE clause on the reserve update. The condition spent + reserved + :est <= cap is evaluated inside the same statement that mutates the row, so two workers cannot both pass a check that only one should pass. The check and the write are the same operation, which is the property a config flag can never provide.
Step 3 — Route with the ledger, not with a flag
The routing function is small enough to review in one sitting. It returns a decision and a reason string, and the reason is what makes post-run debugging possible.
def route(job_id, est_tokens, carries_secret, ledger, scope, *, offline,
secret_policy="local-only"):
if carries_secret and secret_policy == "local-only":
return "local", "secret-policy"
if offline:
return "local", "offline"
if not ledger.reserve(job_id, scope, est_tokens):
return "local", "budget-exhausted"
return "remote", "budget-reserved"
The offline argument must come from an actual probe rather than a cached assumption, because laptops change networks more often than config files do. A cheap probe is a short connection attempt with a hard timeout, recorded alongside the job row. The secret check belongs here rather than inside the transport layer, since the transport is exactly the place where a secret would leave the host.
Step 4 — Reconcile estimates against reported usage
Token estimation without the provider tokenizer is approximate; a characters-divided-by-four heuristic is a starting point, not a measurement. The ledger absorbs that error by committing the usage number reported in the response instead of the reservation estimate. When a run finishes, the difference between reserved and spent should be zero on every scope, and any residual is a bug worth investigating.
Drift deserves an alarm. If the ratio of actual to estimated tokens exceeds roughly 1.25 across several consecutive jobs, the estimator is misconfigured for the workload, and the cap it enforces is optimistic. Recording both columns makes that visible without a separate metrics pipeline, which matters for small deployments that will never adopt one.
Step 5 — Recover reservations left behind by a crash
A worker that dies between reserve and commit leaves a hold on the budget forever. Startup recovery closes open reservations older than a time-to-live, and the job table keeps the evidence for review rather than deleting it. The recovery query is two statements: select stale reservations, then release them through the same method the normal path uses.
The honest caveat is that a crash after the remote call succeeded but before commit will under-count real usage. The ledger therefore behaves as a soft budget guard, not a billing system. When hard numbers matter, read the provider's own usage endpoint and reconcile it against spent, treating the local table as a safety margin rather than an invoice.
Step 6 — Prove the behavior with a fake transport
The test suite below needs no network and no credentials, which is what makes it runnable in review. Each scenario asserts a decision, and the failures are simulated through the transport rather than through mocking internals.
| Scenario | Injected fault | Expected outcome |
|---|---|---|
| Budget exhausted | cap at zero |
local with budget-exhausted
|
| Offline host | probe raises |
local with offline
|
| Transport error | ConnectionError |
reservation released, error re-raised |
| Rate limit | status 429 | reservation released, job requeued with backoff |
| Low estimate | actual equals twice estimate | commit records the reported number, ledger stays consistent |
| Worker crash | process killed after reserve | startup recovery releases the stale hold |
The low-estimate row is the one that catches the most real defects. A harness that only tests successful accounting will report green while a single prompt shape quietly doubles consumption.
Limitations and who should skip this
The approach carries real costs. Estimation stays approximate without the provider tokenizer, stale-hold recovery can under-count a completed call, and the local fallback path may produce different output quality that the ledger cannot detect. Teams that need billing-grade attribution across multiple tenants should use provider-side accounting and chargeback, not a SQLite file. Single-shot scripts and environments with unlimited paid capacity gain little from the extra machinery.
The ledger also does not replace rate-limit handling; a released reservation on a 429 is correct, but the retry policy and backoff belong to the caller. Finally, any free-tier figure is a moving target, so treat caps as configuration injected at deploy time rather than constants compiled into the code.
A practical starting point
For teams experimenting with free remote model access, the MonkeyCode project is one place to point the transport layer while the ledger handles the spending decision. The project's current outreach states free model access with a ten-million-token allowance and a free server option; both are operator-supplied claims for the time of writing, so confirm the live terms on the project's own pages before planning capacity around them. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The durable idea is smaller than any product: reserve before you spend, commit what was reported, release what failed, and record the reason for every path taken. A free tier runs out eventually, but a run that degrades to local with a readable reason is a much better outcome than a batch that dies at prompt nineteen.
Top comments (0)