DEV Community

Dakota Wu
Dakota Wu

Posted on

One Model Call Per Delivery With a File Outbox

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.

  1. The HTTP handler writes an event file and returns. It does not call a model.
  2. A later step may call the model once, then stores the result beside the event.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:]))
Enter fullscreen mode Exit fullscreen mode
python preflight.py --diff changes.diff --cost-class cost_class.txt
echo "preflight_exit=$?"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
python -m unittest test_outbox.py -v
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode
FIXTURES=1 python app.py
curl -fsS http://127.0.0.1:8080/health
Enter fullscreen mode Exit fullscreen mode

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)