DEV Community

Taylor Zhu
Taylor Zhu

Posted on

Prove the Exit Before the Entry: A Portability Gate for AI Prototypes on Free Tiers

You can ship a prototype on a free tier and still be unable to leave it. That is the real risk, and it has nothing to do with the model's answer quality.

Free model access and a free server are grants, not contracts. They can be re-scoped, rate-limited, or retired on someone else's schedule. So the readiness question is not "does it work?" — it is "have I proven I can move it?"

Below is a checklist you can copy: six gates, one small script, and a drill you run while everything is still free.

The failure mode: coupling, not capability

Prototypes usually break on a free tier for boring reasons. A prompt is hard-coded to one endpoint shape. The only copy of the SQLite file lives on one host's disk. A retry loop reads a status code that only one vendor emits.

None of those are model problems. They are dependency problems, and they are invisible until you need to move.

A portability gate makes them visible on demand instead of on incident day.

Gate 0: name what you depend on

Before any checklist means anything, write down the external things your app actually touches. Not the ones you think it touches — the ones in the code.

# crude but honest: find the edges of your app
grep -rnE "https?://|fetch\(|requests\.|axios\." --include="*.py" --include="*.js" --include="*.ts" . \
  | grep -v node_modules | grep -v ".test."
Enter fullscreen mode Exit fullscreen mode

Every hit is either a dependency to declare or dead code to delete. There is no third option.

The six gates

# Gate Question Evidence artifact Fail-closed rule
G1 Inventory Is every external dependency named in a checked-in file? requirements.json Undeclared dependency blocks promotion
G2 Declaration Are capabilities for both environments filled from docs, with a date and source? caps-a.json, caps-b.json Undeclared field counts as unmet
G3 Gate run Does the hard requirements check exit 0 for both environments? CI log with exit code Non-zero exit blocks promotion
G4 Swap drill Can one real request be served by environment B with config changes only? Status code + response shape Any code edit is a coupling defect
G5 State residency What state exists only on environment A's disk? Written list with an owner Unowned state blocks promotion
G6 Allowance end What does the app do when the free allowance stops? The degrade path you chose No defined path blocks promotion

Two rules make this a gate rather than a wish list. First, evidence must be timestamped and committed to the repo — a screenshot in a chat thread expires with the scrollback. Second, a gate that cannot be evaluated is a failure, not a pass.

The artifact: requirements vs. declared capabilities

You need two small files. One describes what your app demands. The other describes what an environment claims to provide. You fill the second by hand, by reading that provider's docs on a specific date.

requirements.json:

{
  "model": {
    "json_mode":        { "level": "hard" },
    "streaming":        { "level": "soft" },
    "context_tokens":   { "level": "hard", "min": 32000 }
  },
  "host": {
    "always_on":        { "level": "hard" },
    "outbound_https":   { "level": "hard" },
    "persistent_disk_gb": { "level": "soft", "min": 1 }
  }
}
Enter fullscreen mode Exit fullscreen mode

caps-a.json mirrors the same shape, plus provenance:

{
  "checked_on": "YYYY-MM-DD",
  "source": "the docs page you actually read",
  "model": { "json_mode": true, "streaming": true, "context_tokens": 32000 },
  "host":  { "always_on": true, "outbound_https": true, "persistent_disk_gb": 1 }
}
Enter fullscreen mode Exit fullscreen mode

The check itself is short on purpose. Illustrative code — written for this article and not executed against any provider.

#!/usr/bin/env python3
# portability_gate.py - illustrative. Fails closed on unmet hard requirements.
import json, sys
from pathlib import Path

BLOCKS = ("model", "host")

def load(p):
    return json.loads(Path(p).read_text(encoding="utf-8"))

def check(block, req, caps):
    findings = []
    for key, spec in req.items():
        level = spec.get("level", "hard")
        declared = caps.get(key)          # absent means: not declared, not "fine"
        want = spec.get("min")
        if declared is None:
            findings.append((level, f"{block}.{key}: not declared"))
        elif isinstance(want, (int, float)) and isinstance(declared, (int, float)):
            if declared < want:
                findings.append((level, f"{block}.{key}: {declared} < required {want}"))
        elif declared is False:
            findings.append((level, f"{block}.{key}: declared unsupported"))
    return findings

def main(req_path, cap_path):
    req, cap = load(req_path), load(cap_path)
    findings = []
    for block in BLOCKS:
        findings += check(block, req.get(block, {}), cap.get(block, {}))

    hard = [m for lvl, m in findings if lvl == "hard"]
    for m in (m for lvl, m in findings if lvl != "hard"):
        print(f"WARN  {m}")
    for m in hard:
        print(f"FAIL  {m}")

    if hard:
        print(f"\n{len(hard)} hard requirement(s) unmet -> do not promote this environment.")
        return 1
    print("\nAll hard requirements met. Commit caps file as evidence.")
    return 0

if __name__ == "__main__":
    sys.exit(main(sys.argv[1], sys.argv[2]))
Enter fullscreen mode Exit fullscreen mode

Run it and read the exit code, not the prose:

python3 portability_gate.py requirements.json caps-a.json; echo "exit=$?"
python3 portability_gate.py requirements.json caps-b.json; echo "exit=$?"
Enter fullscreen mode Exit fullscreen mode

The swap drill

The gate only compares declarations. The drill is what catches the coupling the declarations miss.

  1. Duplicate caps-a.json to caps-b.json and fill it from a second provider's docs. Date it.
  2. Run the gate against both files. Both must exit 0, or your requirement set is too tight for your own good.
  3. Point the app at environment B with an environment variable — nothing else.
  4. Send exactly one real request. Record the status code and the response shape.
  5. If step 3 or 4 required a code edit, stop. That edit is the defect the whole exercise exists to find.

One request is deliberately not a load test. You are testing the seam, not the throughput.

Where a free environment earns its place

A free environment is most valuable before you have a reason to care about the exit. Use it for the G1 work — the inventory, the first half of requirements.json — while the cost of changing your mind is zero.

This is the honest framing for MonkeyCode's free model access and the free server option the project operator states it provides. The operator also states a free allowance of 10 million tokens. Treat all three as the starting environment for the drill, not as a foundation: terms, available models, and limits are the operator's to change, and you should confirm the current numbers on the project's own documentation before you plan around them.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

What makes it fit this workflow is not generosity, it's sequencing. You want environment A to be something you are not yet paying for, so that the drill's real output — a list of what you accidentally coupled to — arrives before anything is at stake.

Limitations you should not paper over

  • Declarations are not behavior. caps-a.json records what a provider says and when you read it. It cannot prove the endpoint holds up under your traffic.
  • The drill is a spot check. One request confirms the seam exists; it says nothing about concurrency, retries, or long-run stability.
  • Capabilities drift. A gate that passed three months ago is a historical document. Re-run it before you rely on it.
  • The gate is only as good as the inventory. Miss a dependency in G1 and every downstream gate is confidently wrong.

Who should skip this

If the prototype's honest expected lifetime is a weekend demo, deletion is a perfectly good exit strategy — just write that decision down somewhere. Teams already running paid infrastructure with real SLAs have different gates.

But if you intend to keep the thing, run the drill while it is free. Proving the exit costs an afternoon now and a rewrite later.

Copy-paste summary

  • Declare every external dependency in requirements.json; undeclared means unmet.
  • Fill one capabilities file per environment by hand, with a date and a source.
  • Fail closed: a gate you cannot evaluate is a failure.
  • Run the one-request swap drill; any code edit is a defect to file.
  • Name what lives only on the first host's disk, and give it an owner.
  • Decide now what happens when the free allowance ends, and write it down.

Start with G1 today. It takes one grep and one uncomfortable list.

Top comments (0)