DEV Community

Odd_Background_328
Odd_Background_328

Posted on

Stand Up an Agent Decision Ledger on Free Infrastructure: A Verified Walkthrough

Your agent decided something at 09:14. By 09:16 it forgot. It asked the model the same question again. You paid twice for the same answer.

That is the memory problem behind the reasoning-ledger discussions on DEV this week. The fix is not a fancier agent. The fix is a database.

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

MonkeyCode is an open-source project. It offers free model access and a free server option for agent workloads. This walkthrough uses both. You build a decision ledger. Every stage has a verification step. Nothing here needs a credit card.

What you build

ledger_agent.py is a single-file Python service. It does three things:

  • Hashes each prompt.
  • Checks SQLite for a prior decision.
  • Calls a model only on a cache miss.

The ledger stores every decision. It also stores failures. That gives you replay, audit, and a rate-limit signal.

Stage 1: Schema first

Create the table before you write any logic.

def init_db():
    con = sqlite3.connect("ledger.db")
    con.execute("""
        CREATE TABLE IF NOT EXISTS decisions (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            input_hash TEXT UNIQUE,
            decision TEXT,
            model TEXT,
            status TEXT,
            latency_ms INTEGER,
            created_at TEXT DEFAULT (datetime('now'))
        )
    """)
    con.commit()
    return con
Enter fullscreen mode Exit fullscreen mode

Verify:

$ python3 -c "import ledger_agent; ledger_agent.init_db()"
$ sqlite3 ledger.db ".schema decisions"
Enter fullscreen mode Exit fullscreen mode

Expected output is the CREATE TABLE statement. If you see it, the storage layer works.

Stage 2: Decide with a cache

The core function is short.

def decide(prompt):
    h = hashlib.sha256(prompt.encode()).hexdigest()
    con = init_db()
    row = con.execute(
        "SELECT decision FROM decisions WHERE input_hash = ? AND status = 'ok'",
        (h,),
    ).fetchone()
    if row:
        return {"source": "cache", "decision": row[0]}
    t0 = time.time()
    try:
        decision = ask_model(prompt)
        status = "ok"
    except Exception as exc:
        con.execute(
            "INSERT INTO decisions (input_hash, decision, model, status, latency_ms) VALUES (?, ?, ?, ?, ?)",
            (h, f"error: {exc}", MODEL, "failed", int((time.time() - t0) * 1000)),
        )
        con.commit()
        raise
    con.execute(
        "INSERT INTO decisions (input_hash, decision, model, status, latency_ms) VALUES (?, ?, ?, ?, ?)",
        (h, decision, MODEL, status, int((time.time() - t0) * 1000)),
    )
    con.commit()
    return {"source": "model", "decision": decision}
Enter fullscreen mode Exit fullscreen mode

Cache hits never touch the model. Failures are recorded, not hidden.

Add a minimal HTTP layer. You need /health and /decide.

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/health":
            rows = init_db().execute("SELECT count(*) FROM decisions").fetchone()[0]
            self.send_json({"status": "ok", "ledger_rows": rows})

    def do_POST(self):
        if self.path == "/decide":
            length = int(self.headers["Content-Length"])
            prompt = json.loads(self.rfile.read(length))["prompt"]
            try:
                self.send_json(decide(prompt))
            except Exception:
                self.send_response(503)
                self.end_headers()

    def send_json(self, obj):
        body = json.dumps(obj).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(body)
Enter fullscreen mode Exit fullscreen mode

Stage 3: Test against a mock before real tokens

Do not burn free tokens on wiring mistakes. Run a local mock model first.

# mock_model.py
from http.server import BaseHTTPRequestHandler, HTTPServer
import json, time

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        time.sleep(0.2)
        body = {"choices": [{"message": {"content": "mock decision"}}]}
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(body).encode())
    def log_message(self, *a):
        pass

HTTPServer(("127.0.0.1", 9000), H).serve_forever()
Enter fullscreen mode Exit fullscreen mode

Start the mock. Start the agent pointed at it.

$ python3 mock_model.py &
$ MODEL_URL=http://127.0.0.1:9000/v1/chat/completions \
  MODEL_API_KEY=test \
  python3 ledger_agent.py &
$ curl -s -X POST localhost:8080/decide \
  -d '{"prompt":"should we retry the deploy?"}'
Enter fullscreen mode Exit fullscreen mode

First call returns {"source": "model", "decision": "mock decision"}. Second call with the same prompt returns {"source": "cache", ...}.

Verify the row:

$ sqlite3 ledger.db "SELECT input_hash, status, latency_ms FROM decisions;"
Enter fullscreen mode Exit fullscreen mode

One row. Status ok. The cache works.

Stage 4: Wire in MonkeyCode's free model access

Point the same code at MonkeyCode's free model endpoint. Set MODEL_URL and MODEL_API_KEY from your MonkeyCode account. The current endpoint is in the project README.

Send one real prompt. Check the ledger again.

$ sqlite3 ledger.db "SELECT model, status, latency_ms FROM decisions ORDER BY id DESC LIMIT 1;"
Enter fullscreen mode Exit fullscreen mode

You should see the model name and a real latency. If the row says failed, read the error. The ledger just debugged your integration.

Track token usage on the provider side. The ledger tracks latency and status. Compare both after the drill.

Stage 5: Deploy to the free server

The free server option runs the same file. It exposes the service at a public URL. See the project docs for the current deploy command.

$ curl -s https://<your-free-server-url>/health
Enter fullscreen mode Exit fullscreen mode

Expected: {"status": "ok", "ledger_rows": 2}. The server works.

Stage 6: Failure drill

Free endpoints rate-limit. See that failure before it hits you.

Change the mock to return 429. Point the agent at it. Send ten requests.

self.send_response(429)
self.send_header("Retry-After", "30")
Enter fullscreen mode Exit fullscreen mode

Expected behavior:

  • New prompts get a 503 from the API.
  • The ledger records status = 'failed'.
  • Cached prompts still return from SQLite.

Check the failure rows:

$ sqlite3 ledger.db "SELECT status, count(*) FROM decisions GROUP BY status;"
Enter fullscreen mode Exit fullscreen mode

You now know what a saturated endpoint looks like in your own telemetry.

Thresholds

Use the ledger as a signal source. Start with this table.

Signal Threshold Action
Cache hit ratio below 60% over 5 min prompts too diverse; cache is useless
p95 decide latency above 5 s endpoint saturated; add a queue
Failed rows above 5% of recent rows stop direct calls; switch to fallback

Pick numbers for your workload. Write them in your runbook.

Cleanup and rollback

$ sqlite3 ledger.db "DROP TABLE decisions;"
$ kill %1 %2   # stop agent and mock
Enter fullscreen mode Exit fullscreen mode

Rollback is simpler. Revoke the API key. Delete the free server instance. Nothing else persists.

Limitations

The 10M-token free allowance and the free server are the current offer as of 2026-08-22. Quotas change. Verify the numbers in the project docs before you depend on them.

The free tier is for experiments and light workloads. It is not for hard latency SLOs, regulated data, or guaranteed capacity. Budget for paid infrastructure if you need those.

Who should not use this

Skip this setup if your team needs a production SLA today. Skip it if you cannot tolerate a 503 during model outages. Skip it if your prompts are so unique that caching never hits.

For everyone else: the ledger gives you replay, audit, and an early rate-limit warning. That is a lot of signal from one SQLite table.

Try the same walkthrough with MonkeyCode's free tier this week. The ledger will tell you, with rows and timestamps, whether the free path fits your workload.

Top comments (0)