DEV Community

Dakota Liu
Dakota Liu

Posted on

From Zero to a Gated Agent Loop: Six Bootstrap Stages, a Smoke Test, and a Token Ledger

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 usage before 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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 usage field 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)