The itch
My home server already runs my own Netflix and Spotify — self-hosted, Docker, reachable from anywhere over Tailscale. The next annoyance wasn't about subscriptions, it was about habit: checking Hacker News, a couple of tech blogs, and a few AI-news sites every morning, out of FOMO more than intention.
So I built Hermes — not a chatbot, a background agent. It reads a config file of topics and RSS feeds, checks every 15 minutes whether anything's due, and when it's time, fetches the latest articles, sends them to an LLM to actually curate and summarize, and delivers a clean digest straight to Telegram. No dashboard. Nothing to open. It either shows up, or it doesn't need to.
Building it in provable pieces
I didn't write the orchestrator first and hope for the best. Every stage was proven standalone before anything touched anything else:
Telegram delivery, tested first — before a single line of Python existed, I created a bot via BotFather and fired a raw curl request at the Telegram API just to confirm the token and chat ID actually worked. If the simplest possible test fails, nothing built on top of it matters.
Feed fetching, tested alone — a script that pulled entries from one RSS feed and printed them. No AI, no Telegram — just "does this return real, current data."
Summarization, read critically — fed real fetched entries into an LLM with a prompt built to curate (skip filler, keep substance) rather than just paraphrase headlines. I read the actual output for quality before trusting it, and had to swap models mid-build after my first choice returned a 404 for being deprecated for new API keys — a good reminder that "the docs I remember" and "what the API currently supports" aren't always the same thing.
Full pipeline, run by hand — only once all three worked alone did I chain them and trigger the whole thing manually, watching it land on my phone for real.
Scheduling, last — a systemd timer plus a oneshot service, ticking every 15 minutes, checking a small SQLite table for what's actually due.
The bug that looked like success
The first morning, nothing arrived. That was confusing, because everything looked fine: the timer had fired exactly on schedule, every 15 minutes, all night. The logs showed clean, successful runs. No errors, no crashes.
The actual problem was in the logic itself, not the infrastructure around it. I'd written the "is this topic due" check as a rolling cooldown: has it been 24 hours since the last send? That's the correct question if you want something to run "every 12 hours" or "every 6 hours." It's the wrong question if what you actually want is "every day at a specific clock time." A topic last sent at 11:58 PM the night before wouldn't cross the 24-hour mark again until nearly midnight the next day — completely sailing past the 7 AM window I'd configured, day after day, without a single error to flag it.
The fix was to stop treating a fixed daily time as a duration problem at all. For any topic with a set send time, the real question is: has today's target time already passed, and have I not sent anything today yet? That's a date comparison, not an hours-elapsed comparison — two ideas that look almost identical in code but produce completely different schedules.
There was a second, smaller version of the same lesson: I'd also assumed the server ran in UTC when scheduling times, when in reality the fix needed to account for the actual system timezone matching the times I cared about — otherwise "7 AM" silently means 7 AM somewhere else.
What actually proved it worked
Not the timer showing active (waiting). Not the service exiting with status=0/SUCCESS. The only real proof was checking journalctl logs for the actual scheduled window and confirming a topic was fetched, summarized, and marked sent — and then checking my phone. A green checkmark from systemd means the process didn't crash. It says nothing about whether the logic inside it did what I actually wanted.
Where it stands now
Hermes runs unattended today, delivering a curated digest every morning without me touching anything. The build was small compared to some of my other server projects, but it reinforced the same principle: nothing "just works," and the gap between "ran without errors" and "did the right thing" is exactly where the real bugs hide.
Next up: a fallback chain across multiple AI providers, so a rate limit on one doesn't mean a missed morning — and reusing this same dispatch-and-deliver skeleton for other background agents instead of rebuilding the plumbing from scratch each time.
Top comments (0)