A solo webhook ships today only when the retry path cannot create a bill. One process and a local file can hold a demo. An AI patch that adds a broker, a hosted cache, or a fresh model call on every timeout cannot.
Cap the work in the repo first. Then publish the route. That order is the method, and it is stricter than a prompt that says "keep it cheap."
A public demo page is cheap to generate. The signup webhook behind it is where cost and retries hide. Host allowlists and recorded fixtures solve different failures. This note covers one delivery, one model call, and a file that remembers both after a restart.
Where the draft spends money
Generated handlers fail in a predictable shape. The route accepts a POST, a timeout fires, and a loop calls the model again. A second edit imports a queue client so the loop can be "more reliable." A third edit reads a region variable that a single process never needed.
Short patches. Large bills. The shape is the same even when the wording changes.
Three checks catch it before merge.
- The HTTP handler writes an event file and returns. It does not call a model.
- A later step may call the model once, then stores the result beside the event.
- A retry loads the stored result. It does not get another model call, and it does not require a broker, a hosted cache, or a worker fleet.
If a draft cannot pass those checks, the feature waits. The founder does not upgrade hosting to rescue the draft.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. Free model access is relevant only as a drafting aid on the founder's laptop. The free server is relevant only as a place to run the fixture-backed process after the tests pass. Quotas, hardware, duration, model names, and permanence are omitted on purpose. No primary source for those figures was attached to this draft, and an unchecked numeric allowance should not be published as current fact.
Day-one decision table
Read the table before opening the generated diff. The Allow column is the cost class for today. The Reject column is what a helpful draft will often suggest. Take the Allow cell, or cut the job.
| Job | Allow on day one | Reject on day one | Why |
|---|---|---|---|
| Accept a signed POST | One process, one port | Managed gateway | A single host can bind a port |
| Survive restart | Append-only JSON files | Paid queue or hosted cache | A file is enough for a solo demo |
| Stop double effects | Idempotency key in the file | External lock service | The key travels with the event |
| Classify the payload | One model call, then store it | Model call per retry | Retries must stay free |
| Prove the route | Fixture double, no socket | Live model calls in CI | CI must not drain an allowance |
| Notice failure | Log line and non-zero exit | Paid paging vendor | A founder can read a log today |
A row that cannot be done in the Allow column is out of scope for this ship. That is a product cut, not a hosting upgrade.
Five steps, in order
The steps are mechanical. A comment in the pull request is not a substitute for any of them. Run them on the laptop before the free server sees the tree.
1. Pin the cost class
Write a one-line file and commit it. The preflight fails if that line changes.
printf 'zero\n' > cost_class.txt
git add cost_class.txt
The pin is dull on purpose. Dull controls survive a rushed afternoon better than a paragraph of review notes.
2. Split accept from classify
accept hashes the body, writes JSON, and returns. classify runs later. It increments a counter only when the result is empty, and it refuses to pass a budget of one.
# Proposal. Not run against a live host for this article.
import hashlib
import json
from pathlib import Path
ROOT = Path("outbox")
BUDGET = 1
def event_id(raw: bytes) -> str:
return hashlib.sha256(raw).hexdigest()[:16]
def accept(raw: bytes) -> dict:
ROOT.mkdir(exist_ok=True)
eid = event_id(raw)
path = ROOT / f"{eid}.json"
if path.exists():
return json.loads(path.read_text())
record = {
"id": eid,
"body": raw.decode("utf-8"),
"model_calls": 0,
"result": None,
}
path.write_text(json.dumps(record))
return record
def classify(eid: str, model) -> dict:
path = ROOT / f"{eid}.json"
record = json.loads(path.read_text())
if record["result"] is not None:
return record
if record["model_calls"] >= BUDGET:
raise RuntimeError("model budget exhausted")
record["model_calls"] += 1
record["result"] = model(record["body"])
path.write_text(json.dumps(record))
return record
The HTTP route may import accept only. A draft that calls classify inside the request function has already broken the budget, even if the import scanner is quiet.
3. Scan the diff for brokers
Keep the banned list local and short. Add a word only after a real draft uses it. The CLI below is the merge gate.
# Proposal scanner and CLI. Unexecuted example.
import sys
from pathlib import Path
BANNED = ("boto3", "sqs", "redis", "amqp", "celery", "kafka", "pubsub")
def scan(diff_text: str) -> list[str]:
hits = []
for line in diff_text.splitlines():
if not line.startswith("+") or line.startswith("+++"):
continue
low = line.lower()
for word in BANNED:
if word in low:
hits.append(word)
return sorted(set(hits))
def cost_ok(text: str) -> bool:
return text.strip() == "zero"
def main(argv: list[str]) -> int:
if "--diff" not in argv or "--cost-class" not in argv:
return 2
diff_path = argv[argv.index("--diff") + 1]
class_path = argv[argv.index("--cost-class") + 1]
hits = scan(Path(diff_path).read_text())
if hits or not cost_ok(Path(class_path).read_text()):
print("rejected", ",".join(hits))
return 2
print("accepted")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
python preflight.py --diff changes.diff --cost-class cost_class.txt
echo "preflight_exit=$?"
Exit 0 means no banned import and a zero cost class. Exit 2 stops the merge. Do not override that exit because the patch looks small.
4. Prove the retry with a double
The test model counts calls. The second classify must not increment that count. Any drafting session, including one that uses MonkeyCode's free model access, stays outside this process. The test should pass on a laptop with the network disabled.
# Proposal test. Unexecuted example.
def test_second_classify_is_free(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
calls = {"n": 0}
def model(body: str) -> str:
calls["n"] += 1
return "ok"
payload = "{'type':'signup'}"
rec = accept(payload.encode("ascii"))
classify(rec["id"], model)
again = classify(rec["id"], model)
assert calls["n"] == 1
assert again["result"] == "ok"
python -m unittest test_outbox.py -v
A failure here is a stop. Do not deploy a red tree to the free server to see what happens.
5. Boot fixtures, then one manual POST
Start with fixtures on. Confirm the health payload. Then send one real body from the founder's machine and read the outbox file. A missing file, a second model call, or a new import means roll back the process. It does not mean rewrite the cost class.
# Proposal health payload. Unexecuted example.
def health() -> dict:
klass = Path("cost_class.txt").read_text().strip()
return {"cost_class": klass, "model_calls": 0, "fixtures": True}
FIXTURES=1 python app.py
curl -fsS http://127.0.0.1:8080/health
Health should report a zero cost class and zero model calls before that manual POST. After the POST, the matching JSON file should show a model call count of 0 or 1, never 2.
Failure analysis
Four breaks are worth drilling before the route is public. Each one has the same response. Name it, keep the cost class, and cut scope.
Disk wipe comes first. A free server may drop local files on restart or redeploy. The outbox then forgets idempotency keys. Treat the demo as disposable, or wait until a durable disk is actually available. Do not fix a wipe by adding a hosted cache in the same patch.
Two processes are the second break. They will race on one directory. This design allows one process. A second instance is a new design, not a config flag, and it is outside the day-one table.
The truncated id is the third break. Sixteen hex characters are enough for a solo demo, not for a high-volume bus. If volume grows, lengthen the id. A paid lock service is not the first response to a short hash.
The fourth break is a crash after the counter increments and before the result is stored. The sample spends the budget and stores nothing. That is fail-closed. A founder who needs to retry a failed call must add an explicit error field and a separate rule. Silent re-entry into another model route is not that rule.
The free server is not a puzzle to outsmart. When a break shows up, the ship gets smaller.
Who should not use this
Skip the file outbox when more than one instance must run, when the payload is regulated data, or when a missed file is an unacceptable loss. Also skip it when the side effect cannot be stored and replayed. A zero-bill demo is not a compliance design, and free model access is not a throughput commitment.
Operators who need fan-out, multi-region delivery, or a retention guarantee should buy the queue and write that choice into the cost class file. Pretending a free process can imitate that stack wastes the afternoon this method is meant to save.
The next patch
Run the scanner on the diff before reading the generated explanation. If the scanner is empty and the unit test still shows one model call, the tree can move to the free server in fixture mode. If a broker import appears, delete the import and keep the file. Hosting does not get a vote until those two checks agree.
Founders who draft with MonkeyCode's free model access can run that same unittest on the laptop tree and again on the free-server checkout. Ship only when both call counts match. That comparison is the check worth keeping. Whatever the allowance is this week, it is not a design input.
Top comments (0)