Sunday night, 11:47 PM. I had thirty-one open tabs: papers, Stack Overflow threads, and half-finished markdown notes from my ML course. I closed the laptop and knew, with the certainty of someone who has done this before, that I would remember maybe ten percent of it by Friday.
So I built a small machine to re-read my notes for me. Every night at 2 AM, a free server wakes up, looks at whatever I wrote that day, sends it to a free model, and publishes a three-line summary to a static page. I let it run for seven days without touching it. This is the case study: what worked, what broke, and why I think free infrastructure is a great teacher — as long as you expect it to fail.
The question I wanted to answer was simple. Can a free model plus a free server run a genuinely useful nightly batch job without me babysitting it? And the follow-up: where exactly does it break?
The setup
MonkeyCode is an open-source project that gives students like me two things: free model access and a free server option. No credit card, no "upgrade to continue" popup. The free tier included a 10M token allowance when I set this up — a token is roughly a word or a chunk of code — and that number comes from the project's README, so check the current value before you rely on it. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
My plan was boring on purpose. I keep my course notes as markdown files in a folder. The server would run a cron job — a scheduled task, in case you haven't met cron yet — that finds notes modified in the last 24 hours, truncates each to a safe length, asks the free model for a three-line summary, and writes the results into a single HTML page. No database. No queue. No framework. Just Python, cron, and one API call per note.
Prerequisites: Python 3.12, a folder of markdown notes, an API key from the MonkeyCode project, and one free server instance. The whole thing took about an hour to build and deploy.
The code
Here's the entire script, minus the endpoint details:
# digest.py
import os
import json
import urllib.request
from pathlib import Path
from datetime import date, datetime
NOTES = Path("notes")
PUBLIC = Path("public")
MODEL_URL = os.environ["MONKEYCODE_MODEL_URL"] # from the project README
API_KEY = os.environ["MONKEYCODE_API_KEY"]
def recent_notes():
cutoff = datetime.now().timestamp() - 24 * 3600
return [p for p in NOTES.glob("*.md") if p.stat().st_mtime > cutoff]
def summarize(text: str) -> str:
payload = {
"messages": [
{"role": "system", "content": "Summarize this student note in three plain lines. No markdown, no headers, no bullets."},
{"role": "user", "content": text[:2000]},
]
}
req = urllib.request.Request(
MODEL_URL,
data=json.dumps(payload).encode(),
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as resp:
data = json.load(resp)
return data["choices"][0]["message"]["content"].strip()
def main():
notes = recent_notes()
if not notes:
print(f"{datetime.now()} - nothing new, skipping")
return
lines = [f"<h2>{date.today()}</h2>"]
for path in sorted(notes):
text = path.read_text()
lines.append(f"<h3>{path.stem}</h3>")
lines.append(f"<p>{summarize(text)}</p>")
PUBLIC.mkdir(exist_ok=True)
(PUBLIC / "index.html").write_text("\n".join(lines))
print(f"{datetime.now()} - digest written for {len(notes)} notes")
if __name__ == "__main__":
main()
And the cron line that makes it run while I sleep:
0 2 * * * cd /home/alex/digest && python3 digest.py >> run.log 2>&1
Deploy it to the free server, point a URL at public/, and go to bed.
Expected output
The next morning, index.html looked like this:
<h2>2026-08-19</h2>
<h3>attention-is-all-you-need-notes</h3>
<p>Self-attention computes weighted averages over all positions.
The weights come from scaled dot products. Multi-head means several
of these run in parallel and get concatenated.</p>
Not beautiful. But I read it in thirty seconds on the bus, and that was the whole point.
What happened over seven days
Six of seven nights produced a digest I actually wanted to read. The summaries were imperfect — occasionally too vague, once confidently wrong about a gradient descent formula — but good enough to jog my memory before class. The whole week cost a small slice of the free token allowance. The server never asked me for money.
The failures were the real content, though. I hit three, and each one taught me something specific.
Night two: the model ignored the format. I asked for three plain lines. It gave me markdown headers and bold text. The digest still worked, but it looked like a ransom note. The fix was validation: check the response for # or ** before writing it, log the violation, and retry once with a stricter prompt. Garbage in, formatted garbage out.
Night four: my truncation cut through a code block. One note had a 300-line PyTorch training loop. text[:2000] sliced it mid-function, and the summary described an incomplete forward() that never existed in my code. The fix: truncate at the last newline before the limit — safe = text[:2000].rsplit("\n", 1)[0] — and skip any note where a code fence appears in the first 2000 characters. Truncation is not free; it changes meaning.
Night six: the job never ran. The server was fine. My cron line had a typo — 2 * * * * instead of 0 2 * * *. The free server doesn't send a sad email when your job silently dies. I only noticed because the digest page went stale. That was the most valuable failure of the week: a batch job without a heartbeat is just a rumor. I added a last_updated timestamp to the page and a tiny external check that pings me if it's older than 36 hours.
Lessons and limits
Free infrastructure teaches you because the failure modes are visible. There's no SLA to hide behind. The first call of the night was noticeably slower — the server had to wake up from a cold start, the first request after idle time — while the next calls were fast. That's fine for a 2 AM batch job and a dealbreaker for anything real-time.
Who should not copy this approach? Anyone processing data they can't afford to expose, because a free server is not your private vault. Anyone who needs guaranteed delivery by 9 AM, because "free" and "guaranteed" rarely share a sentence. And anyone whose documents routinely exceed a few thousand tokens without chunking, because truncation will quietly destroy meaning.
The extension I'm trying next: asking the model to flag notes that contradict an earlier digest — a tiny "wait, did I change my mind?" detector. If you try this with your own notes, I'd genuinely like to know which failure mode you hit first. Mine was the code-block truncation, and it took me two days to notice.
Top comments (0)