Your students' agent demos don't fail because the model is weak. They fail because nobody but the author can start them.
That's the whole lab. One endpoint, four gates, and a rubric that gives zero points for cleverness and full points for reproducibility.
I've watched cohort after cohort ship a demo that lives on one laptop, in one shell, with one .env nobody committed. The free server didn't break it. The handoff did.
So here's the lab I hand out: deploy a tiny agent service to a free server, then prove a stranger can bring it back from a clean clone.
Why target a free server on purpose
A paid box hides sloppiness. You over-provision, you leave a process running for a week, and the demo passes.
A free server option forces the questions students should be asking anyway:
- What does this thing need at boot, exactly?
- What happens when the model provider says no?
- Where does state live when the process dies?
- Who turns it off, and how do we know it's off?
Those four questions are the four gates below. The free tier isn't the lesson. It's the constraint that makes the lesson visible.
For the model side, my students use MonkeyCode's free model access so the whole exercise runs without a card on file. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
One caution before you paste this into a syllabus: free tiers change. Check the project's current terms for quotas, limits, and retention on the day you teach. Don't teach a number you read in a blog post — including this one.
Checkpoint 0: write the smoke test before the service
Students want to build first and test later. Flip it. The smoke test is the spec.
# lab/smoke.sh — the only definition of "working" we accept set -euo pipefail BASE="${BASE_URL:?set BASE_URL}" curl -fsS "$BASE/health" | grep -q '"ok":true' curl -fsS -X POST "$BASE/ask" \ -H 'content-type: application/json' \ -d '{"q":"reply with the single word: pong"}' \ | grep -qi 'pong' echo "SMOKE OK" Two checks. /health proves the process is up. /ask proves the model path is wired. If a student can't make both pass from a fresh terminal, they don't have a project yet — they have a session.
Checkpoint 1: the cold-clone gate
This is the gate that eats half the cohort.
#!/usr/bin/env bash # lab/handoff.sh — run this as a stranger, from an empty directory set -euo pipefail REPO="${1:?usage: handoff.sh <git-url>}" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT git clone --depth 1 "$REPO" "$WORK/app" cd "$WORK/app" [ -f README.md ] || { echo 'GATE1: no README'; exit 10; } [ -f .env.example ] || { echo 'GATE1: no .env.example'; exit 11; } grep -q 'deploy' README.md || { echo 'GATE1: no deploy steps'; exit 12; } # student must provide an idempotent start command ./scripts/start.sh BASE_URL="http://127.0.0.1:8000" ./lab/smoke.sh The rule: handoff.sh may not use anything from the student's machine. No global packages, no pre-existing database, no exported variables from an earlier shell.
Common failures, in order of frequency:
-
.envcommitted but.env.examplemissing, so nobody knows which keys matter. -
pip install -r requirements.txtwithout a pinned Python version. - A start script that assumes port 8000 is free.
- Secrets hard-coded, then scrubbed from git history in a panic at 2 a.m.
Gate 1 is pass/fail. No partial credit. A reviewer who can't start it can't grade it.
Checkpoint 2: the restart gate
Kill the process. Start it again. Ask the same question.
./scripts/stop.sh && ./scripts/start.sh BASE_URL="http://127.0.0.1:8000" ./lab/smoke.sh What we're probing is state. If the service keeps conversation history in a Python dict, this gate exposes it in ten seconds. If it stores results in SQLite on an ephemeral filesystem, the restart looks fine and the next deploy loses everything.
Students must answer one question in a NOTES.md: what survives a restart, and what doesn't? "I don't know" is an acceptable answer in week one, as long as it's written down. Silent assumptions are what we're grading against.
Checkpoint 3: the exhaustion gate
Free model access runs out, or a provider returns 429, or a request just hangs. Students need to see their own code do that.
Don't wait for the real limit. Inject it:
# lab/flaky_provider.py — a stub that fails on demand import os, httpx, time def ask(q: str, timeout: float = 8.0) -> dict: if os.getenv("INJECT") == "429": return {"ok": False, "reason": "rate_limited", "retry_after": 30} if os.getenv("INJECT") == "slow": time.sleep(timeout + 5) r = httpx.post(os.environ["MODEL_ENDPOINT"], json={"q": q}, timeout=timeout) return {"ok": r.status_code == 200, "reason": r.text[:120]} Then require three behaviours, and grade them:
- The endpoint returns a typed error, not a stack trace.
- The client sees a status code a human can act on (429 vs 500 matters).
- The UI, if there is one, says something honest like "busy, try again" instead of spinning forever.
A service that only works when the model works isn't a service. It's a proxy with extra steps.
Checkpoint 4: the teardown gate
Free servers get forgotten. Forgotten servers get charged, quota-walled, or left serving someone else's traffic on a stale key.
So teardown is graded:
-
./scripts/stop.shactually stops the process (pgrepreturns nothing). - The deploy doc names the exact place the service runs.
- The
.env.examplelists every key, with a note on how to revoke it.
Two minutes of work. It has caught more bad habits in my labs than any code review.
The rubric (20 points, fair and boring)
| Gate | Points | Pass condition |
|---|---|---|
| 0. Smoke test written first | 2 | smoke.sh exists and fails on an unstarted service |
| 1. Cold clone | 6 | handoff.sh passes from an empty temp dir |
| 2. Restart | 4 | Smoke passes after stop/start; state documented in NOTES.md |
| 3. Injected failure | 4 | 429 and slow paths return typed errors, no stack traces |
| 4. Teardown | 2 | Stop verified; keys and location documented |
| 5. Notes quality | 2 | Honest about what breaks and what's missing |
Notice what's absent: no points for prompt quality, no points for UI polish, no points for model choice. Those are stretch goals, and stretch goals are graded by humans who feel like it.
Stretch goals for the fast students
- Add a
--dry-runflag that runs the smoke test against a stub provider, so CI never touches the model. - Write a second
handoff.shthat runs on a different OS and see what breaks. - Record a two-minute video of the cold clone failing, then passing. That recording is the best portfolio artifact in the course.
- Measure how long a cold clone takes on the free server. Publish the number with the date next to it, because it will change.
Where this lab is the wrong tool
Be honest with your cohort about the edges:
- Anything with real user data. Free tiers are for practice. If the lab handles PII, health data, or credentials, use an institution-controlled environment instead.
- Long-running or GPU-bound work. A lab endpoint that answers one question is a fit. A training job or a queue worker is not.
- Availability as a requirement. Free means "sometimes unavailable." If your syllabus needs 100% uptime during a graded window, you need a box you control.
- Production-portfolio claims. Students should describe this as a classroom deployment, not as evidence they can run production.
And if you teach a cohort where nobody has a credit card, none of this works unless the entry cost is actually zero. That's the only reason I route the model calls through MonkeyCode's free model access and park the service on the free server option — the lab has to be runnable on day one, from a lab machine, without a purchase order.
Start with gate 1, not the build
If you take one thing from this lab: write handoff.sh before the first feature. The script is the contract, and the contract is what the rubric grades.
Run it against last semester's project. Watch it fail. Then decide whether your course is teaching deployment or teaching a demo with extra confidence.
If you want to try it with your own cohort, both the free model access and the free server option are reasonable starting points — read the current terms first, and tell your students what happens when the free part ends.