Hacker News Daily digest ๐ฐ
August 28, 2026 ยท View on GitHub
A completely free, self-hosted email newsletter that sends you the top Hacker News stories every morning, each with a short AI summary (article + community reaction) and links to both the article and its HN discussion.
No servers, no paid services, no n8n. Just a GitHub Action on a cron schedule.
Top-N stories ยท AI summaries by Google Gemini ยท delivered over Gmail SMTP
๐ See a sample (generated from the .html file).
Project structure
.
โโโ .github/workflows/ # GitHub Actions (daily cron + manual dispatch)
โ โโโ daily-digest.yml
โโโ docs/
โ โโโ example-email.png # Screenshot shown in this README
โ โโโ example-digest.html # Sample rendered email (open in a browser)
โโโ src/
โ โโโ config.py # Env-based configuration
โ โโโ hn_client.py # Hacker News API client (stories + comments)
โ โโโ article_fetcher.py # Best-effort article text extraction
โ โโโ summarizer.py # Gemini summaries (batched, paced + retried)
โ โโโ schedule.py # Reads the workflow cron -> "next run" time
โ โโโ email_renderer.py # HTML email template
โ โโโ mailer.py # Gmail SMTP sender
โ โโโ list_models.py # Helper: list models your API key supports
โโโ main.py # Orchestrator (fetch -> summarize -> send)
โโโ pyproject.toml # Project metadata & dependencies (uv)
โโโ uv.lock # Locked, reproducible dependency versions
โโโ .env.example # Template for local environment variables
โโโ README.md # This file
Why it's free
| Piece | Service | Cost |
|---|---|---|
| Scheduling / compute | GitHub Actions | Free, unlimited minutes for public repos |
| Story data | Hacker News Firebase API | Free, no auth, no rate limits |
| AI summaries | Google Gemini API (free tier) | Free; stories are batched into ~4 requests/day |
| Email delivery | Gmail SMTP (app password) | Free |
The consumer Google AI Pro subscription is not required; the Gemini API has its own free tier (the pricing page lists the Free, Paid, and Enterprise tiers) available to any Google account.
How it works
GitHub Actions (cron, ~05:17 Poland time)
โ
โผ
main.py โโโถ Hacker News API (top 30 stories + top comments)
โ โโโถ fetch article pages (best-effort text extraction)
โ โโโถ Google Gemini (batched: ~8 stories per request)
โ โโโถ render HTML email
โผ
Gmail SMTP โโโถ your inbox ๐ฌ
Each story in the email shows its rank (#1, #2, โฆ), score, title (โ article), source domain, N replies (โ HN discussion), author, age, and a concise 1-2 sentence summary.
Setup (โ 10 minutes)
1. Fork / use this repo
Fork it (or click Use this template) into your own account so the Action runs under your quota. Keep it public for free unlimited Actions minutes.
2. Get a free Gemini API key
Go to aistudio.google.com/apikey โ Create API key. Copy it.
3. Create a Gmail App Password
- Enable 2-Step Verification on your Google account.
- Go to myaccount.google.com/apppasswords.
- Create a password (name it e.g. "HN digest"). Copy the 16-character value.
Tip: consider using a dedicated Gmail account as the sender (e.g.
you.news@gmail.com) instead of your personal one. It keeps the daily digest out of your personal Sent folder, gives the newsletter its own avatar (set a profile picture on that account), and isolates it. PointRECIPIENTSat your real inbox.
4. Add repository secrets
In your repo: Settings โ Secrets and variables โ Actions โ New repository secret.
| Secret | Value |
|---|---|
GEMINI_API_KEY | your Gemini key |
GMAIL_USERNAME | you@gmail.com |
GMAIL_APP_PASSWORD | the 16-char app password |
RECIPIENTS | (optional) comma-separated recipients; defaults to GMAIL_USERNAME |
Optional Variables (same page, Variables tab) to tweak without editing code:
NUM_STORIES (default 30), GEMINI_MODEL (default gemini-3.7-flash).
Model not available? Gemini model IDs change over time. If a run fails with a
404 ... model is no longer availableerror, list what your key supports and setGEMINI_MODELto one of them:GEMINI_API_KEY=your-key uv run python -m src.list_models
5. Test it
Go to Actions โ Daily HN Digest โ Run workflow.
- Tick Dry run to build the email as a downloadable artifact without sending.
- Leave it unticked to send a real email.
6. Done
It now runs automatically every day at โ 05:17 Poland time, year-round.
How the timing survives daylight saving: GitHub cron is UTC-only and ignores DST, so the workflow schedules two UTC times (
03:17and04:17) and a guard (RUN_ONLY_AT_LOCAL_HOUR=5) lets only the run that actually lands on 05:xx inDISPLAY_TIMEZONEproceed; the other exits in seconds. To change the time, edit the twocronlines and the guard hour in.github/workflows/daily-digest.yml(times there are UTC; crontab.guru helps).
Run locally (without GitHub Actions)
The whole digest runs as a plain Python script, so you can generate or send it from your own machine with no GitHub Actions involved. You only need a Gemini API key; Gmail credentials are needed solely for the real send.
Dependencies are managed with uv (install it first, then run):
uv sync # creates a virtualenv from uv.lock
cp .env.example .env # then fill in your keys
# Preview only: writes output/digest.html and sends nothing.
# Needs just GEMINI_API_KEY. Open the file in a browser to see the result.
DRY_RUN=true uv run python main.py
# Send for real (needs GMAIL_USERNAME + GMAIL_APP_PASSWORD too).
uv run python main.py
This is exactly what the GitHub Action does; it just runs the same python main.py on a schedule. Running locally is handy for previewing layout changes or
sending an ad-hoc digest.
Trigger a run manually
You don't have to wait for the daily schedule; you can run it any time:
From GitHub (no setup needed): open Actions โ Daily HN Digest โ Run workflow. Tick Dry run to build a downloadable HTML artifact without sending, or leave it unticked to send a real email.
From your terminal (needs the gh CLI):
gh workflow run "Daily HN Digest" -f dry_run=true # preview (no email)
gh workflow run "Daily HN Digest" # send for real
Manual runs always execute immediately and bypass the daylight-saving guard (that guard only applies to the scheduled cron runs).
Configuration
All settings are environment variables (see .env.example):
| Variable | Default | Description |
|---|---|---|
GEMINI_API_KEY | โ | Gemini API key (required) |
GEMINI_MODEL | gemini-3.7-flash | Model used for summaries |
GEMINI_FALLBACK_MODELS | gemini-3.6-flash | Comma-separated models tried if the above is unavailable |
GMAIL_USERNAME | โ | Sender Gmail address |
GMAIL_APP_PASSWORD | โ | Gmail app password |
RECIPIENTS | sender | Comma-separated recipients |
NUM_STORIES | 30 | Stories per email |
MIN_SCORE | 0 | Skip stories below this score |
MAX_COMMENTS | 6 | Top comments fed to the summarizer |
BATCH_SIZE | 8 | Stories summarized per Gemini request |
DISPLAY_TIMEZONE | Europe/Warsaw | Timezone for timestamps shown in the email |
DISPLAY_TZ_LABEL | (empty) | Force a fixed tz label; empty = DST-aware (CET/CEST) |
RUN_ONLY_AT_LOCAL_HOUR | (empty) | DST guard: only run at this local hour on schedule |
FETCH_ARTICLES | true | Also fetch article bodies for context |
REQUEST_DELAY_SECONDS | 6 | Pause between Gemini batches (free-tier pacing) |
GEMINI_TIMEOUT_SECONDS | 90 | Hard cap on a single Gemini request |
SUMMARY_DEADLINE_SECONDS | 600 | Total budget for summarizing; then fall back |
DRY_RUN | false | Write HTML file instead of emailing |
Notes & troubleshooting
- Gemini free-tier rate limits are per-minute. Stories are summarized in
batches (
BATCH_SIZE, default 8), so 30 stories cost only ~4 requests. If you still see429retries, lowerBATCH_SIZEor raiseREQUEST_DELAY_SECONDS. 404 model not available? Model IDs get deprecated. Runuv run python -m src.list_modelsand set theGEMINI_MODELvariable to a listed one.- Every summary says
(summary unavailable)? The model was unreachable for the whole run, usually a sustained503while Google rebalances capacity for a popular ID.GEMINI_FALLBACK_MODELSexists for exactly this: the run switches to the next model listed and stays there. If even the fallbacks are down, the digest still sends with placeholders rather than failing. - Some articles won't be fetched (paywalls, JS-only, PDFs, videos). The summary then falls back to the title + HN comments, which is usually enough.
- Email in spam? Mark it "not spam" once; sending to yourself is very reliable.
- Scheduled runs can be delayed by GitHub during peak load, and occasionally
skipped altogether. Roughly 40 minutes late is typical here; we have also seen
11 hours, and days with no run at all.
scheduleis best-effort and GitHub offers no delivery guarantee, so this is normal Actions behaviour rather than a bug in the digest. Run the workflow manually from the Actions tab to catch up. - A run that seems stuck is bounded from two directions: every Gemini request
gives up after
GEMINI_TIMEOUT_SECONDS, and the whole summarization phase afterSUMMARY_DEADLINE_SECONDS, after which remaining stories get placeholder summaries so the email still goes out. The job's owntimeout-minutes: 30is only an outer backstop; reaching it means something is genuinely wrong.