Friday, 9 PM. I had an idea and 48 hours. Budget: zero dollars.
I wanted to prove something. A side project can go from idea to live URL without spending money. Not a tutorial. A real demo. Something I would actually use.
There is a DEV discussion that keeps circling: "What do you do while AI codes?" My answer after this weekend: you cut scope. The model writes code fast. Your job is deciding what not to build.
The idea
I have a few small services that I check by hand. When one goes down, I notice hours later. I wanted a public status page. It should ping a URL every minute and show latency history.
Full vision: multi-user, alerting, webhooks, charts, Docker, a database.
Shipped version: one Python file, SQLite, a background thread, a plain HTML table.
The scope-cut table
| Feature | Full vision | Shipped | Why cut |
|---|---|---|---|
| Auth | Multi-user login | None | Nobody else needs to log in |
| Alerts | Email + Slack | None | A demo is not on-call |
| Charts | Chart.js dashboard | Plain HTML table | One endpoint, one page |
| Database | Postgres | SQLite file | One process, one user |
| Deploy | Docker + compose | python app.py |
Fewer moving parts |
Every cut made the demo more likely to finish. That was the point.
The stack
MonkeyCode is an open-source project. Its free tier currently includes a 10M token allowance and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Free tiers change. Check the project README before you depend on the numbers.
I used the free model access to generate the first draft. I hosted the demo on the free server. I did not use the model to design anything. I used it for the boring parts: request handling, the polling loop, HTML escaping.
Step 1 — Scaffold
mkdir pingpage && cd pingpage
python -m venv venv && source venv/bin/activate
pip install fastapi uvicorn
Step 2 — The prompt
I gave the model a tight spec. One paragraph. Explicit constraints:
Single-file Python app. FastAPI. Background thread polls a target URL every 60 seconds. Stores status and latency in SQLite. Serves a minimal HTML status page. No auth. No external chart library.
Short prompts work better when the constraints are hard rules. I listed what to exclude, not just what to include.
Step 3 — Review the generated code
The first draft ran. It also had a real bug. The model created one global SQLite connection at import time. The polling thread used it. SQLite objects are thread-bound, so the poller crashed on the first write.
The fix: create a connection inside the loop.
TARGET = "https://example.com"
INTERVAL = 60
DB = "pingpage.db"
def poll():
while True:
start = time.time()
try:
with urllib.request.urlopen(TARGET, timeout=10):
status, latency = "up", round(time.time() - start, 3)
except Exception:
status, latency = "down", None
conn = sqlite3.connect(DB)
conn.execute(
"INSERT INTO checks (ts, status, latency) VALUES (?, ?, ?)",
(datetime.now(timezone.utc).isoformat(), status, latency),
)
conn.commit()
conn.close()
time.sleep(INTERVAL)
The model wrote the structure. I found the thread bug. That split is normal. Treat generated code as a first draft, not a merge-ready PR.
Step 4 — The missing pieces
The rest is standard FastAPI. One init function, one startup hook, one route.
def init_db():
conn = sqlite3.connect(DB)
conn.execute("CREATE TABLE IF NOT EXISTS checks (ts TEXT, status TEXT, latency REAL)")
conn.commit()
conn.close()
@app.on_event("startup")
def startup():
init_db()
Thread(target=poll, daemon=True).start()
@app.get("/", response_class=HTMLResponse)
def index():
conn = sqlite3.connect(DB)
rows = conn.execute(
"SELECT ts, status, latency FROM checks ORDER BY ts DESC LIMIT 50"
).fetchall()
conn.close()
items = "".join(
f"<tr><td>{ts}</td><td>{status}</td><td>{latency or '-'}</td></tr>"
for ts, status, latency in rows
)
return f"""<!doctype html><html><head><meta charset='utf-8'>
<title>pingpage</title></head><body>
<h1>pingpage</h1><table border='1'>
<tr><th>time</th><th>status</th><th>latency</th></tr>{items}</table>
</body></html>"""
I skipped a chart library. A table is fine for 50 rows.
Step 5 — Run it locally
uvicorn app:app --port 8000
curl -s http://localhost:8000/ | head -20
Wait 60 seconds. Then check the database.
sqlite3 pingpage.db "SELECT COUNT(*) FROM checks;"
One row per poll. If the count grows, the loop works.
Step 6 — Deploy to the free server
The free server runs the same process. No Docker, no build step.
git clone <your-repo-url> pingpage
cd pingpage
python -m venv venv && source venv/bin/activate
pip install fastapi uvicorn
nohup uvicorn app:app --host 0.0.0.0 --port 8000 &
curl -s http://<your-server-address>:8000/
That was the whole deploy. One command to start, one to verify.
Step 7 — What I skipped
- Docker. One Python file does not need it.
- A frontend framework. The page is 30 lines of HTML.
- A test suite. I ran curl, checked the row count, restarted the process.
- Alerting. The page is passive. I look at it when I remember.
Skipping is a feature. Every skipped item was time I did not spend debugging.
Limitations
The polling thread dies if the process restarts. SQLite on a free server may live on ephemeral storage, so history can disappear. One region, one process, no alerting. This is a demo, not an SLA.
Free tiers change. The 10M token allowance and the free server are not permanent promises. Verify the current terms before you build anything important on them.
Who should not use this
Teams that need real uptime monitoring. If an outage costs you money, use a paid service with alerting, multiple regions, and a guarantee.
This approach is for learning, demos, and small personal tools. It worked for that.
The takeaway
The build took one evening. Most of that time was scope cutting, not coding. The model wrote the code. I decided what not to build.
If you want to try the same flow, MonkeyCode's free tier is a low-risk place to start. Point the poller at your own URL and see what breaks. Then cut something from your own plan before you write a line of code.
Top comments (0)