A solo founder has one quiet morning and a half-built changelog tool. The landing page still lives as a prompt in a scratch file. A paid host would clear the card before the first user arrives.
The useful move is smaller than a full product launch. Freeze the prompt, write one static page, and commit that file. After the commit, the public box only serves the file.
No visitor request should wake a model again. That split keeps the morning bill at zero. It also accepts a blunt limit: the page cannot think.
Think of a bakery that prints the menu before dawn. The counter hands out paper and never cooks a new sentence. A free server works the same way when the file is already done.
The sketch uses one free coding-model pass, then stops. The operator describes MonkeyCode as an open-source project with free model access. A free server option is the other operator-supplied claim.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. Those two availability claims are not a measured quota or a hardware spec. Readers should confirm the live project page before relying on either offer.
A specific token grant is easy to quote and easy to stale. This article refuses a number for that reason. The workflow treats whatever allowance exists as finite and changeable.
A zero-bill day fails when generation hides inside the request path. Each refresh spends allowance, and a spike can exhaust it. The page also slows down, because a model sits on the critical path.
The fix is a gate that refuses repeat work. A hash of the prompt file becomes the lock. If the hash matches the last stamp, the script exits before any call.
The local budget is a character ceiling, not a vendor meter. Twelve thousand characters is a proposal for this sketch, not a benchmark. A longer prompt means the founder should cut scope first.
Raising the ceiling in silence hides the real cost. Cut a section, or split the page into a second frozen file. Do not let the model invent a backend to make it dynamic.
The lock in code
The script below is an unexecuted example for a local repo. It writes a static page and a small ledger. Swap the placeholder writer for the current free-model client.
#!/usr/bin/env python3
"""Freeze a prompt and refuse a repeat model call.
Unexecuted example. write_page() does not call a network.
Replace it with the current free-model client when needed.
"""
import hashlib
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parent
PROMPT = ROOT / "prompts" / "changelog.md"
OUT = ROOT / "public" / "index.html"
LEDGER = ROOT / ".token-ledger.tsv"
STAMP = ROOT / ".gen-stamp"
CHAR_BUDGET = 12_000
def fail(msg: str, code: int = 2) -> None:
print(msg, file=sys.stderr)
raise SystemExit(code)
def write_page(tmp: Path) -> None:
html = "\n".join([
"<!doctype html>",
"<html lang='en'>",
"<meta charset='utf-8'>",
"<title>Ship notes</title>",
"<main>",
"<h1>Ship notes</h1>",
"<p>Frozen artifact. This page does not call a model.</p>",
"</main>",
"</html>",
"",
])
tmp.write_text(html, encoding="utf-8")
def main() -> None:
if not PROMPT.is_file():
fail(f"missing prompt file: {PROMPT}")
raw = PROMPT.read_bytes()
if len(raw) > CHAR_BUDGET:
fail(f"prompt exceeds local char budget: {len(raw)}")
digest = hashlib.sha256(raw).hexdigest()
old = STAMP.read_text().split()[0] if STAMP.exists() else ""
if digest == old and OUT.is_file() and OUT.stat().st_size > 0:
print("prompt unchanged; refusing a new model call")
return
OUT.parent.mkdir(parents=True, exist_ok=True)
tmp = OUT.with_suffix(".html.tmp")
write_page(tmp)
if tmp.stat().st_size == 0:
fail("refusing to publish an empty page")
tmp.replace(OUT)
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
with LEDGER.open("a", encoding="utf-8") as handle:
handle.write(f"{now}\t{digest}\t{len(raw)}\n")
STAMP.write_text(digest + "\n", encoding="utf-8")
print(f"wrote {OUT}")
if __name__ == "__main__":
main()
The hash check is a lock on the door, not a timer. Unchanged prompts should not buy a second opinion from the model. Changed prompts still need a human to read the HTML before publish.
Write to a temp file, then rename it into place. A crash mid-write should not leave a half page live. The rename is a small trick a free box will not add.
The ledger stores time, hash, and character count. It does not store a vendor invoice, because this sketch never saw one. Treat each row as a local receipt, not as proof of a free balance.
A dumb public tree
Serving stays dumb on purpose for this class of tool. A static file server has no secret and no session. Python's module server is only a local stand-in for the free box.
mkdir -p prompts public
printf 'Ship notes for a one-morning changelog.\n' > prompts/changelog.md
python3 freeze_page.py
python3 -m http.server 8080 --directory public
Before upload, a second script walks the public tree. It looks for model endpoints that would run per visit. A match means the freeze failed, so the upload stops.
#!/usr/bin/env python3
"""Fail if public files look like a live model client."""
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent / "public"
PATTERN = re.compile(
r"api\.openai|anthropic\.com|generativelanguage|fetch\s*\(|XMLHttpRequest",
re.I,
)
def main() -> None:
if not ROOT.is_dir():
print("missing public dir", file=sys.stderr)
raise SystemExit(2)
bad = []
for path in ROOT.rglob("*"):
if not path.is_file():
continue
text = path.read_text(encoding="utf-8", errors="ignore")
if PATTERN.search(text):
bad.append(str(path))
if bad:
print("request path is not frozen", file=sys.stderr)
print("\n".join(bad), file=sys.stderr)
raise SystemExit(3)
print("public tree has no obvious model call")
if __name__ == "__main__":
main()
This is a test plan, not a claim about a live host. Run the writer twice with the same prompt file. The second run should print a refusal and leave the ledger unchanged.
Then change one line in the prompt and run again. The stamp should move, and the ledger should gain one row. Open the page and confirm the HTML contains no fetch to a model.
A third check belongs in the browser, not only in grep. Disable the network after load and reload the page. A frozen page still renders, because nothing remote was required.
python3 freeze_page.py
python3 freeze_page.py
python3 check_public.py
test -s public/index.html
awk -F '\t' 'END {print NR}' .token-ledger.tsv
A morning sequence keeps the founder honest about spend. Create the prompt file before opening any model chat. Run the freeze script, read the HTML, then run the public check.
Only after both commands pass should the public directory be copied. Copy means an upload, not a second generation pass. The free server then has nothing left to decide.
If the check fails, fix the file by hand. Do not ask the model to remove the script in a loop. Each loop is another spend, and the hash will move.
Hand edits after generation are allowed, and they should be committed. They do not require a new model call. The stamp still refers to the prompt, not to the HTML.
That mismatch is intentional for this small workflow. The lock stops repeat generation from the same prompt. It does not block a human from correcting a sentence.
Failures worth catching
Read the ledger the next morning before any new prompt edit. One row means one intentional generation, which is the point. Three rows from a single morning mean the gate was bypassed.
Bypass usually comes from a temp file left outside the stamp. Point the client at the same prompt path every time. A second prompt file is a second bill, even when the words match.
Partial pages are the other quiet failure mode. If the output file is empty, the stamp must not update. The example updates the stamp only after a successful replace.
Clock skew does not matter to the hash, and that is deliberate. The stamp stores a digest, not a timestamp comparison. Two machines can disagree on time and still agree on the file.
The free server should receive only the public directory. Do not upload the prompt, the stamp, or the ledger. Those files are workshop notes, and they do not belong in public.
A tiny contract keeps the model from drifting into an app. The page must be one HTML file with no script source. The title must be plain text, and the body must not embed a key.
If the generated file adds a script tag, reject it. If it adds a form that posts somewhere, reject it. The founder is shipping notes, not a signup funnel, on this morning.
Who should skip it
Limits sit in the open for anyone copying this loop. A character count is not a token count, and vendors count differently. The ledger can drift from the real bill if the client adds hidden context.
Free hosting can sleep, rate-limit, or reject a custom domain. This draft does not name hardware, regions, or uptime. Those facts change, and a blog post should not freeze them.
The page cannot personalize, charge a card, or store a user. Auth, webhooks, and per-visit generation do not belong here. If the product needs those, this zero-bill shape is the wrong tool.
Who should skip the approach is equally concrete. A team with an uptime contract should not bet on a free box. A founder who will not read generated HTML should not publish it.
Anyone handling private customer text should stay off this path. The prompt file can leak into the artifact and the ledger. Keep secrets out of both, and keep the repo private until review.
There is a quieter cost than the hosting bill. A frozen page goes stale the hour the product changes. The founder must choose staleness, or spend another generation pass.
That choice is the indie constraint in plain clothes. Ship the file today, and accept that it will not adapt. Update it tomorrow only when the prompt hash truly changes.
MonkeyCode fits this loop only as the optional generator and host. Remove the name and the method still stands. The method is the split between a one-time write and a dumb read.
The same split works with any client that can write a file once. If current free model access and a free server still fit, use them here. Read the live terms first, then run the hash gate before the first call.
Top comments (0)