The plan I wrote on a Tuesday night
I had a webhook receiver that worked on my laptop and no real idea what would happen once it lived somewhere else for two days. The job was small on purpose: accept raw issue text, ask a model for a structured triage record, store the record, and never lose one. I wanted the boring parts to break early, while the blast radius was one internal tool and zero customers.
So I set three rules for the 48 hours and promised myself I would not bend them the moment something looked fine.
- Run the extraction step against MonkeyCode's free model access, so a bad prompt costs me seconds instead of an invoice.
- Host the receiver on MonkeyCode's free server option instead of a paid box I would forget to shut down on Friday.
- Commit every script, fixture, and check to the repository so somebody else can rerun the whole loop in ten minutes.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. Everything below comes from my own harness, and I have deliberately left out quotas, hardware, model names, and run durations, because those are operator-supplied availability claims that age faster than articles do. Check the current product docs before you plan capacity around them.
One more honesty note before the details: I am writing these as the checks that caught me, not as a claim about how any platform behaves for every user. Your free tier may differ, and the scripts are how you find out.
Break one: the answer arrived wrapped in a fence
The first failure was almost funny, because nothing was actually broken except my parser. I asked for a JSON object, and the model politely returned a JSON object with a sentence in front of it and a helpful sentence behind it. Run this yourself and watch json.loads give up on character one.
raw = """Here is the triage record you asked for:
```json
{"title": "Disk full on worker-3", "severity": "high"}
```
Tell me if you want it as YAML instead."""
import json
json.loads(raw) # JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Is that a reasoning failure? No, it is a formatting failure, and formatting failures belong in my code rather than in a support ticket. I stopped treating the model as a function that returns a dict and started treating it as a text producer behind a contract I enforce.
The repair function I now ship first
This is the whole trick: look for a fenced block, fall back to the raw text, then take the outermost braces and try to parse that slice. It is defensive, it is dumb, and it turned a two-hour afternoon into a twenty-line file.
import json
import re
from typing import Any
FENCE = re.compile(r"```
(?:json)?\s*(.*?)
```", re.DOTALL)
def extract_record(text: str) -> dict[str, Any]:
"""Return the first JSON object found in text, fenced or not."""
candidates = FENCE.findall(text) or [text]
for chunk in candidates:
chunk = chunk.strip()
start, end = chunk.find("{"), chunk.rfind("}")
if start == -1 or end <= start:
continue
try:
return json.loads(chunk[start : end + 1])
except json.JSONDecodeError:
continue
raise ValueError("no JSON object found in model output")
Fixtures beat opinions
I keep three fixture files next to the test, because the third one is the case I always forget to imagine. They are cheap, they run offline, and they document what "working" means without needing the network.
-
bare.json— a plain object, the happy path nobody has trouble with. -
fenced.txt— a fenced block with prose on both sides. -
prose.txt— no braces at all, which must raiseValueErrorrather than return half a record.
Break two: the process was up, and the proxy could not find it
The second one cost me more time because everything looked healthy from the inside. My server printed a startup line, accepted a request on localhost, and stayed alive while the public route returned an error. Classic question: is the platform down, or is my process listening in the wrong place?
What I had written, and what I should have written, differ by exactly two lines.
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
# port = 8000 # what I wrote
port = int(os.environ.get("PORT", "8000")) # what survives a proxy
host = "0.0.0.0" # not 127.0.0.1
print(f"listening on {host}:{port}")
HTTPServer((host, port), Handler).serve_forever()
Binding to 0.0.0.0 and reading $PORT are the two cheapest habits in this entire article. If you only copy one snippet, copy that one, and log the resolved port on boot so the first thing you read is the truth.
Break three: the state file was a lie I told myself
Here is the interesting one. A free server option gives you a running process, and a running process is not the same thing as a durable disk. I had been writing a small dedupe ledger to the working directory and assuming it would still be there after a restart, which is exactly the kind of assumption that waits until Friday.
So I wrote a boot audit instead of an argument. It records a boot_id on first start, appends a run, and prints what it found, which means a restart answers the question for me rather than inviting me to speculate.
# boot_audit.py
import json
import os
import pathlib
import time
import uuid
STATE = pathlib.Path(os.environ.get("STATE_DIR", ".")) / "state.json"
def load() -> dict:
if STATE.exists():
return json.loads(STATE.read_text())
return {"boot_id": None, "seen": []}
def save(state: dict) -> None:
tmp = STATE.with_suffix(".tmp")
tmp.write_text(json.dumps(state))
os.replace(tmp, STATE) # atomic when tmp and STATE share a filesystem
def main() -> None:
state = load()
first_boot = state["boot_id"] is None
state["boot_id"] = state["boot_id"] or str(uuid.uuid4())
state["seen"].append({"written_at": time.time()})
save(state)
print(
f"path={STATE.resolve()} first_boot={first_boot} "
f"boot_id={state['boot_id']} entries={len(state['seen'])}"
)
if __name__ == "__main__":
main()
Run it, restart the service from your dashboard or CLI, then run it again and compare the two lines.
python boot_audit.py
# restart the service however your platform lets you
python boot_audit.py
If boot_id is unchanged and entries grew, the disk survived and you can keep your ledger local. If boot_id changed and entries reset to one, the disk is per-instance scratch space, and every record you wrote is gone. That is not a bug report, it is a design constraint, and the fix is either an external store or a stateless service.
The 20-minute acceptance run I now do before trusting anything
I wish I had done this on hour one instead of hour thirty, because each step is short and each one has an unambiguous answer. None of them require you to trust a changelog.
-
Binding check —
curl -sS http://127.0.0.1:$PORT/from inside the instance, then hit the public URL and compare. -
Restart check — the
boot_audit.pyloop above, run before and after a restart. -
Contract check — the three fixtures, offline, asserting the parser raises on
prose.txt. - Timeout check — point the client at a deliberately slow stub and confirm you surface an error instead of hanging forever.
- Config echo — print resolved port, state directory, and model identifier at boot so the logs answer questions later.
- Cold-start measurement — timestamp the first request after a restart, and write down the number instead of guessing it.
The decision table I will reuse
| Workload | Free model + free server? | Reason |
|---|---|---|
| Low-stakes text to JSON triage with schema validation | Yes, keep a human review queue for parse failures | A formatting miss costs seconds, not records |
| Anything that must never lose a write | Not yet — move state out first | The boot audit decides this, not the marketing page |
| Tight interactive latency budgets | Usually no, measure first | Cold starts are part of your p50, not a footnote |
| Regulated or personal data | Only after you read the provider's data terms yourself | I cannot answer that question for you |
What I would repeat, and what I would not
I would repeat the audit-first ordering, because it converted three vague suspicions into three concrete checks I can hand to anyone. I would repeat keeping the model behind a narrow contract with fixtures, since that is what made the free access genuinely useful rather than merely available. I would not repeat writing state to the working directory before proving it survives a restart, and I would not repeat debugging from the inside of the process before curling from the outside.
Limitations and who should skip this
This workflow is for people who can add validation code and read their own logs, and it is a poor fit beyond that. If you need guaranteed uptime, a durable disk, fixed egress addresses, or a signed availability commitment, a free tier is the wrong instrument and no amount of scripting changes that. If your task needs long multi-step reasoning rather than short structured extraction, measure the free model against your own fixtures before you build on it, and be prepared to walk away.
And if you cannot put a validator between the model and your write path, do not put the model in the write path at all.
The free model access and the free server option were enough to run this loop for two days, and the boot audit is the reason I know what "enough" actually meant. Run it on your own deployment, keep the numbers you get, and let the second boot_id tell you the truth before your users do.
Top comments (0)