The model is not the risky part — the missing stage gate is. I have swapped endpoints mid-week without touching a line of my loop, and I have also watched a "cheap" agent eat a whole free allowance in one afternoon because nothing between the model and my repo knew how to say stop.
So this is not a post about picking a model. It is a six-stage bootstrap where each stage has to prove something before the next one is allowed to run. In an earlier post I capped turns and gated git apply --check. This one starts further back: cold machine, no key, no server, no ledger.
What you will have at the end
- A repo where a leaked key is a failing test, not a bad day.
- A server that only listens on the ports you chose.
- An endpoint smoke test that returns
usagebefore any agent code exists. - A token ledger with an offline test suite and a hard abort.
- One measured task, expressed as tokens per accepted diff.
I am not promising speed, savings, or that any hosted endpoint stays free. Availability claims age fast. The operator describes MonkeyCode's free tier as 10 million free tokens plus a free server option; verify both on the project's current docs the day you build, and re-check before you depend on them.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Where a free hosted endpoint actually fits
Ask yourself one question before Stage 0: is this workload allowed to leave my machine at all?
| Workload | Free hosted endpoint | Why |
|---|---|---|
| Toy repo, public code, learning the loop | Yes | Failure is cheap and nothing sensitive moves |
| Refactor a private service | Only after a data review | Prompt text leaves your network; read the provider's retention terms |
| Secrets, keys, customer records | No | Redaction is a job of its own; do it first or not at all |
| Long unattended runs | Not yet | No SLA on a free tier means rate limits you cannot schedule around |
| Offline or air-gapped work | No | Local weights or nothing |
The pattern I keep landing on: free hosted capacity is excellent for the first 80% of a loop's development and poor for the last 20% of unattended operation. Build on it, then move the run, not the design.
Stage 0 — Freeze a baseline you can diff against
Before anything talks to a network, create a repo and record tool versions. If Stage 5 misbehaves, you want to know whether jq changed, not to guess.
mkdir -p ~/gated-agent && cd ~/gated-agent
git init -q
python3 -V && curl --version | head -n1 && jq --version
printf 'AGENT_LEDGER=ledger.jsonl\nMONKEYCODE_API_KEY=\nMONKEYCODE_MODEL=\n' > .env.example
printf '.env\nledger.jsonl\n__pycache__/\n' > .gitignore
git add .env.example .gitignore && git commit -qm "baseline"
Verify: git check-ignore -v .env must print a rule. If it prints nothing, your ignore file is wrong and the rest of this tutorial is a liability.
Stage 1 — Make a leaked key impossible to commit
Copy .env.example to .env, fill it in, and never echo it. I keep real model identifiers in MONKEYCODE_MODEL rather than hardcoded in scripts, because the endpoint shape is the thing I want to change cheaply.
cp .env.example .env && chmod 600 .env
set -a; . ./.env; set +a
git ls-files -z | xargs -0 grep -nE 'sk-[A-Za-z0-9]{16,}' || echo "no key-shaped strings tracked"
Verify: the grep line above must print the fallback message. Run it again after every commit; it takes under a second and it catches the one mistake that cannot be undone by force-pushing.
Stage 2 — Provision the free server and make it boring
Whatever image your provider hands you, the goal is the same: a non-root user, one open port, updates that happen without you. Replace $HOST with the hostname you were given.
ssh root@"$HOST" 'adduser --disabled-password --gecos "" agent && \
usermod -aG sudo agent && \
install -d -m 700 -o agent -g agent /home/agent/.ssh && \
cp /root/.ssh/authorized_keys /home/agent/.ssh/ && \
chown agent:agent /home/agent/.ssh/authorized_keys'
ssh agent@"$HOST" 'sudo ufw default deny incoming && \
sudo ufw allow OpenSSH && sudo ufw --force enable && sudo ufw status verbose'
Verify: ssh agent@"$HOST" 'ss -tulpn' should list SSH and essentially nothing else. Anything unexpected is a conversation, not a shrug.
Stage 3 — Smoke test the endpoint before you write an agent
Do not build the loop first. Send exactly one request with a 32-token cap and look at what comes back.
curl -sS --max-time 60 "$BASE_URL/chat/completions" \
-H "Authorization: Bearer $MONKEYCODE_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"model\":\"$MONKEYCODE_MODEL\",
\"messages\":[{\"role\":\"user\",\"content\":\"Reply with JSON only: {\\\"ok\\\":true}\"}],
\"max_tokens\":32}" \
| jq '{model, usage, content: .choices[0].message.content}'
That request shape assumes an OpenAI-compatible surface — check your provider's docs, because a wrong assumption here wastes an afternoon. Verify two things: the call succeeds, and usage is populated. If usage is missing, your ledger becomes an estimate, and you must label it as one.
Stage 4 — A token ledger with a testable abort
This is the only code in this post I would call non-negotiable. It appends one line per call and refuses to start the next one when the budget gets close.
# ledger.py — append-only spend log. Not executed against a live provider here;
# adapt the usage field names to whatever your endpoint returns.
import json, os, time
from pathlib import Path
def _path() -> Path:
return Path(os.environ.get("AGENT_LEDGER", "ledger.jsonl"))
def record(stage: str, usage: dict, accepted=None) -> dict:
row = {"ts": time.time(), "stage": stage,
"prompt_tokens": int(usage.get("prompt_tokens", 0)),
"completion_tokens": int(usage.get("completion_tokens", 0)),
"accepted": accepted}
with _path().open("a") as fh:
fh.write(json.dumps(row) + "\n")
return row
def spent() -> int:
p = _path()
if not p.exists():
return 0
return sum(sum(v for k, v in json.loads(line).items() if k.endswith("_tokens"))
for line in p.read_text().splitlines() if line.strip())
class BudgetExceeded(RuntimeError):
pass
def gate(budget: int, reserve: int = 2000) -> None:
if spent() + reserve >= budget:
raise BudgetExceeded(f"spent={spent()} reserve={reserve} budget={budget}")
The reserve matters more than the budget. Why? Because the call that crosses your limit is already paid for; the reserve is what stops the next twenty.
# test_budget.py — pytest, offline, no key, no network.
import pytest
from ledger import BudgetExceeded, gate, record, spent
def test_spent_sums_both_token_fields(tmp_path, monkeypatch):
monkeypatch.setenv("AGENT_LEDGER", str(tmp_path / "ledger.jsonl"))
record("stage-3-smoke", {"prompt_tokens": 120, "completion_tokens": 30})
record("stage-5-task", {"prompt_tokens": 900, "completion_tokens": 100})
assert spent() == 1150
def test_gate_aborts_inside_reserve(tmp_path, monkeypatch):
monkeypatch.setenv("AGENT_LEDGER", str(tmp_path / "ledger.jsonl"))
record("stage-5-task", {"prompt_tokens": 9000, "completion_tokens": 500})
with pytest.raises(BudgetExceeded):
gate(budget=10_000, reserve=2_000)
def test_gate_allows_call_with_room_left(tmp_path, monkeypatch):
monkeypatch.setenv("AGENT_LEDGER", str(tmp_path / "ledger.jsonl"))
gate(budget=10_000, reserve=2_000) # nothing spent yet
Verify: pytest -q passes with no environment variables set. A guard you cannot test offline is a guard you will disable the first time it gets in the way.
Stage 5 — One real task, measured in tokens per accepted diff
Pick a boring task: a failing test in a toy repo, a rename across five files, a linter fix. Run it, then call record(..., accepted=True) only when the diff survives your review and applies cleanly. Anything else is accepted=False.
The ratio you want is tokens spent per accepted diff. Mine looked like this in shape, though your numbers will differ — treat the bands as a decision aid, not a benchmark:
| Tokens per accepted diff | Reading | Next move |
|---|---|---|
| Low, stable across three tasks | The task is well scoped | Keep the free tier for this class of work |
| Rising run over run | Context is being re-sent, not reused | Shrink the prompt, split the task |
| Spiky, mostly rejected | The task is under-specified | Fix the spec before touching the model |
| Flat refusal or rate errors | Capacity, not quality | Pause; do not retry in a loop |
If three scoped tasks all land in the last row, stop optimizing the model. Something in Stages 1–4 is wrong.
Limitations, and who should not do this
- No SLA on free anything. Rate limits, model availability, and quota terms change without notice. Re-read them; the 10 million token figure and the free server option are the operator's claims, not a contract.
-
The ledger is approximate. Billing counts differ from API-reported usage, retries double-count, and a missing
usagefield makes every number an estimate. - Not for regulated or secret-bearing code. If you cannot answer "where does the prompt go and for how long is it kept?", the answer is no.
- Not for unattended overnight runs. A free tier plus no human gate equals a bill-shaped surprise or a silent stall.
- Skip this entirely if you need reproducibility guarantees, audit trails, or a fixed model version for compliance reasons. Those needs are legitimate and this workflow does not serve them.
The short version
Stage 0 freezes the baseline. Stage 1 makes leaks testable. Stage 2 shrinks the attack surface to one port. Stage 3 proves the endpoint returns usage. Stage 4 turns spend into a raised exception. Stage 5 turns spend into a ratio you can act on.
If you want to reach Stage 3 without provisioning anything yourself, MonkeyCode's free model access and free server option are the starting point the operator offered. The gates and the ledger are still yours to write — and honestly, they are the part that keeps working after the free tier changes.
Top comments (0)