Last week, a DEV thread asked: "What do you do while AI codes?" My answer: watch the meter. Because last month, I stopped watching for one afternoon.
I pointed an agent at a free model endpoint. The endpoint came with a token allowance and a free server for testing. The agent failed on a malformed payload. So it retried. Then it retried again. Forty calls later, the allowance was gone. The agent did not care. The server did not care. Only my calendar did.
This is a letter to the me who built that stack without a gate. If you are about to do the same, read it.
Disclosure
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option for experiments. I used both to test the pattern below. The code is generic. It works with any OpenAI-compatible chat completions endpoint.
Why an agent needs a meter
Agents treat context as free. They do not see quotas. They do not see billing cycles. They only see errors and timeouts.
A human sees the bill. That is the missing control.
Recent threads ask why "AI promoted every developer to reviewer." My review found the real gap: nobody reviews spending. We review code, not token burn.
The budget gate
The solution is a small reverse proxy. It sits in front of the model API. It checks the daily budget before every call. It returns 402 when the budget is exceeded. Then it records real usage after the call.
This is not magic. It is a middleman.
Here is the full gate in Python. It uses FastAPI and SQLite.
# budget_gate.py
import os
import sqlite3
import datetime
import httpx
from fastapi import FastAPI, Request
app = FastAPI()
DB = os.getenv("USAGE_DB", "usage.db")
DAILY_LIMIT = int(os.getenv("DAILY_TOKEN_LIMIT", "100000"))
UPSTREAM = os.getenv("UPSTREAM_URL")
if not UPSTREAM:
raise RuntimeError("Set UPSTREAM_URL to your model's chat completions URL")
def _db():
con = sqlite3.connect(DB)
con.execute("CREATE TABLE IF NOT EXISTS usage (day TEXT, tokens INTEGER)")
return con
def used_today() -> int:
con = _db()
row = con.execute(
"SELECT COALESCE(SUM(tokens), 0) FROM usage WHERE day = ?",
(datetime.date.today().isoformat(),),
).fetchone()
con.close()
return int(row[0])
def estimate(payload: dict) -> int:
chars = sum(
len(m.get("content", ""))
for m in payload.get("messages", [])
)
return max(1, chars // 4)
@app.post("/v1/chat/completions")
async def gate(request: Request):
payload = await request.json()
guessed = estimate(payload)
if used_today() + guessed > DAILY_LIMIT:
return {
"error": {
"message": "daily token budget exceeded",
"used": used_today(),
"limit": DAILY_LIMIT,
}
}, 402
headers = {"Authorization": request.headers.get("Authorization", "")}
async with httpx.AsyncClient() as client:
upstream = await client.post(UPSTREAM, json=payload, headers=headers)
data = upstream.json()
usage = data.get("usage", {})
tokens = usage.get("total_tokens", guessed)
con = _db()
con.execute(
"INSERT INTO usage (day, tokens) VALUES (?, ?)",
(datetime.date.today().isoformat(), tokens),
)
con.commit()
con.close()
return data
How to run it
- Save the file as
budget_gate.py. - Install dependencies:
pip install fastapi uvicorn httpx. - Set the real endpoint:
export UPSTREAM_URL="https://your-provider.example/v1/chat/completions". - Set a sane daily cap:
export DAILY_TOKEN_LIMIT=50000. - Start the gate:
uvicorn budget_gate:app --host 0.0.0.0 --port 8000. - Point your agent at
http://localhost:8000/v1.
The agent never sees a normal reply after a 402. Most agents stop on HTTP errors. The retry loop dies before the budget does.
Test the gate
Run two calls. The first should pass. Then lower the limit to one token. The second should return 402.
curl -s http://localhost:8000/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer test" -d '{"messages":[{"role":"user","content":"ping"}],"max_tokens":5}'
sqlite3 usage.db "SELECT SUM(tokens) FROM usage WHERE day = date('now');"
Debugging the retry loop
When your agent retries, don't patch the agent. Check the boundary. Run the same call with curl. If curl works, your agent sends a different payload. If curl fails, inspect the status code.
-
402means budget exhausted. Increase the cap or wait for reset. -
400means malformed payload. Fix the prompt schema. -
429means rate limited. Sleep before retry. -
401means bad key. Rotate it.
The free server temptation
The free server removes local GPU pain. It also hides the burn. You no longer watch your fan spin. You watch a dashboard only if you build one. This gate is that dashboard.
When this gate makes sense
| Workload | Use gate? | Why |
|---|---|---|
| Interactive coding | Yes | Agents retry on any error. |
| Batch processing | Maybe | A queue with a stop condition may be cleaner. |
| One-off script | Yes | It costs 30 seconds to add. |
| Production traffic | No | You need a real rate limiter and auth. |
Limitations
The estimate is crude. It divides characters by four. English tokens average roughly four characters. Code and JSON vary.
SQLite is fine for one user. A team server needs Postgres or Redis.
This gate checks tokens only. It does not block filesystem access, tool calls, or prompt injection. It is not a security boundary.
Who should not use this
Skip the gate if your agent is a single curl. Skip it if your company already has a proxy. Skip it if you need sub-millisecond latency.
A Python middleman adds maybe 10 milliseconds. That matters only for tight loops.
The letter I would send back
Dear past me: the free tier is not a license to forget. The agent will not stop. The server will not stop. Only you can make the meter.
Run the gate. Set the cap. Check the log.
If you start a free MonkeyCode stack, bring this gate. It turns "free" into "metered free." Your future self will not burn a day on a quota error.
Top comments (0)