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
Verify:
$ python3 -c "import ledger_agent; ledger_agent.init_db()"
$ sqlite3 ledger.db ".schema decisions"
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}
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)
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()
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?"}'
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;"
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;"
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
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")
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;"
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
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)