Skip to main content
← Blog

I replaced the server with a prompt: a self-running AI news digest for $0

· Mick Brzeziński

  • AI agents
  • Claude Code
  • build in public

I built a daily AI news digest, then deleted the server it ran on. The cron daemon is now a prompt, the database is a git branch, and the editorial bills to a subscription I already pay for — $0 on the metered API account.

The whole thing runs as a scheduled Claude Code routine: an ephemeral cloud container wakes once a day, runs the pipeline unattended, deploys to Cloudflare Pages, and commits its history to a GitHub branch so nothing is lost when the container evaporates.

From the outside it’s one clean static page a day — a themed headline, a short overview, 4–8 top stories (each with a technical summary, a “why it matters” line, and verbatim source links), up to 10 quick links, RSS, and an archive.

The pipeline: ~28 sources → fetch → cluster → rank → enrich → digest → build, splitting to Cloudflare Pages and a gh-pages branch. Blue stages are cheap and deterministic; the single orange stage (the Opus digest) is the only expensive step.

The core idea: the LLM is the editor, not the reporter

The single most important decision: Claude never reports, it only edits. It works exclusively with articles the pipeline already fetched, and it copies URLs verbatim — it cannot invent a story, a fact, or a link. Every story cites at least one real, fetched article. Feeds are the foundation (structured, free, they never hallucinate).

Cheap deterministic code does the mechanics — fetch, dedup, score, select — and the expensive model does the judgment: which stories matter, how to phrase them, what the take is. That split keeps cost flat and makes the $0 rail viable.

The pipeline, stage by stage

A modular pipeline; each stage is independent and replaceable, exposed both individually (gonio fetch, gonio digest, gonio build) and as one gonio run.

Fetch reads a source registry, not a scraper. Sources live in config/sources.yaml — adding one is a YAML edit, never code. ~28 today, weighted by editorial trust: vendor/lab blogs highest (OpenAI, Anthropic, Google AI, DeepMind, Hugging Face), then big-tech infra, technical newsletters (SemiAnalysis, Simon Willison, Latent Space, Interconnects), tech press, and community signal (Hacker News, r/LocalLLaMA). A new fetcher type is one file with a @register("type") decorator. Requests go through a shared fetchclient that tries a cheap headers-only GET first and escalates to a residential proxy on a 403/429; a broken feed logs and is skipped, never fatal. Dedup is a two-layer pass in SQLite.

Cluster is a wide pass with Haiku: lexical dedup can’t tell that “OpenAI launches GPT-5” and “GPT-5 is here” are the same event, so Haiku groups the day’s articles by concrete event (same release, same paper; when unsure, keep separate). Two details worth stealing: a _repair step re-checks the model’s grouping in code — never trust its bookkeeping — and any error fails open to one-article-per-cluster, so the pipeline never dies because triage hiccuped.

Rank is deliberately boring arithmetic, no ML:

score = source_weight + recency_decay + keyword_boosts

Source weight from the YAML, linear recency decay over 48h, and boosts for high-signal technical terms. The clever bit: it scores whole event clusters, not individual articles (with a small multi-source corroboration bonus, capped at 5 links per event), so each prompt slot is a distinct story and one hot event can’t swallow the day.

Enrich happens after ranking, only for the handful of articles that made the prompt: the pipeline fetches the full article body (trafilatura), because feed summaries are often one line and empty for Hacker News. A dozen requests a day, not one per feed item — and it fails open: a dead link just falls back to the feed summary.

Digest is the one expensive call. Opus gets the selected clusters and a tight system prompt for the voice — technical, concrete, skeptical of marketing, dry, never clickbait — plus hard rules: work only with the provided articles, copy URLs exactly, 4–8 stories, drop low-signal items. It returns a Digest Pydantic model, a structured output, so it can’t return something unrenderable. A thin-day guard skips the call entirely if the 24h window has fewer than 10 articles: a quiet day is a free no-op, not a failure.

Build renders Jinja templates to static HTML, RSS, and a sitemap — and also emits agent-readable projections: /llms.txt, /latest.md, /md/<day>.md, /api/*.json. The digest is already structured, so HTML is just one projection; handing agents markdown and JSON costs nothing and pays off as more readers are machines.

The serverless trinity: GitHub + Routines + Cloudflare Pages

The pipeline used to run on a VPS with Docker Compose. It now runs on three free surfaces, none of them a server you operate:

  • Routines — the cron daemon is now a prompt. A scheduled Claude Code routine is an ephemeral container that runs daily (~06:40 UTC, after EU/US feeds update) with your repo cloned in. The “program” is a prompt: the operational runbook lives in ROUTINE.md and gets pasted in. The orchestration logic is natural language, version-controlled, executed by an agent that can reason about failures rather than just exit non-zero.
  • Cloudflare Pages — serving. Deployed via Direct Upload (wrangler), not CF’s Git integration — token-driven, no dashboard step. Free tier, global CDN, HTTPS, custom domain one CNAME away.
  • GitHub — durable memory. The container is ephemeral; when the routine finishes, the SQLite DB is thrown away. So history lives on a gh-pages branch used as a private store (read from git, not HTTP): one digest JSON per day, the raw article corpus with full bodies, and an audit log. Each run rehydrates it at startup and re-snapshots it at the end as one force-pushed commit.

The serverless trinity: a scheduled, ephemeral routine (compute) sits between the GitHub gh-pages branch (durable state, rehydrated each run and persisted first) and Cloudflare Pages (serving). Numbered flow: 1 rehydrate, 2 fetch, 3 persist first, 4 deploy second.

Two ordering rules carry the design. Persist before you publish: history is committed first, deployed second, so a failed deploy can’t lose data. And keep the private corpus off the public CDN: full article bodies stay on the private branch and never reach the site dir, so the CDN serves the digest and links while the raw text stays private.

The $0 question: subscription vs API

There are two billing rails behind one interface. The default is subscription; the paid API rail is opt-in, so a stock run cannot accidentally spend API tokens.

RailHowBilled to
apianthropic.Anthropic().messages.parsePer token, your ANTHROPIC_API_KEY
subscriptionclaude_agent_sdk.query with structured outputsYour Claude plan’s usage — $0 on the metered API account

You can’t shortcut this by using a Pro/Max token as an API key: a subscription token is inference-only, the Messages API rejects it, and as of February 2026 Anthropic prohibits using OAuth from Free/Pro/Max plans in third-party integrations outside Claude Code and Claude.ai. What is sanctioned: the routine already runs on your subscription (Claude Code on the web is Anthropic’s first-party harness, not the third-party kind the ban targeted), and Agent SDK usage draws from your normal subscription limits, not the metered API. So the generation runs through the Agent SDK with output_format={"type":"json_schema"}, which keeps schema validation — and I verified its output matches the API rail’s in quality.

A moving target (verified June 2026). Anthropic announced that Agent SDK / claude -p usage would move to a separate non-rollover monthly credit ($20 Pro / $100 Max-5x / $200 Max-20x). The change was paused before it shipped; per Anthropic’s docs it still draws from your subscription’s usage limits. Either way the conclusion holds: generation is $0 on the metered API account, and even at API rates a daily ~$0.12-equivalent run sits far inside the smallest ($20) credit. Re-check before quoting exact numbers.

List prices: Opus 4.8 $5/$25, Sonnet 4.6 $3/$15, Haiku 4.5 $1/$5 per 1M tokens. Two calls a day (Haiku triage + Opus digest), with the real cost logged every run:

PathCost / run~ / month
API key, Opus (with full-text enrichment)~$0.12–0.14~$4
API key, Sonnet digest~$0.08~$2.5
Subscription rail (default)$0 API$0

The subscription rail isn’t free of everything — it’s ~2× slower than the API rail and leans on the Claude Code harness (~40k cached tokens/run, plan-side). On a subscription error the default rail fails rather than falling back, and the paid fallback only fires under explicit auto with a key present.

Designing for “no human is online”

Running unattended shapes everything. Never block on questions. Never deploy a partial or empty site — abort only on a real catastrophe like a zero-article run. On any unrecoverable failure the agent diagnoses which stage broke and opens a GitHub issue with the relevant log tail instead of dying silently.

The unglamorous part is encoding expected soft-failures as facts. The residential proxy is blocked in the routine environment, so two feeds reliably 403 — the prompt says up front that those two are expected and don’t count, while any other failure is worth a look. That environment knowledge lives in version control next to the code. And because the container vanishes, observability is committed too: one JSON line per run in _data/runs.jsonl (article counts, failed sources, token usage, cost_usd) — a greppable history with no external logging service.

What this unlocks

The same structured digest becomes many things almost for free. A read-only MCP server exposes the news DB to other agents — the latest digest, any past day, and full-text search across the entire fetched corpus, not just the published picks. Because the raw corpus is persisted, any past day can be re-digested with new logic or a different model: digests are a derived cache, the corpus is the source of truth. The obvious next channel — a real email newsletter — is one new module in publish/; auto-posting the top story to X or LinkedIn is another adapter, on the same $0 rail. Once you have a clean structured artifact and a serverless publish pipeline, a new channel is a small additive change, not a new system.

Caveats

  • Feeds-only coverage (for now). RSS + Hacker News. A story that only breaks off the source list, or on a site with no feed, isn’t seen. A web-search fetcher is on the roadmap as an additional input.
  • The subscription rail is bounded. It draws down your normal subscription usage, and there’s a daily cap on routine runs — fine for one digest a day, not an unlimited compute faucet. Routines + Agent-SDK billing are research-preview and in flux; pin the behaviour you rely on.
  • No semantic dedup, single-writer SQLite. Clustering is one Haiku pass over titles — cheap, but not embeddings; you’d want vector dedup at higher volume. And db.py is the only storage-aware module, so it’s swappable, but today it’s a deliberate single-box constraint.
  • Quality ceiling = model + inputs. The editor only sees what the pre-ranker selected and what enrichment could fetch; a bad selection or a blocked body caps the day.

The real lesson: software without servers

The digest is the product; the reusable part is the operating model — a stateful, scheduled application with no servers, billed to a flat subscription instead of metered API. Swap the domain logic and the same near-zero-cost, self-healing pipeline runs almost any “generate something useful every day and publish it” idea.


Fact-check (verified June 2026): Claude API pricing; the subscription-OAuth ban; the paused June 15 Agent SDK billing change and that Agent SDK usage still draws from subscription limits. Routines and subscription-billing for the Agent SDK are a research-preview, actively-changing area — re-verify before republishing.