An agent that never stops looks a lot like an agent that is working. Rehearse the token ceiling on a free server with a hard stop rule so you meet BUDGET_EXHAUSTED in a test, not on a paid invoice.
The cursor moves. The logs scroll. The token meter climbs. The bug does not move. Would you notice before the bill does? I want a stop rule, not a spinner. Most teams meet the token ceiling in production. A free test allowance is a better place to meet it.
MonkeyCode ships a free server option and a 30,000,000-token test allowance. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The project is open source, so the server path is not a black box. I will show you one rehearsal you can keep. I point the same guard at a free model first, then at free models when I rotate the task mix, always on free servers so a failed stop rule costs nothing.
I do not name a model here. The model name changes faster than this article. Your console is the source of truth. Token accounting still matters when the vendor page changes; the public OpenAI tokenizer and tiktoken are useful if you want an independent count of prompt size before the first call.
Put the stop rule in front of the prompt
A model can be slow, verbose, or stuck. Those look different from the outside. I separate them before the first call. A spinner is a UI. A stop rule is a contract.
| Condition | Trigger | Action |
|---|---|---|
| Token ceiling | cumulative usage passes 30,000,000 | exit with BUDGET_EXHAUSTED |
| Dry run | no token use for 12 minutes | exit with NO_PROGRESS |
| Destructive action | delete, force push, schema drop | refuse and request a human owner |
You do not need more rules. You need these rules before the first request.
Compare that with the default agent loop most teams ship: retry on timeout, retry on empty output, retry on a tool error. Each retry looks like progress. Each retry spends tokens. Without a ceiling, a verbose free model and a paid model fail the same way—they keep talking. The difference is who pays. I put the ceiling in process memory, not in a dashboard I might ignore at 2 a.m.
I also refuse destructive actions in this rehearsal. Summarizing commits is reversible. Dropping a schema is not. The stop rule is not a permission system, but it is the first gate. If the guard cannot refuse a delete, it is not ready for a paid workflow.
Write a guard you can run from any shell
I keep this file in the root of every agent experiment. It talks to whatever gateway the free server console gives me. The transport can change. The budget math cannot.
import os
import time
from openai import OpenAI
client = OpenAI(
base_url=os.environ['MODEL_GATEWAY_URL'],
api_key=os.environ['MODEL_GATEWAY_KEY'],
)
MODEL = os.environ['MODEL_GATEWAY_MODEL']
BUDGET = int(os.environ.get('TOKEN_BUDGET', '30_000_000'))
STALL_LIMIT = int(os.environ.get('STALL_SECONDS', '720'))
used = 0
last_mark = time.time()
audit = []
def guard(prompt):
global used, last_mark
if used + 4096 > BUDGET:
raise SystemExit('BUDGET_EXHAUSTED used=' + str(used) + ' budget=' + str(BUDGET))
reply = client.chat.completions.create(
model=MODEL,
messages=[{'role': 'user', 'content': prompt}],
temperature=0,
)
used += reply.usage.total_tokens
last_mark = time.time()
audit.append({'used': used, 'at': last_mark})
if time.time() - last_mark > STALL_LIMIT:
raise SystemExit('NO_PROGRESS after ' + str(STALL_LIMIT) + ' seconds')
return reply.choices[0].message.content
if __name__ == '__main__':
task = os.environ.get('TASK', 'Return the word ok.')
print(guard(task))
print(audit[-1])
If your free server uses a different protocol, keep the guard and swap the transport. The decision rules do not change. I check used + 4096 > BUDGET before the call so a single oversized completion cannot sneak past the ceiling. The 4,096 headroom is a local safety margin, not a product limit. Rate-limit behavior still belongs to the provider; OpenAI's rate-limit guide is a useful contrast because HTTP 429 is not the same as BUDGET_EXHAUSTED. One is the vendor protecting the API. The other is you protecting the invoice.
Environment variables keep the rehearsal portable across a free server and later paid runs:
-
MODEL_GATEWAY_URL— the free server endpoint from your console -
MODEL_GATEWAY_KEY— the key the console issues -
MODEL_GATEWAY_MODEL— whatever free model the console lists today -
TOKEN_BUDGET— default 30,000,000, overridden in cheap failure tests -
STALL_SECONDS— default 720, so a silent loop dies in twelve minutes
Prove the guard fails cheap, then run a reversible task
Do not test a 30,000,000-token ceiling with 30,000,000 tokens. Fake it. A rehearsal on free servers exists so the failure mode is cheap, visible, and repeatable.
- Run
TOKEN_BUDGET=4096 python agent_guard.pyand confirm a normal prompt either completes under the fake ceiling or exits cleanly. - Run
TOKEN_BUDGET=1 python agent_guard.py. The tiny budget should exit withBUDGET_EXHAUSTED, not hang. - Time a dry run with
STALL_SECONDS=2if you need to proveNO_PROGRESSwithout waiting twelve minutes.
A hanging spinner is a failed test. BUDGET_EXHAUSTED on a one-token budget is a passed test. That contrast is the whole point: production bills hide behind motion, and a fake ceiling does not.
Then pick a reversible task on the free server. I start with something that can be rolled back. Summarize three commits into release notes. A bad answer is annoying. It is not destructive. Set the environment from the MonkeyCode free server console. Then run the guard once.
TASK='Summarize three commits into release notes.' MODEL_GATEWAY_URL='your-console-url' MODEL_GATEWAY_KEY='your-key' MODEL_GATEWAY_MODEL='your-model' python agent_guard.py
You want a line that shows used and a stop reason. The stop reason matters more than the token total. If the process returns to the shell with BUDGET_EXHAUSTED or a printed audit row, the rehearsal worked. If the cursor keeps blinking, the guard is not in front of the prompt yet.
Read the evidence, not the vibe
A token total is not progress. A stop reason is progress. I keep a review card with the exact fields the team needs to approve next time.
Review card
Task: summarize three commits
Tools requested: read_file
Tokens used: 87,214
Stop reason: complete
Human action: approve patch
This is the hand-back. A staff reviewer can read the card without watching the cursor. A screen-reader user can skim the same text. Nobody has to trust the model.
I treat the card as the only artifact that can promote a workflow from a free model rehearsal to a paid run. If the card is missing a stop reason, the run did not finish—it faded. If the card shows BUDGET_EXHAUSTED on a summary task, I shrink the prompt or the tool list before I ever attach a billing account. Compare that with watching a live trace: a trace is entertainment. A card is a decision.
What this rehearsal cannot prove
A free server proves your stop rule, not your model. Token use varies by task mix. Latency can look like a stall under load. A quiet model is not a safe model. Free models and a paid model can share a tokenizer and still diverge on verbosity, so I never treat one successful summary as a forecast of production spend.
Do not run a destructive task until you have a rollback plan and a human owner. Do not treat the 30,000,000-token allowance as a benchmark. It is a training floor. The allowance exists so you can afford to fail the guard on purpose. It does not mean a production agent should ever approach that number.
So start with a stop rule. Put agent_guard.py in front of the first free request on a free server. I would rather meet BUDGET_EXHAUSTED in a rehearsal than on a paid invoice. Clone the guard, fake a tiny TOKEN_BUDGET, and do not ship the paid workflow until the stop reason prints.
MonkeyCode provides free models that can run this workflow.
Top comments (0)