Programmable Feeds: RSS Is the Bus

When I launched emit it did one thing: poll your blog’s RSS feed and email new posts to your subscribers. Two months later it does considerably more than that, and the interesting part is that almost none of the new surface required new plumbing. It all fell out of one decision made early for boring reasons, which is that everything inside the system is a feed.
The shape of the product now
The taxonomy I landed on is four words: feeds in, broadcasts out. A feed is anything that produces items over time. A broadcast is one delivery of some items to some audience. Between them sits whatever transformation you want.
Concretely, the things that can be a feed now:
- An RSS or Atom URL, the original case.
- A web page with no feed at all, watched by diffing its links.
- A prompt. You describe an interest in plain language, emit finds candidate sources, monitors them, and runs a relevance filter over everything new. What comes out the other end is a normal Atom feed at a durable URL, so you can subscribe to it in any reader.
And the things a feed can be delivered into:
- Email, to a double-opt-in subscriber list on your own domain.
- A Slack or Discord channel.
- A signed webhook with retries.
- A reader inside the dashboard, for when you want to see what a feed is doing before you point it at anyone.
Every one of those combinations works, and I wrote roughly one adapter per row rather than one per pair.
Why RSS in the middle
The reason this stayed small is that the interchange format is RSS, and RSS is the one thing every source and every sink already speaks. A page scraper’s job ends when it produces feed items. The email engine’s job starts when it receives feed items. Neither knows the other exists.
Compare that to the version where each ingress talks directly to each egress. Three sources and four destinations is twelve integrations, each with its own field mapping and its own bugs, and adding a fifth destination means writing three more. With a bus in the middle, three sources and four destinations is seven adapters, and the fifth destination is one.
Picking an internal interchange format is a normal architecture decision and people make it all the time with JSON envelopes and event schemas. The thing I would push on anyone building something similar: consider whether a boring public standard can be your internal format. RSS gave me a spec I did not have to design, a debugging story where every intermediate stage is a URL I can open in a browser, and a free feature, which is that any intermediate feed is also a legitimate product surface. The prompt-built feeds are the clearest case. They exist because the filter had to write its output somewhere, and the somewhere was a feed, and a feed at a stable URL is something people want on its own.
What the transform layer actually does
The middle of the bus is where the LLM lives, and it does two jobs.
The first is relevance filtering. Each new item is judged against the watcher’s prompt and gets a score plus a written reason, both of which ride along on the emitted item. Items that fall below the cutoff are suppressed but kept, so the question every filter eventually gets asked (“why didn’t I see X?”) has an answer you can look up instead of guess at.
The second is a borderline tier that I like more than I expected to. Items scoring within a narrow band of the cutoff get their full page fetched and re-judged on real content instead of on a feed excerpt. Feed summaries are frequently useless, and this recovers the cases where a dependency release note buries the one line you care about deep in the body. That pass is capped per cycle, because the point is to spend a little more attention on genuinely ambiguous items, not to fetch everything.
Everything in the transform layer fails open. Discovery runs on one vendor, filtering on another, rendered page fetching on a third, and any of them can be absent or down. When the filter cannot run, items pass through unjudged rather than getting stuck, because a newsletter that goes out slightly unfiltered beats a newsletter that does not go out.
The pricing consequence
A bus with a model in the middle has a cost structure that a plain RSS mailer does not, and I decided early that the customer should still see one meter. Credits are denominated in emails, one credit is one email, and the filter debits the same balance: one credit per 4,000 characters judged. Polling is free, serving a feed is free, and if you have no LLM key configured the filter is free because it is a pass-through.
Metering the filter by characters rather than by items was the right call and took me an embarrassing amount of time to see. Items vary wildly in size. The longest real dependency release note I measured runs about 13,600 characters, and headlines run about 80. Charging per item makes the headline watcher subsidize the changelog watcher. Charging per character means a credit buys a fixed volume of judged text and the cost lands where the work is.
What I would do differently
The sequencing. I built the prompt-to-feed watchers before the transform layer was general, so for a while the filter only worked on watcher feeds and not on a plain RSS URL you brought yourself. That is backwards. The filter is the valuable part and it should have been available to every feed from the start, with prompt-built sources arriving later as one more way to get items into the bus.
If you are building something with this shape, my one piece of advice is to write down the noun list first. I spent two months with “feeds,” “watchers,” “broadcasts,” and “sinks” all meaning slightly overlapping things, and the cleanup was mostly renaming. Feeds in, broadcasts out took an afternoon to apply and made every screen easier to explain.
Linked from
- Emit: Pay for Emails, Not Subscribers I built emit, an RSS-to-newsletter service that charges $1.60 per thousand …
Stay in the loop
Get notified when I publish new posts. No spam, unsubscribe anytime.