DEV Community

Riley Wang
Riley Wang

Posted on

Your Agent Is Leaking Tokens. Build a Decision Ledger on a Free Server.

Your agent loops on the same tool call. It rephrases. It retries. Tokens vanish.

No crash. No error. Just a silent budget leak.

The current agent conversation keeps circling one idea: remember decisions, not just data. Raw logs tell you what happened. A decision ledger tells you why. This tutorial turns that idea into a runnable pattern.

You will build a self-auditing agent on a free server. Every step ends with a verification check. The final artifact is a SQLite ledger with a hard token budget.

What You Need

  • MonkeyCode is an open-source project. Its current free tier includes a 10M token allowance and a free server option. Quotas change, so verify the numbers in your dashboard before you build.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

  • Python 3.10+ on the server.
  • requests and the standard library sqlite3.

The design is endpoint-agnostic. You will adapt one function to the API contract in the project docs.

Step 1: Provision the Free Server

MonkeyCode's free server option gives you a Linux box. The dashboard shows the SSH command.

ssh root@<server-ip>
python3 --version
apt update && apt install -y python3-venv python3-pip
pip install requests
Enter fullscreen mode Exit fullscreen mode

Verify: python3 --version prints 3.10 or newer. python3 -c "import requests, sqlite3" exits without errors.

Step 2: Set API Credentials

The dashboard exposes three values: an API key, a base URL, and a model name. Export them as environment variables.

export MC_API_KEY="<from-dashboard>"
export MC_BASE_URL="<from-dashboard>"
export MC_MODEL="<from-dashboard>"
export MC_BUDGET_TOKENS="100000"
Enter fullscreen mode Exit fullscreen mode

Verify: echo ${MC_API_KEY:?} ${MC_BASE_URL:?} ${MC_MODEL:?} prints all three values. No empty strings.

Step 3: Write the Ledger Agent

Create agent.py. The loop is simple: ask the model for an action, run the tool, record the decision, check the budget.

import json
import os
import sqlite3
import time

import requests

API_KEY = os.environ["MC_API_KEY"]
BASE_URL = os.environ["MC_BASE_URL"]
MODEL = os.environ["MC_MODEL"]
BUDGET = int(os.environ.get("MC_BUDGET_TOKENS", "100000"))
DB = "ledger.db"


def init_db():
    con = sqlite3.connect(DB)
    con.execute("""
        CREATE TABLE IF NOT EXISTS decisions (
            id INTEGER PRIMARY KEY,
            ts REAL,
            step INTEGER,
            action TEXT,
            tool_input TEXT,
            tokens_in INTEGER,
            tokens_out INTEGER,
            total_used INTEGER
        )
    """)
    con.commit()
    return con


def call_llm(messages):
    # Adapt auth and payload to the endpoint contract in the project docs.
    resp = requests.post(
        f"{BASE_URL}/chat/completions",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={"model": MODEL, "messages": messages},
        timeout=60,
    )
    resp.raise_for_status()
    data = resp.json()
    usage = data.get("usage", {})
    tokens_in = usage.get("prompt_tokens", 0)
    tokens_out = usage.get("completion_tokens", 0)
    return data["choices"][0]["message"], tokens_in, tokens_out


def search_tool(query):
    # Replace with a real tool. Returns a canned result.
    return f"results for {query}: 3 items, none critical"


def main():
    con = init_db()
    used = 0
    step = 0
    messages = [
        {"role": "system", "content": (
            "You are a cautious operator. Return JSON only: "
            '{"action": "search" | "stop", "query": "..."}'
        )}
    ]
    while used < BUDGET:
        step += 1
        msg, tokens_in, tokens_out = call_llm(messages)
        used += tokens_in + tokens_out
        try:
            action = json.loads(msg["content"])
        except json.JSONDecodeError:
            break
        query = action.get("query", "")
        if action.get("action") == "stop":
            con.execute(
                "INSERT INTO decisions VALUES (?,?,?,?,?,?,?,?)",
                (None, time.time(), step, "stop", query,
                 tokens_in, tokens_out, used),
            )
            con.commit()
            break
        result = search_tool(query)
        messages.append({"role": "assistant", "content": msg["content"]})
        messages.append({"role": "tool", "content": result})
        con.execute(
            "INSERT INTO decisions VALUES (?,?,?,?,?,?,?,?)",
            (None, time.time(), step, action.get("action"), query,
             tokens_in, tokens_out, used),
        )
        con.commit()
        print(f"step={step} used={used} action={action.get('action')}")
    con.close()
    print(f"final_used={used}")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Verify: python3 agent.py prints increasing used values. The script stops when the model returns "action": "stop".

Step 4: Enforce the Hard Budget

The while used < BUDGET condition is the enforcement point. When the budget is exhausted, the loop exits. No tool call runs after that.

Test it with a tiny budget:

MC_BUDGET_TOKENS=500 python3 agent.py
Enter fullscreen mode Exit fullscreen mode

Verify: final_used is less than or equal to 500. The script never exceeds the cap.

Step 5: Read the Decision Ledger

The ledger answers questions raw logs cannot. Query it with sqlite3:

sqlite3 ledger.db \
  "SELECT step, action, tool_input, tokens_in, tokens_out, total_used \
   FROM decisions ORDER BY step;"
Enter fullscreen mode Exit fullscreen mode

Verify: every row has a monotonic total_used. The final row matches final_used from the script output.

Why This Pattern Works

A hard budget is a constraint, not a punishment. It forces the agent to stop and reconsider. The ledger makes the reasoning visible after the fact.

The ledger is written before the next step runs. That makes it enforcement, not forensics. Two failure modes disappear:

  • Looping: repeated tool calls with the same tool_input are obvious in the query output.
  • Silent overruns: total_used is checked before every step, not after the damage.

Limitations

  • The 10M token figure is current as of this writing. Free quotas change. Re-check before you rely on them.
  • The ledger trusts the endpoint's usage field. If the endpoint omits it, both values default to zero. Add a character-based fallback if your API does not report usage.
  • This is a single-process design. No concurrency, no retries, no queue.
  • The tool is a stub. Replace search_tool with a real integration and add error handling.

Who Should Not Use This

  • Teams that need a latency SLA or guaranteed uptime. A free server has no contract.
  • High-throughput production workloads. The SQLite write per step becomes a bottleneck.
  • Sensitive data pipelines without a review of where the server and API sit.

Try It Against a Real Allowance

The fastest way to validate the budget logic is to compare it with a real allowance. The MonkeyCode dashboard shows your remaining tokens. Run the script, then compare the ledger total with the dashboard number. If they disagree, your endpoint's usage reporting is the first suspect.

MonkeyCode provides free models that can run this workflow.

Top comments (0)