Back to projects

Daily News

2025

A Discord bot that pulls news feed every morning, using an LLM API to rank and theme them against a written profile of what I care about, and DMs me the result at 7am.

  • asyncio
  • discord.py
  • Claude API
  • APScheduler
  • Automation

The core idea


Every morning was the same five or six news sites, opened one at a time, and most of what was on them had nothing with what I care about. The reading was never the problem. The repetition was, and it cost me the same twenty minutes every day.

So I needed to solve this minor issue of mine. I decided to automate this process instead by having all the sources come to me instead. A bot pulls the feeds, ML model ranks them against a description of what I actually care about, and the brief lands in Discord at seven. There is no site to visit and nothing to check.

How it works


Four stages left to right: news sites, fetch, LLM rank, Discord DM. Under each is a fallback: gated RSS is scraped, a timed-out feed is skipped while the rest still land, malformed JSON falls back to fetch order, and a failed DM is posted to the channel.
Fetch, rank, deliver. The dashed paths are what happens when a step fails.
  • All the sources live in a single list in the config, each one a category, an RSS url, and optionally a scrape url with a CSS selector for feeds that gate their content. Adding a paper means adding a line.
  • The news sites are fetched concurrently over aiohttp with a ten second timeout on each. feedparser is synchronous, so it runs in an executor rather than blocking the event loop the bot itself is running on.
  • Bloomberg and the FT gate their RSS, so those entries fall back to scraping headlines with BeautifulSoup. A feed that times out logs the failure and returns nothing, and the other twenty-one are unaffected.
  • Only the title, the first 200 characters of the description, and the url are sent to an LLM. Article bodies never leave the fetcher, which keeps an entire morning of news inside a single request.
  • The prompt goes out as plain text and comes back as a JSON object: a short tldr, plus an array of cards each holding its theme, tier, title, hook, why, source, url and score.
  • Relevance is a paragraph of plain text in the config that gets dropped into the prompt verbatim. It describes the reader, weighting AI and ML, developer tooling, and the AI job market above everything else.
  • The articles are assigned a final theme from a fixed set of four: AI & Tech, World, Markets and Builder. The category attached to the feed is only a hint, so stories group by subject rather than by which site they came from.
  • The daily job runs on APScheduler's async scheduler, sharing discord.py's event loop and starting once the bot connects. An hour of misfire grace means a short outage delays the brief instead of skipping the day.
  • Failures degrade rather than cancel. Malformed JSON from the model falls back to fetch order, and a DM that cannot be delivered is posted to the channel instead.

What I learned


The interesting parameter is how aggressive the filter is. Loose filtering rebuilds the problem it was meant to solve; tight filtering silently drops things I would have wanted. Tuning that against my own reading over a few weeks did more than any change to how it fetched or formatted the news.

Making relevance a paragraph of prose instead of a scoring function was the right call. It is readable and it is one edit to retune. It also does not learn. The profile is fixed, so the tool only improves when I sit down and rewrite it.

Made me more likely to read the news because of how simple it is to read the news.