AI Morning Digest Agent: Build & Monetize Your Email Summarizer
Why I Automated My Morning Information Overload

For years, my mornings started the same way: phone in hand, thumb scrolling through a chaotic mix of newsletters, promotional emails, and sensationalist headlines. Forty minutes would vanish before I even poured coffee. The mental energy required to filter signal from noise was stealing the sharpest hours of my day. I realized I didn't need to read faster; I needed a system that read for me. That realization led me to build a personal AI agent that handles email triage and news digest creation while I sleep, delivering a single, clean briefing to my phone at 6:30 AM.
The Architecture: Simple, Cheap, and Reliable
The Pipeline Flow
The data flow is deliberately boring, which is exactly why it works:
- Collectors: Dumb Python scripts pull unseen emails
- State Store: A local JSON file tracks SHA-1 hashes of every processed item ID. This ensures the Large Language Model (LLM) only ever sees brand-new content, keeping context windows tiny and costs near $0.10 per day.
- LLM Triage Agent: A structured prompt classifies and summarizes the fresh batch.
- Renderer: A Jinja2 template builds the final Markdown digest.
- Delivery: A cron job posts the file to a private Telegram channel
Collector Scripts: The Plumbing
The collectors do zero intelligence work. They fetch, truncate, and hash. Here is the mental model for the email collector:
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
The RSS collector follows the same pattern: parse top ten entries per feed, hash the link, truncate summary to 800 characters. Both scripts write to a staging folder; a thin orchestrator merges them, checks the state file, and passes only net-new items to the model.
Constraining the Agent: Structured Output Over Free Text
My first prototype failed because I handed the model a wall of text and asked for a summary. The output was verbose, generic, and occasionally hallucinated connections between unrelated stories. The fix was ruthless constraint.
Enforced Schema
I defined a strict Pydantic schema (or JSON Schema if you prefer) that the model must obey. For email, every item receives exactly one label: act_today, fyi, or noise. For news, every item gets a one-sentence summary, a topic tag, and a deduplication key so the renderer can cluster coverage of the same event across outlets.
Prompt Discipline
The system prompt is short and surgical:
- "You are a triage engine. Output valid JSON only."
- "If uncertain, choose 'fyi' for email or 'low_priority' for news."
- "Never invent facts. If the
Because the input batch is small (usually 30–60 items), I use a fast, cheap model like GPT-4o-mini or Claude 3 Haiku. Latency stays under ten seconds; cost stays negligible.
Deployment on a Raspberry Pi: Zero-Ops Infrastructure
Running this on a Raspberry Pi 4 (4 GB RAM) eliminates server bills and vendor lock-in. The Pi boots from an SSD for reliability. A systemd timer triggers the pipeline at 6:00 AM; the Telegram message lands thirty seconds later.
Environment Hardening
- Secrets live in a
.envfile with 600 permissions, loadedpython-dotenv. - Logs rotate daily
logrotate; I keep fourteen days for debugging. - A weekly cron job prunes the state file of hashes older than thirty days to prevent unbounded growth.
If the Pi loses power, the state file survives. On reboot, the timer fires at the next scheduled run and the pipeline picks up exactly where it left off — no duplicate digests, no missed items.
What Broke and How I Fixed It
IMAP "Unseen" Drift
RSS Feed Churn
Feeds change URLs, drop summary fields, or serve malformed XML. The collector now wraps each feed in a try/except block, logs failures separately, and continues. A quarterly audit of the feed list keeps noise low.
Telegram Markdown Limits
Telegram caps messages at 4,096 characters. The renderer now splits the digest into two messages if needed: "Part 1 — Email Triage" and "Part 2 — News Digest." A simple length check handles this automatically.
Monetizing the Skill Set
Building a personal automation like this teaches a stack — Python, LLMs, scheduling, Linux — that clients pay for. Here are three paths to turn the project into revenue:
1. Freelance Implementation Packages
List a fixed-scope service on Upwork or Fiverr: "I will deploy a daily AI briefing bot on your Raspberry Pi or VPS." Charge $300–$600 for setup plus a monthly retainer of $50 for feed maintenance and prompt tuning. Deliverables include a private GitHub repo, a one-page runbook, and a thirty-day support window.
2. Sell the Template
3. Content and Community
Tools Worth Naming
Throughout the build, a few tools saved disproportionate time:
- feedparser — tolerant RSS/Atom parsing.
- python-dotenv — secrets hygiene.
- Jinja2 — clean Markdown rendering.
- httpx — async Telegram Bot API calls.
- loguru — structured logging with rotation built in.
- cron / systemd timers — zero-dependency scheduling.
Next Steps for Your Build
- Clone the collector pattern above; swap Gmail for Outlook or Fastmail by changing the IMAP host.
- Curate your RSS list in an
feeds.txtfile (one URL per line). - Write the JSON schema for your triage categories before touching the prompt.
- Test the full pipeline locally with
python -m pipelinebefore moving to the Pi. - Add a "digest preview" command that prints to stdout so you can iterate on the prompt without waiting for the morning cron.