The cheapest control for an AI-coded weekend product is a failed test. Any patch that opens a real socket should die on the founder’s laptop, and every invented SaaS call should land on an in-process stub. That is the whole method.
Indie backends do not usually burn money on prompts first. They burn it when a generated handler quietly talks to Stripe, Resend, Redis, or a managed queue. The agent is completing a tutorial-shaped pattern. A tripwire treats that pattern as a bug, not as a roadmap.
This workflow is for a solo founder who must ship today and keep the bill at zero. It accepts ugly limits: memory inboxes, codes printed to stdout, SQLite, one process. Those limits are the product constraint, not a prompt to argue with.
Why generated backends grow invoices
Weekend MVPs start as a process and a local database file. Then a coding agent adds “just a webhook” and a hosted mail provider. Those lines look small in a diff. They are not. They demand accounts, API keys, and a public URL the founder does not have before lunch.
Agents also assume retries, queues, and object storage. Each assumption is another vendor dashboard. The waitlist that was supposed to exist by Sunday night becomes four integrations and zero users. Prompts will not hold that line. A test that cannot see the public internet will.
Treat the agent as a fast if-statement with a large training prior. If the prior says “payments belong on a hosted API,” the patch will import that API. The tripwire does not debate the prior. It fails the connect.
The artifact
The artifact is three files in the repo root. No cloud account is required. The loop stays local.
-
stub_map.json— the only hosts, schemes, and env keys the process may mention. -
tripwire.py— a development guard that refuses outbound sockets and records attempts. -
test_tripwire.py— a reproducible check run before any AI patch is merged.
Label the snippets below as a starter kit, not as production policy. They are meant to be copied, run, and tightened.
1. Declare the stub map
Create the map first. Empty vendor lists are valid on day one. That emptiness is the point.
{
"allowed_hosts": ["127.0.0.1", "localhost"],
"allowed_schemes": ["http"],
"allowed_env": ["DATABASE_URL", "SECRET_KEY"],
"stubs": {
"email": "memory",
"payments": "memory",
"queue": "memory",
"auth_codes": "stdout"
}
}
email: memory stores messages in a list. auth_codes: stdout prints a one-time login code in the terminal. Neither needs a vendor. Keep the file in git. A patch that adds STRIPE_SECRET_KEY without updating this map should fail in step 4.
2. Install the tripwire
The tripwire wraps socket.socket.connect. It is a fail-fast for local tests. It is not a sandbox, a firewall, or a compliance control.
# tripwire.py
from __future__ import annotations
import json
import socket
from pathlib import Path
MAP_PATH = Path(__file__).resolve().parent / "stub_map.json"
_map = json.loads(MAP_PATH.read_text())
ALLOWED = set(_map.get("allowed_hosts") or [])
LOG: list[str] = []
_real_connect = socket.socket.connect
def _guarded_connect(self, address):
host = address[0] if isinstance(address, tuple) else address
host = str(host)
if host not in ALLOWED:
LOG.append(host)
raise RuntimeError(
f"tripwire: blocked outbound connect to {host!r}. "
"Add an in-process stub or update stub_map.json."
)
return _real_connect(self, address)
socket.socket.connect = _guarded_connect # type: ignore[method-assign]
Load it before application code:
export PYTHONPATH=.
python -c "import tripwire; import app"
In tests, import the guard first:
import tripwire # noqa: F401 — must import before app code
Short rule: if the test process can reach the public internet, the tripwire is not installed. Fix the import order before debugging the app.
3. Provide in-process stubs
Stubs should be boring. They exist so the agent can keep writing handlers without opening accounts. Wire them in one module. Do not let route files construct SDK clients.
# stubs.py
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
@dataclass
class MemoryMailbox:
messages: list[dict] = field(default_factory=list)
def send(self, to: str, subject: str, body: str) -> dict:
item = {
"to": to,
"subject": subject,
"body": body,
"sent_at": datetime.now(timezone.utc).isoformat(),
}
self.messages.append(item)
return item
@dataclass
class MemoryLedger:
charges: list[dict] = field(default_factory=list)
def charge(self, amount_cents: int, currency: str = "usd") -> dict:
if amount_cents <= 0:
raise ValueError("amount_cents must be positive")
item = {
"id": f"stub_{len(self.charges) + 1}",
"amount_cents": amount_cents,
"currency": currency,
"status": "stubbed",
}
self.charges.append(item)
return item
@dataclass
class StdoutCodes:
def issue(self, email: str) -> str:
code = f"{abs(hash(email)) % 10**6:06d}"
print(f"[auth-stub] {email} -> {code}")
return code
# wiring.py
from stubs import MemoryMailbox, MemoryLedger, StdoutCodes
mailbox = MemoryMailbox()
ledger = MemoryLedger()
codes = StdoutCodes()
A route calls mailbox.send(...). The generated code still looks like a product. The invoice does not exist. That trade is the indie default until someone pays.
4. Run the test plan
The test plan is the merge gate. Run it on every AI patch, including the manual edits that follow a stalled agent.
# test_tripwire.py
import json
import re
from pathlib import Path
import pytest
import tripwire # noqa: F401
from stubs import MemoryMailbox, MemoryLedger
ROOT = Path(__file__).resolve().parent
MAP = json.loads((ROOT / "stub_map.json").read_text())
ENV_RE = re.compile(
r"os\.environ\[(['\"])([A-Z0-9_]+)\1\]"
r"|os\.getenv\((['\"])([A-Z0-9_]+)\3"
)
def test_stub_map_lists_only_local_hosts():
for host in MAP["allowed_hosts"]:
assert host in {"127.0.0.1", "localhost", "::1"}
def test_tripwire_blocks_public_dns():
import socket
with pytest.raises(RuntimeError, match="tripwire"):
socket.socket().connect(("example.com", 443))
def test_mailbox_does_not_touch_network():
box = MemoryMailbox()
box.send("founder@localhost", "hello", "ship")
assert box.messages[0]["to"] == "founder@localhost"
def test_ledger_rejects_zero():
with pytest.raises(ValueError):
MemoryLedger().charge(0)
def test_source_env_keys_are_declared():
allowed = set(MAP["allowed_env"])
undeclared: set[str] = set()
for path in ROOT.rglob("*.py"):
if path.name.startswith("test_") or path.name == "tripwire.py":
continue
text = path.read_text(encoding="utf-8")
for match in ENV_RE.finditer(text):
key = match.group(2) or match.group(4)
if key and key not in allowed:
undeclared.add(f"{path.name}:{key}")
assert not undeclared, f"undeclared env keys: {sorted(undeclared)}"
Commands:
python -m pip install pytest
python -m pytest test_tripwire.py -q
Expected result on a clean stubbed app: all tests pass. After an agent adds os.environ["STRIPE_SECRET_KEY"], the env test fails. After a code path calls a public host, the connect test fails if that path runs under the guard.
Grep is not a parser. Dynamic os.environ.get(name) can slip through. Keep the live tripwire as the backstop when the handler actually executes.
5. Debug a red patch in order
When the gate fails, do not start by asking the model to “fix CI.” Walk the failure in this order.
- Read the traceback. A
tripwire: blocked outbound connectline names the host. That host is the invented vendor. - Search the diff for SDKs and URLs, not for architecture essays.
- Replace the call with a function on
stubs.py. Keep the route signature stable so the agent does not rewrite the rest of the file. - If a new env key is required for a later paid phase, do not add it now. Add a comment in
stub_map.jsononly when revenue justifies the vendor. - Re-run
python -m pytest test_tripwire.py -quntil it is green. Then merge.
A useful extra check is a model reading the diff against the map. Static tests miss a TODO that says “switch this mailbox to a hosted provider after launch” sitting next to half-wired client code. MonkeyCode’s free model access and free server option can run that narrow pass without adding a second vendor bill. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Keep the review prompt as a checklist:
Read the diff and stub_map.json.
Fail if the patch:
- imports a payments, email, SMS, queue, or object-storage SDK
- adds env keys not listed in allowed_env
- documents a hosted provider as a follow-up in the same files as runtime code
Pass only if new I/O uses stubs.py or localhost.
Keep secrets off that server. The stub map and the diff are enough. API keys should not exist yet. Availability of free model access and a free server can change, so do not plan capacity around them.
Decision table
| Signal in the patch | Day-one action | Later, with revenue |
|---|---|---|
| Email SDK | MemoryMailbox |
One provider, one env key, update the map |
| Payments SDK |
MemoryLedger with status: stubbed
|
Real processor behind the same charge()
|
| Redis / queue | In-process list | Only after a measured backlog |
| Auth email | StdoutCodes |
Email provider, still one issue()
|
| Object storage | Local ./data directory |
Bucket only for real user uploads |
| New env key | Fail the test | Add to allowed_env with a comment |
Ship the left column today. The right column is a product decision. It is not an agent default.
Limitations
The tripwire patches CPython socket.socket.connect. It will not wrap every HTTP stack on every interpreter. Native extensions, subprocesses, and os.system("curl ...") bypass it. A founder who needs a hard network namespace should use OS-level isolation, not this module.
The env scanner is a regular expression. Obfuscated access wins. Teams that need a real policy engine should parse AST or run the app inside a network jail.
Stubs lie. A memory mailbox cannot teach deliverability. A stub charge cannot teach disputes or refunds. That is acceptable for a waitlist. It is not acceptable for money movement.
Free model access and a free server do not make the tripwire stronger. They only add a second reader for the diff. Do not treat them as a quota, a hardware spec, or a permanent plan.
Who should not use this
Skip the tripwire if the product already depends on a paid provider. Skip it for regulated workloads, production incident response, and anything that must send a real message today.
Skip it if failing tests will be ignored. A red suite that always gets -k is theater. Multi-service staging clusters do not belong here either. This workflow assumes one process, one developer, and a bill that must stay at zero until a user pays.
The original problem remains: agents complete vendor-shaped code. The tripwire makes that completion expensive in minutes, not in dollars. Minutes are the resource a solo founder can spend before lunch.
Top comments (0)