DEV Community

ULNIT
ULNIT

Posted on

How I Built an AI Agent That Reads the News and My Inbox Before I Wake Up

How I Built an AI Agent That Reads the News and My Inbox Before I Wake Up

For years my morning routine was the same: reach for the phone, open email, open a few news sites, and lose 40 minutes to a mix of newsletters I didn't care about and headlines designed to spike my cortisol. By the time I actually sat down to work, the best part of my focus was gone — spent on information that, honestly, an intern could have filtered for me.

So I built the intern. An AI agent, running on a Raspberry Pi sitting on my desk, that reads everything for me overnight and hands me a one-page digest with my coffee. This is the story of how I built it, what broke, and what I'd do differently.

The Goal

I wanted three things:

  1. Inbox triage — sort email into "act today", "FYI", and "noise" before I see it.
  2. News synthesis — follow ~40 RSS sources and summarize only what's genuinely new, deduplicated across outlets.
  3. One artifact — a single markdown digest delivered to Telegram at 6:30 AM. No apps to open, no feeds to scroll.

The hard part isn't any of these individually. It's doing them reliably, cheaply, and without the agent hallucinating a news story that doesn't exist.

The Architecture

The pipeline is deliberately boring:

[IMAP + RSS collectors] → [dedup/state store] → [LLM triage agent] → [digest renderer] → Telegram
Enter fullscreen mode Exit fullscreen mode

Everything runs on cron. The collectors are dumb scripts; the LLM only ever sees new items since yesterday's run. That single decision — never re-send the full dataset to the model — keeps the whole thing under ~$0.10/day in API costs.

The collectors are straightforward Python:

import imaplib, email, feedparser, json, hashlib

def fetch_unseen_emails(since_hours=24):
    mail = imaplib.IMAP4_SSL("imap.gmail.com")
    mail.login(EMAIL, APP_PASSWORD)
    mail.select("INBOX")
    _, data = mail.search(None, "UNSEEN")
    items = []
    for num in data[0].split():
        _, msg = mail.fetch(num, "(RFC822)")
        m = email.message_from_bytes(msg[0][1])
        items.append({
            "id": hashlib.sha1(num).hexdigest(),
            "from": m["From"],
            "subject": m["Subject"],
            "body": str(m.get_payload())[:1500],
        })
    return items

def fetch_feeds(urls):
    items = []
    for url in urls:
        for e in feedparser.parse(url).entries[:10]:
            items.append({
                "id": hashlib.sha1(e.link.encode()).hexdigest(),
                "title": e.title,
                "summary": e.get("summary", "")[:800],
                "link": e.link,
            })
    return items
Enter fullscreen mode Exit fullscreen mode

A state file tracks which item IDs have already been processed, so the agent only triages fresh material.

The Agent Layer

This is where most DIY versions fall apart. My first attempt handed the LLM a wall of text and said "summarize this." The result was bland, generic, and occasionally invented connections between stories that didn't exist.

What actually works is constraining the agent hard:

  • Structured output only. Each item must be classified (act_today / fyi / noise for email, read / skip for news) with a one-line justification.
  • No synthesis across items. The agent summarizes each item independently; I do the connecting. This killed hallucinations almost entirely.
  • A scoring rubric in the prompt. "Relevant to: self-hosting, security research, LLM tooling, Raspberry Pi. Everything else is noise unless it's genuinely major."
PROMPT = """You are a triage agent. For each item below, return JSON:
{verdict: "act_today"|"fyi"|"noise"|"read"|"skip", reason: "<10 words"}
Relevance rubric: self-hosting, security research, LLM tooling,
Raspberry Pi. Be ruthless — default to noise/skip."""
Enter fullscreen mode Exit fullscreen mode

The digest renderer then groups verdicts into sections and sends the markdown to Telegram via the bot API. Total runtime: about 3 minutes per morning.

What Broke (and What I'd Do Differently)

Rate limits. My first version fired one API call per item. Batching items into groups of 10 cut cost and latency by 80% with no quality loss.

Feed noise. Some RSS feeds duplicate stories across categories. Deduplicating by title similarity (a quick difflib ratio check) before the LLM stage saved a third of the tokens.

Plumbing vs. thinking. Here's my honest takeaway: the interesting 20% of this project was the prompt engineering and the triage rubric. The other 80% — collectors, state management, retries, scheduling, output formatting — is solved plumbing you should not hand-roll. I ended up rebuilding the scaffolding on top of the AI Agent Toolkit, a $9 set of ready-made agent patterns and scripts that gave me the collector/state/delivery boilerplate in an afternoon. If you're building something like this, starting there is the difference between a weekend project and a month of yak-shaving. The same skeleton, incidentally, powers my security monitoring too — the Bug Bounty Automation Kit is the same idea pointed at recon pipelines instead of inboxes.

The Result

My screen time in the first hour of the day dropped from ~40 minutes to ~5. The digest is one Telegram message: five emails that actually need answers, six stories worth reading, nothing else. Some days the "noise" bucket has 60 items and I feel zero guilt ignoring it.

The bigger lesson: agents aren't magic, they're interns. They're brilliant when you give them a rubric, a narrow scope, and a format to fill in — and useless when you hand them ambiguity. Design the constraints first; the intelligence takes care of itself.

If you build one of these, start small: ten feeds, one inbox, one delivery channel. You can always widen the aperture once the intern stops making things up.

Top comments (0)