Case study · 2026In production, every morning
Dinapatra: a newspaper that checks its own facts.
An AI-assisted personal newspaper. Every morning it collects reporting from verified public sources, works out which claims independent publishers actually agree on, writes original articles from that evidence, lays them out as a print-ready PDF and sends it to Telegram. It runs on GitHub Actions for about a dollar a month.
Figures from the 24 October 2026 edition. Its quality report passed with zero errors.
The problem
Summaries are easy. Trustworthy summaries are not.
I wanted one paper each morning covering India, the world, Bengaluru, business, technology and sport, readable on my phone or printed double-sided. The obvious version, "feed RSS into an LLM and ask for articles", fails in predictable ways. The model invents a figure, a quote drifts, five outlets reprinting the same wire story look like five confirmations, and a paywalled page gets scraped when it shouldn't be.
So I treated it as an engineering problem about evidence, not a prompting problem. The model is one replaceable component, and everything it writes has to survive deterministic checks before it reaches the page.
Constraints I set
- No fabrication. Every figure, quote, date and name must trace to a source.
- Fully legal: public feeds only, robots.txt obeyed, no paywall or bot-block workarounds.
- No server to run, and my laptop can stay off.
- Costs pennies a day.
- Still produces a paper if the AI provider is down.
What I built
- A six-stage pipeline: collect, identify, verify, write, illustrate, publish.
- Seven grounding checks between the model and the page.
- A newspaper layout engine written from scratch on PDFKit.
- A quality gate that publishes failures as "NOT READY" rather than hiding them.
- Serverless daily delivery via GitHub Actions and a Telegram bot.
How it works
Six stages, every one auditable.
-
Collect
72 sources are configured and 56 are enabled. Each one was verified as permitting automated access. RSS, Atom and RDF are parsed with robots.txt checks, per-host rate limits and HTTP caching. For the stories that make the paper, the two or three strongest article pages are read. Navigation, promos and paywall prompts are stripped, and a paywalled or challenge page yields nothing.
-
Identify
Duplicates are removed by canonical URL, content hash and SimHash near-text matching. Reports of one event are clustered using TF-IDF similarity, headline overlap, named entities, shared figures and timing. Wire copy run by five outlets counts as one piece of evidence, not five.
-
Verify
Claims are extracted sentence by sentence and classified as fact, official statement, allegation, preliminary finding, forecast or opinion. A claim backed by two independent publishers is corroborated, one backed by a regulator, court or agency is primary-confirmed, and conflicting figures are contested, with both accounts printed.
-
Write
Claude writes each article from its evidence bundle. Feed text is fenced and labelled as untrusted data, so instructions hidden in a source can't steer the model. If a draft fails grounding, a deterministic extractive writer takes over, and article length follows the evidence instead of being padded.
-
Illustrate
Vector illustrations and charts are drawn from each story's verified facts. A story whose evidence names no concrete subject gets no picture. Every image is validated (blank, sparse, off-subject, low-resolution) and labelled as generated artwork, never presented as a photo.
-
Publish
A custom column-flow engine paginates the edition with justification, orphan and widow control, cross-page continuations and per-section templates. It renders a PDF with embedded Unicode fonts and clickable source links, then an editorial quality gate and a PDF validator run before release.
The core idea
Keeping the model honest: seven grounding checks.
Every draft the model writes is checked against the evidence it was given. Failing sentences are removed. If more than half a draft goes, or any hard check fails, the whole draft is discarded and the deterministic writer is used instead. Every rejection is logged in the edition's quality report.
Every number must appear in the evidence, within a rounding tolerance.
Must match a collected source verbatim. A paraphrase in quotation marks is rejected.
Must appear in the evidence or be a source's publication date.
A person or organisation that appears in no source is flagged.
"According to X" is rejected when X isn't among the story's sources.
A run of 18 or more words matching a source is rejected as reproduction, not summary.
Each sentence must be backed by an evidence sentence, which catches invented colour like "residents expressed concern".
Why the cheapest model is enough: since correctness is enforced by the checks, not by the model, a stronger model buys smoother prose, not more accurate news. That's why Dinapatra runs on Claude Haiku at about 3–4 cents an edition.
Infrastructure
Serverless delivery, every day at 05:53 IST.
Pipeline design
- The schedule is set off the hour mark to avoid GitHub's busy-queue delays.
- The test suite gates generation, so a broken change fails loudly instead of printing a broken paper.
- A failed quality gate still publishes, flagged "NOT READY", and the run is marked failed.
- A concurrency group stops two editions from running at once, and each release is keyed by date.
- If the Claude API is unreachable, the extractive writer still produces the paper.
Supply-chain and runtime security
- Every third-party action is pinned to a commit SHA, and the runner image is pinned to ubuntu-24.04.
- Headlines are untrusted, so they reach the shell only through environment variables.
- Least-privilege token:
contents: writeonly. - SSRF protection: private, loopback and cloud-metadata addresses are refused, and redirects are re-validated at each hop.
- Secrets live only in the environment and are redacted from logs.
Cost
About a dollar a month.
| Component | Choice | Monthly cost |
|---|---|---|
| Compute | GitHub Actions, ~4 min a day within the free 2,000 minutes | $0 |
| Writing | Claude Haiku, ~70 calls per edition, at low effort | ≈ $1 |
| Storage | SQLite on the runner, with each edition archived as a GitHub release | $0 |
| Delivery | Telegram Bot API | $0 |
Engineering decisions
Trade-offs worth explaining.
Why write a layout engine instead of rendering HTML in headless Chrome?
Chromium is a 150 MB dependency and gives little control over where a column breaks. PDFKit handles font embedding, vector drawing and precise text measurement. On top of it I wrote line breaking, justification, column flow, pagination, orphan and widow control and cross-page continuations. The result is a fast, deterministic render with selectable text and clickable source links.
Why only four runtime dependencies?
Fastify, fast-xml-parser, PDFKit and pg. TypeScript runs directly through Node's native type
stripping, so there's no build step, and SQLite uses the built-in node:sqlite
module. A small dependency tree makes a daily unattended job easier to trust and keep running.
Why does a blocked source stay blocked?
Being publicly reachable doesn't make content free to republish. When a publisher returns a bot challenge or HTTP 403, the source is recorded as restricted and left alone, never worked around. The quality report names every source that failed, so coverage gaps are visible instead of silently filled.
Why publish a failing edition at all?
Hiding failures teaches you nothing. A paper that fails its editorial gate is still delivered, clearly marked "NOT READY", with a report explaining exactly why. Editorial quality and technical rendering are separate verdicts: a PDF can render perfectly and still carry a broken headline.
Why SQLite, with PostgreSQL also supported?
A personal tool shouldn't need a database server, so SQLite is the default. Both databases share
one schema through a driver abstraction, and DATABASE_URL switches to PostgreSQL. The
test suite covers portability between them.
Stack
Built with
- TypeScript
- Node.js 23
- Claude API (Haiku)
- GitHub Actions
- PDFKit + custom layout engine
- Fastify
- SQLite / PostgreSQL
- TF-IDF · SimHash clustering
- Telegram Bot API
- Ollama (local option)
- node:test, 297 tests
Want to see an edition?
The code lives in a private repository, because the paper summarises other publishers' work for personal use. I'm happy to walk through the code or share a sample PDF.