DEV Community

Sam Rivera
Sam Rivera

Posted on

Build a Two-Lane Task Router So Your Free AI Tier Survives the Whole Day

I have a recurring solo-builder problem: I get access to something free — a free model tier, a free hosted runner, a promo quota — and by 11 a.m. I've burned it on tasks that didn't need it. Renaming a function. Summarizing a log I could have grep-ed. Asking a frontier-priced question about a tsconfig error I've seen forty times.

The fix isn't discipline. Discipline loses to momentum by lunchtime. The fix is a tiny router that makes the cheap lane the default and the expensive lane an explicit decision.

This is a ~70-line Python CLI that sorts each AI task into one of two lanes before it fires:

  • Lane A (free): bounded, low-stakes tasks — drafts, renames, boilerplate, log triage. These go to whatever free access you have that day.
  • Lane B (metered): tasks touching money, auth, data loss, or anything I'll be embarrassed about in a postmortem. These require a --spend flag, so they never happen by accident.

Where the free access comes from

I've been running Lane A through MonkeyCode, which offers free model access plus a free server option, so the whole free lane can run without me standing up infrastructure for it.

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

That said, the router doesn't care. It's a config stanza. If the free lane is MonkeyCode today and a local model next month, the artifact below doesn't change — only the endpoint does. That exit property matters more to me than any specific provider, so let's build for it.

The artifact: lane.py

Save this as lane.py. No dependencies beyond the standard library.

#!/usr/bin/env python3
"""Two-lane task router. Free lane by default, metered lane only on --spend."""
import json
import os
import re
import subprocess
import sys
import time
from pathlib import Path

JOURNAL = Path.home() / ".lane-journal.jsonl"

# Tasks matching these patterns are NEVER allowed in the free lane.
# Edit for your own blast radius. Mine: anything that can lose data,
# move money, or ship to users.
LANE_B_PATTERNS = [
    r"\b(migrat|drop table|delete from|destroy)\w*",
    r"\b(auth|oauth|jwt|secret|token|credential)\w*",
    r"\b(payment|billing|stripe|invoice)\w*",
    r"\b(deploy|release|publish|production)\w*",
]

FREE_LANE_CMD = os.environ.get("LANE_FREE_CMD")      # e.g. your free-tier CLI call
METERED_LANE_CMD = os.environ.get("LANE_SPEND_CMD")  # e.g. your paid CLI call


def classify(task: str) -> str:
    for pat in LANE_B_PATTERNS:
        if re.search(pat, task, re.IGNORECASE):
            return "B"
    # Long, open-ended prompts also go metered-or-local: they tend to
    # balloon into multi-turn sessions that eat a free allowance.
    if len(task) > 800:
        return "B"
    return "A"


def log(entry: dict) -> None:
    entry["ts"] = int(time.time())
    with JOURNAL.open("a") as f:
        f.write(json.dumps(entry) + "\n")


def run(cmd_template: str, task: str) -> int:
    cmd = cmd_template.replace("{task}", task)
    return subprocess.call(cmd, shell=True)


def main() -> int:
    spend = "--spend" in sys.argv
    argv = [a for a in sys.argv[1:] if a != "--spend"]
    if not argv:
        print("usage: lane.py [--spend] <task description>", file=sys.stderr)
        return 2
    task = " ".join(argv)
    lane = classify(task)

    if lane == "B" and not spend:
        log({"lane": "B", "decision": "refused", "task": task[:120]})
        print(
            "[lane] This smells like a Lane B task (data/auth/money/prod or very long).\n"
            "       Free lane refused. Rerun with --spend to use the metered lane,\n"
            "       or rewrite the task smaller and safer.",
            file=sys.stderr,
        )
        return 1

    cmd = METERED_LANE_CMD if (lane == "B" and spend) else FREE_LANE_CMD
    if not cmd:
        print("[lane] No command configured for this lane. Set LANE_FREE_CMD / LANE_SPEND_CMD.", file=sys.stderr)
        return 2

    log({"lane": lane, "decision": "ran", "task": task[:120]})
    return run(cmd, task)


if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

Wire it up with whatever you actually use:

export LANE_FREE_CMD='monkeycode run "{task}"'      # free lane
export LANE_SPEND_CMD='my-paid-agent run "{task}"'  # metered lane

chmod +x lane.py

./lane.py "rename getUser to fetchUser across src/"        # -> Lane A, runs free
./lane.py "write the stripe webhook handler"               # -> refused
./lane.py --spend "write the stripe webhook handler"       # -> Lane B, explicit
Enter fullscreen mode Exit fullscreen mode

The journal is the real payoff. After a week, ~/.lane-journal.jsonl tells you what you actually asked for:

# How many Lane B tasks did I try to sneak through the free lane?
grep '"decision": "refused"' ~/.lane-journal.jsonl | wc -l

# What topics keep triggering the expensive lane?
grep '"lane": "B"' ~/.lane-journal.jsonl | head -20
Enter fullscreen mode Exit fullscreen mode

My first week of data was humbling: 60% of my "needs the good model" tasks were refusals I rewrote into smaller Lane A tasks that worked fine. The router didn't save me money by being smart. It saved me money by adding one awkward extra step in front of my own laziness.

Failure fixture (yes, I hit this)

First version had no length cap. I piped a whole stack trace in as the task — about 4,000 characters — and the free lane happily started a session that spiraled into a long back-and-forth. By the time I noticed, the day's free allowance was mostly gone and the actual fix was a one-line config change. Hence len(task) > 800 -> Lane B, and hence the journal, so the next time I do something that dumb there's a receipt.

Second failure: my Lane B patterns initially included the word token, which also matches every "add a design-token to CSS" task. False positives everywhere. Tighten the regexes to your blast radius, not a generic scary-word list.

Limits and who shouldn't bother

  • The classifier is a dumb regex gate, not a risk model. It will mis-sort tasks. The --spend escape hatch exists because of that.
  • If your free access is effectively unlimited for your usage, this is overhead for a problem you don't have.
  • If you work in a team, per-developer journals don't aggregate; you'd want a shared proxy instead, which is a bigger project than this.
  • Free tiers change terms, throttles, and availability without asking your permission. Keep LANE_FREE_CMD swappable and don't hard-code provider assumptions anywhere else in your tooling. The day the free lane disappears, the router should keep working with LANE_FREE_CMD pointed at a local model or nothing at all.

Time and cost boundary: this took about 40 minutes to write and test, costs nothing to run, and the abandonment criterion is simple — if a week of journal data shows zero refusals, the router isn't earning its place and you should delete it.

If you try it, I'm curious about one thing: which pattern in your Lane B list fires most often? Mine was deploy, which says something unflattering about how casually I treat production. If you want a free lane to point the router at while you test, MonkeyCode's free model access and free server are a reasonable starting point — but bring your own regexes.

Top comments (0)