It was 11:47 PM. My agent loop was still running. It had started at 9:00 AM. Fourteen hours of retrying on a free server.
The server cost zero dollars. The debugging cost a full day. This letter is for my past self. It is also for you.
A trending DEV discussion this week asked who tests the AI reviewer. My answer is short. Run the loop. Watch it fail. Fix the loop before you trust its output.
I use free infrastructure every week. I am not here to scare you away. I am here to save you the day I lost.
The setup
The stack was minimal. One free model endpoint. One free server. One small agent loop.
The endpoint and server came from MonkeyCode's open source project. It offers free model access and a free server option. At the time of writing, the free tier includes a 10M token allowance. Quotas change. Read the README before you build.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The workflow below is provider-agnostic. It works with any free endpoint.
I made three mistakes. Each cost hours. All three are avoidable.
Mistake 1: I skipped the probe
I assumed the endpoint was reachable. It was not. My code failed for three hours. I blamed my own logic. The endpoint had been down the whole time.
The fix is a 30-second probe. Run it before any real work. If it fails, stop. Do not debug your code first.
# probe.sh - verify endpoint and server before the agent loop
ENDPOINT="${MODEL_ENDPOINT:?set MODEL_ENDPOINT}"
TOKEN="${MODEL_TOKEN:?set MODEL_TOKEN}"
curl -sS -o /dev/null -w "endpoint http=%{http_code} time=%{time_total}s\n" \
-X POST "$ENDPOINT" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"ping"}]}'
curl -sS -o /dev/null -w "server http=%{http_code}\n" \
"${FREE_SERVER_URL:-http://127.0.0.1:8000}/health"
One command checks both sides. 200 means continue. Anything else means stop and wait.
Mistake 2: I built an unbounded loop
My agent never said "done". It just retried. Every retry called the model. The token allowance drained all afternoon.
The fix has three boundaries. A max step count. A time budget. A stop file.
# boundaries.py - three kill switches for any agent loop
import os
import time
from pathlib import Path
MAX_STEPS = int(os.getenv("MAX_STEPS", "5"))
TIME_BUDGET_S = int(os.getenv("TIME_BUDGET_S", "900"))
STOP_FILE = Path(os.getenv("STOP_FILE", "./STOP"))
def dead_reason(step: int, started_at: float):
if STOP_FILE.exists():
return f"{STOP_FILE} present"
if step >= MAX_STEPS:
return f"MAX_STEPS={MAX_STEPS}"
if time.time() - started_at > TIME_BUDGET_S:
return f"TIME_BUDGET_S={TIME_BUDGET_S}"
return None
Any single boundary kills the loop. You always have an exit. You never wait four hours again.
Mistake 3: I trusted raw model JSON
The model returned valid JSON. The keys were wrong. My parser accepted it silently. Bad data flowed into my database.
The fix is schema validation. Validate every reply. On failure, feed the error back into the conversation. Let the model repair its own output.
# validation.py - reject silent schema drift
import json
REQUIRED_KEYS = {"action", "args"}
def validate_reply(raw: str) -> dict:
data = json.loads(raw) # raises on malformed JSON
missing = REQUIRED_KEYS - set(data)
if missing:
raise ValueError(f"missing keys: {sorted(missing)}")
return data
This turns a silent bug into a visible step. The loop logs it. The model sees the error. The next reply usually passes.
The full bounded loop
The complete script combines probe, boundaries, and validation. It uses only the Python standard library.
#!/usr/bin/env python3
# bounded_agent.py - free endpoint + free server, with a kill switch
import json
import os
import sys
import time
import urllib.request
from pathlib import Path
MAX_STEPS = int(os.getenv("MAX_STEPS", "5"))
TIME_BUDGET_S = int(os.getenv("TIME_BUDGET_S", "900"))
STOP_FILE = Path(os.getenv("STOP_FILE", "./STOP"))
ENDPOINT = os.environ["MODEL_ENDPOINT"] # free model endpoint
TOKEN = os.environ["MODEL_TOKEN"]
REQUIRED_KEYS = {"action", "args"}
def call_model(messages):
body = json.dumps({"messages": messages}).encode()
req = urllib.request.Request(
ENDPOINT,
data=body,
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())["message"]["content"]
def validate_reply(raw):
data = json.loads(raw)
missing = REQUIRED_KEYS - set(data)
if missing:
raise ValueError(f"missing keys: {sorted(missing)}")
return data
def run(task):
messages = [{"role": "user", "content": task}]
start = time.time()
for step in range(MAX_STEPS):
if STOP_FILE.exists():
print("stop: STOP file found")
return "stopped"
if time.time() - start > TIME_BUDGET_S:
print("stop: time budget exceeded")
return "stopped"
raw = call_model(messages)
try:
reply = validate_reply(raw)
except (ValueError, json.JSONDecodeError) as exc:
print(f"[step {step}] invalid reply: {exc}")
messages.append({
"role": "user",
"content": (
f"Your reply failed validation: {exc}. "
f"Return valid JSON with keys {sorted(REQUIRED_KEYS)}."
),
})
continue
print(f"[step {step}] action={reply['action']} args={reply.get('args')}")
if reply["action"] == "done":
return reply.get("args")
print("stop: max steps reached")
return "stopped"
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("usage: bounded_agent.py '<task>'")
print(run(sys.argv[1]))
The loop repeats one bounded unit of work. Check the kill switches. Call the model. Validate the reply. Log the action. If validation fails, the error becomes the next prompt. The loop stays honest.
Adjust the request format to your provider's documented API. The loop pattern stays the same.
How to run it
- Export credentials.
export MODEL_ENDPOINT=...andexport MODEL_TOKEN=.... SetFREE_SERVER_URLif you use the free server. - Run the probe. Confirm both checks return 200.
- Run a one-step test.
MAX_STEPS=1 python3 bounded_agent.py "Return done with args {}". - Test the kill switch.
touch STOP && python3 bounded_agent.py "Do anything". The script exits immediately. - Run the real task with tight limits.
MAX_STEPS=3 TIME_BUDGET_S=120 python3 bounded_agent.py "Find the bug in parse.py".
The test sequence takes five minutes. It catches the three failures that cost me a day.
When this setup is the right call
| Scenario | Free endpoint + free server | Local or paid |
|---|---|---|
| Prototype to validate an idea | Fits | Overkill |
| Learning agent loop patterns | Fits | Not needed |
| Tolerates cold starts and restarts | Fits | — |
| Fixed deadline, batch workload | Risk | Fits |
| Regulated or sensitive data | Never | Fits |
| Production traffic with an SLA | Never | Fits |
The table is a judgment guide. It is not a benchmark. Use it before you wire anything.
Limitations and who should skip this
Free servers have real constraints. Cold starts are common. Shared resources fluctuate. Persistence is not guaranteed. Token allowances are finite.
Treat the free server as a sandbox. Treat the allowance as fuel. Both run out.
Skip this approach for secrets, regulated data, or production traffic. Skip it for jobs with a hard deadline. Use the same pattern locally with a paid endpoint instead.
The letter ends here
Probe before you build. Bound every loop. Validate every reply.
You will still hit surprises. You will not lose a whole day. That is the only outcome that matters.
I ran this exact sequence on MonkeyCode's free model endpoint and free server. The README lists the current allowance and server details. Run the five-minute test before you trust any loop. Keep the first failure log. That log is the real tutorial.
Top comments (0)