Signalfeed: I Stopped Letting the Algorithm Choose What I Read, and Built My Own Timeline

Every feed that reaches you automatically is optimised for someone else. Here is why I think every thoughtful reader now needs their own aggregated timeline, and how I built mine: X, LinkedIn and a dozen news sources, judged post by post by a System 1 model against my own definition of signal, with cross-source summaries, offline reading on the MRT, and a weekly check that the filter is still honest.

A person at a small desk reading one calm column of cards on a phone, while behind a sheet of dark glass three huge billboards churn with viral posts, adverts and notification bells. A small dark lens in the glass lets only a thin stream of cards through to the reader.
Three loud feeds on one side of the glass, one quiet column on the other. The lens in between is a decision model with my taste written into it.

Ten days ago I stopped opening X, LinkedIn and the news sites to find out what is happening in AI. Not as a detox. I still read more of that content than I did before. It just reaches me differently now: a scheduled job reads those feeds for me, a small model judges every post against a paragraph I wrote describing what I actually want, and whatever survives lands in one quiet column on my phone, each card with a link back to the original. The apps are still installed. I open them to post, to reply, and to read the one thing the column pointed me at. I no longer open them to browse.

This post is about why I think that has to become normal for anyone who reads to learn, and then, in as much depth as I can manage, how the thing I built works: the architecture, how often it scrapes, the exact questions the model is asked, how the news sources are pulled in and folded into stories, how the summaries are written, what it costs, and how I check every week that the filter has not quietly gone wrong. The app is called Signalfeed. The model at the centre of it is Jev, which I wrote about at length in the previous post. You do not need that post to follow this one, but the two are a pair: that one was about what a System 1 model is, this one is about living with one.

If you would rather watch than read, there is a companion video for this post on my YouTube channel, The Agentic Engineer: twenty minutes covering the same ground, with my real feeds on screen, the architecture drawn out, the four questions, a live tour of the app and the Sunday benchmark against Claude Opus. The video makes the case. This post is the reference, with the exact wording that goes over the wire, the verdict rule and the numbers.

I Built My Own Social Feed With a System 1 AI Model (Jev), the companion episode on The Agentic Engineer

The feed stopped being a feed

Here is the pattern I had fallen into, and I suspect you recognise it. Two or three times a day I would open LinkedIn with a specific intention: see what people are building with agents, what new tool or library or concept is doing the rounds, pick up a cue I can go and try at work or in a side project. That is the whole value of these platforms for me. People I respect post the first signal of something, weeks before it becomes a blog post or a conference talk, and if I catch that signal early I get to tinker early.

What I actually got was five to ten posts, a growing sense of having wasted the minute, and a close. Someone’s video. Someone sharing someone’s update about a promotion. A carousel of ten tools I must know. An argument. An advert dressed as an insight. Occasionally, and it felt like a lottery, the very first post was exactly the thing: a practitioner describing what broke when they wired a decision model into their agent loop, with numbers. Then I would disappear down that rabbit hole for an hour, happily. But the hit rate was terrible, and I could feel it getting worse.

I did the things you are supposed to do. I unfollowed generously. I muted the people who had switched from building things to posting about building things. I used “not interested”. On X I moved to the Following tab. None of it held, because none of it changes the objective the ranking is optimising for. The platform is not trying to show me what I want to read. It is trying to keep me on the page, and the content that keeps the median person on the page is average, loud and viral. Those are not insults, they are the mathematics of a feed ranked by engagement across millions of people. Anything that reaches you automatically, at scale, has to be the content that works on average. The content that works on you specifically has to be reached.

Two funnels. On the left a faceless corporate machine pours adverts, trending flames and engagement bait down onto a tired reader who did not choose any of it. On the right the reader holds the funnel themself, with a hand-written page of their interests clipped to the rim as the filter, and only a few bright cards fall through onto a calm reader.
The same river of posts through two funnels. The difference is who wrote the filter.

That sentence is the whole premise of this post, so let me say it plainly. If information arrives at you by itself, it is average or it is viral. If you want the good stuff, the one build log in a hundred posts, you have to go and get it, and the only way to do that at the volume these platforms produce is to put a filter of your own in front of them. Not the platform’s filter with your tweaks. Yours, written in your words, running on your machine, answering to nobody else’s objective.

And it is not only AI content. I read a lot about the world too: what is happening in the United States, in India, in Singapore, in the wider economy. I noticed the same thing there, more sharply. Every outlet and every influencer is telling a narrative. Very few are telling the news. The only way I have found to form an objective view of anything is to see three or four of those narratives next to each other, from the sources themselves, and notice where they disagree. That is a second timeline I have not built yet, and I will come back to it at the end. The point is that the problem is general. The tooling to fix it has only just become cheap enough to build on a Sunday.

What a feed is for

Before the how, one more paragraph on the why, because it decides every design choice below.

I do not want a feed to entertain me. I want it to be an index. The best outcome of opening it is that I close it within a minute and go somewhere else: to a repo, to a paper, to the author’s own post where I can read the comments and reply. The feed’s job is to surface cues, which is why every card in Signalfeed has exactly one prominent button, “Open on LinkedIn”, “Open on X” or “Open primary source”, and why the card itself is deliberately not a place to linger. That turned out to be the biggest behavioural change of the whole project, and I will explain it properly later, but it needs saying up front: a timeline you own is only useful if it points away from itself.

Why a lens on the feed was not enough

The first version of this idea was Jev Lens, the open source browser extension from the last post. It watches posts scroll into view on X and LinkedIn, sends each batch of eight to Jev with four questions, and paints a verdict on the card: a green border for READ, a dim for SKIP. It worked, it still works, and it taught me most of what is in the filter today.

It also showed me the limit of the approach. I still had to open the apps. I still had to scroll past the dimmed posts, which is an act of will when the dimmed post is a video with autoplay. The verdicts vanished when the tab closed, so there was no “what did I miss since yesterday”. It did nothing on my phone, where I do most of my reading, on the MRT, with one bar of signal in the tunnels. And it only ever saw the ranked feed the platform chose to render, so the algorithm was still upstream of my filter.

What I wanted was the inverse. Let the filter be upstream of me. Collect in the background, judge in the background, store what survives, and give me one page that opens instantly anywhere and never shows me a SKIP at all.

What Signalfeed is

One page. One column. No side-by-side cards, because I noticed my eyes only ever want to move down. Every card is a post from X or LinkedIn, or a news story, with the verdict and the model’s reasons in grey machine text under it, the full post text, and a single coral button that leaves the app.

The Signalfeed timeline on a desktop, Read view: three LinkedIn posts in one column, each with the author, a READ badge with a percentage, a grey line of machine text saying the kind of post, its depth score and its AI relevance, the post text in a serif face, and an Open on LinkedIn button.
The Read view on a laptop. Each card says what the model thought and why, in one grey line, and then gets out of the way.

The header has five views. All is everything that survived, newest first, with the posts I have already looked at dimmed. Unseen is the backlog. Read and Maybe are the two kept verdicts. News is the stories alone. There is a source toggle for X, LinkedIn and news, a search box, and a Settings screen with every threshold, every cadence and every cost on it. A card is marked seen when it has been at least sixty percent on screen for a continuous second and a half, so scrolling past something does not count as reading it, and the number in the header is honestly the number of posts I have never dwelt on.

Twenty seconds of it, recorded against the live app: the card anatomy, the Read and Maybe views, the source toggles for X only, LinkedIn only and news only, then the Settings screen where the cadence of every job is a dropdown and the whole filter is two paragraphs you can read.

Signalfeed in twenty seconds. One column; a verdict, a reason and a link on every card; Read and Maybe views; X, LinkedIn or news alone; the cadence dropdowns; and the two paragraphs that are the entire filter.

The same timeline on a phone, Read view, one card per screen with the author, READ badge, post text and the Open on LinkedIn button.
The phone, which is where most of the reading happens.

It runs on the Mac mini in my study, is reachable from my phone and laptop over Tailscale behind a passcode, and nobody else can see it. That last part matters for a reason I will get to: it reads my feeds from my own logged-in browser, so it is personal by construction.

The architecture

flowchart TD
    subgraph browser["Sathi Chrome, already logged in"]
        X["x.com/home
Following tab"] LI["linkedin.com/feed"] BPAGES["Anthropic newsroom
VentureBeat AI"] end subgraph feeds["Plain HTTPS, no browser"] RSS["9 RSS and Atom feeds
+ Hacker News API"] GN["Google News AI section
links decoded headlessly"] end SCHED["Sathi scheduler
cron per job"] -. fires .-> COL SCHED -. fires .-> NEWS browser -- "CDP :9222, scroll and read only" --> COL["social collector"] feeds --> NEWS["news collector"] BPAGES --> NEWS COL --> DB[("SQLite
one file")] NEWS -- "cluster into stories" --> DB DB -- "untriaged posts and stories" --> TRI["triage"] TRI -- "4 questions each, batches of 8" --> JEV["Jev
api.typesafe.ai"] JEV -- "ai, kind, depth, fit" --> VERDICT["verdictFor()
READ / MAYBE / SKIP"] VERDICT --> DB DB -- "kept stories, new or grown" --> SUM["summaries
claude -p on the subscription"] SUM --> DB DB --> SRV["Node web server :8792"] SRV --> APP["React timeline
service worker + IndexedDB"] APP -- Tailscale --> PHONE["phone, laptop"] LAUNCHD["launchd"] -. keeps alive .-> SRV

An isometric dark workbench: a browser on a clockwork timer peels cards off a social feed onto a conveyor, a stack of newspapers from different mastheads merges into a single card on a second belt, a compact lens stamps each card with a teal tick, an amber question mark or a faded cross, the crossed cards drop into a bin, and the survivors slide into a glass jar with a database icon from which one calm column rises into a phone.
The same picture without the boxes: scrape on a timer, fold the news, stamp every card, bin the crosses, keep the rest in one jar, read it on the phone.

There are deliberately few parts. A Node process per scheduled run, a single SQLite file, a plain HTTP server, a React page built by esbuild, and two external calls: Jev for every judgment and the claude command line for the summaries. No framework, no queue, no cloud. The whole repository is under ten thousand lines, and most of the interesting lines are in the config file and the question wording, not the code.

Three things about the shape are worth dwelling on.

It reads my browser, not an API. The collector attaches to a Chrome profile that is already logged in to X and LinkedIn, over the remote debugging port. It opens a tab of its own, goes to the feed, scrolls a fixed number of screens, reads the posts out of the page, and closes the tab. It is read only by contract: it never likes, follows, comments, hides or posts, and the one control it ever clicks is the Following tab on X. This is the same Chrome that my other automations drive, so there is no second login to maintain and nothing for the platforms to see except a user scrolling at a human pace.

The decision is made in code, not by the model. Jev is asked four closed questions and returns four typed answers with probabilities. The verdict, READ, MAYBE or SKIP, is a dozen lines of plain JavaScript over those answers. That means the rule is the same every time, it is unit tested, it is on the Settings screen, and I can change it without re-judging anything. The model is never asked “should I read this”, because that question has no stable answer. It is asked things that do.

Generation happens exactly once, and never for a post. The only place a language model writes prose is the news summary, one call per kept story, and that runs through claude -p on my existing subscription rather than an API I would have to pay per token. Everything else in the product is classification. That is why the whole thing costs cents.

How often it goes and looks

The cadence is the first thing I got wrong and the thing I would tell anyone copying this to think about carefully.

X throttled this account in July 2026 after a previous experiment scraped it too eagerly. So the X collector is gentle by design: every four hours, at seven minutes past, six screens of the Following tab, with a randomised one to two second pause between scrolls, which comes to roughly eighteen posts a run. LinkedIn runs every four hours at twenty-three past, fourteen screens, roughly twenty-six posts, because its feed renders fewer posts per screen. Both feeds are virtualised, so posts are accumulated screen by screen as they scroll past rather than read once at the end.

The news runs twice a day, at 06:30 and 21:30 Singapore time, because that is how often I actually read news and anything more eager would just move the same stories around. The two sources that have to be read through the browser run twenty minutes after that. And the benchmark, which I will come to, runs on Sunday morning for the previous day.

Job When (Singapore time) What it reads
X collector every 4 hours at :07 6 screens of the Following tab, about 18 posts
LinkedIn collector every 4 hours at :23 14 screens of the feed, about 26 posts
News collector 06:30 and 21:30 9 feeds, the Hacker News API, Google News; then stories, verdicts, summaries
News via Chrome 06:50 and 21:50 Anthropic’s newsroom and VentureBeat, one tab each
Benchmark Sunday 08:30 yesterday’s posts and stories, re-judged by Claude Opus

All of these are jobs in a scheduler I already run on the Mac mini for other automations, and each one has a dropdown on the Settings screen: every thirty minutes, every two hours, every four hours, twice a day, once a day. Picking one rewrites the job’s cron in the scheduler’s registry, mirrors it into the config file, and restarts the daemon so it is live. The line next to the dropdown is the cron read back from the registry, so it shows what will really fire, not what I wished for. Each social run takes about half a minute to collect and a few seconds per batch of eight to judge.

The prompt, or rather, the four questions

People ask what prompt I gave the model, and the honest answer is that there is no prompt in the sense they mean. Jev does not take instructions and produce text. It takes a piece of state, here a batch of eight posts, and a set of typed questions about that state, and returns an answer per question with a calibrated probability. These are the four, in the exact wording that goes over the wire, because Jev reads literally and the wording is what the thresholds were tuned against.

The first is a Noul, a yes or no with a probability:

posts[i] is substantively about AI, machine learning, LLMs, AI agents, AI developer tooling, or AI applied to any part of the software development lifecycle (requirements, design, coding, review, testing, release, operations). Mentioning AI in passing does not count.

The second is a Choice over nine kinds of post. “What kind of post is posts[i]? Judge by its text (and quoted_text if present).” The kinds, each with the criterion as sent: a research result or release explained with specifics; first-hand practitioner experience with concrete details, including when the author links to their own write-up or video about it; a tool launch that says what the tool does; a tutorial; an opinion or hot take; a news repost with little or no added insight; a listicle or engagement bait (“bookmark this”, “agree?”, hype with no specifics); self-promotion or a job post, which is explicitly defined as NOT including an author sharing their own technical article with a concrete description; and not about AI at all.

The third is a Score from zero to three. “How much concrete, actionable technical substance does posts[i] contain?” Zero is a slogan or vibe. One is a single claim with no evidence or mechanism. Two is a specific mechanism, numbers, code or first-hand observations. Three is a novel technical insight with evidence the reader could act on.

The fourth is the one that matters most, and it is where my taste lives:

The reader, a hands-on software engineer who builds AI agents and applies them across the software development lifecycle, would stop scrolling to read posts[i]. Judge the post against the criteria.

with two criteria, true and false, and each criterion is a paragraph of my own words. This is the profile, verbatim from the config file:

Agentic engineering: how people actually build and run AI agents (harnesses, tool design, Claude Code, Codex, OpenCode, MCP, skills, AGENTS.md), with first-hand detail. Anything hands-on about Jev, TypeSafe, System One models or RLCD: benchmarks, cost numbers, things people built with it. Evals, verification and calibration of models; RL post-training (RLHF, RLVR, RLCD, SFT, DPO, GRPO) explained hands-on, including repos you can run. Spec-driven development with coding agents (specs, acceptance tests, EARS, requirements to code). AI applied across the software development lifecycle: requirements and specs, design, coding agents, code review, test generation and automation, release and operations, browser and mobile automation without scripts, LLM-as-judge with numbers. Harness design guides. Developer tooling launches with concrete capabilities and real benchmarks. Build logs: someone tried a thing, reports what worked, what broke, cost and latency. Research results explained by the people who did them. An author linking to their own article, video or tool about these topics still counts as READ when the post says concretely what is in it. Examples of READ: ‘I spent 11 cents processing 3.1 million tokens with Jev, here is what I learned’; ‘Claude Code Skills explained by building them into a real test automation project, video inside’; ‘AGENTS.md support lands in Claude Code 2.1, here is how the loading order works’.

And the anti-profile:

Generic AI news reposts with no added insight; motivational or career-advice posts; listicles (‘10 tools you must know’, ‘bookmark this thread’); AI-generated hype with no specifics; certificates, badges and course completions; crypto, trading, politics; product ads and funnels; LinkedIn-voice storytelling with a lesson at the end and no technical content; engagement bait (‘agree?’, ’thoughts?’); job posts and hiring announcements. Examples of SKIP: ‘I just became a Claude Certified Architect’; ‘This is the cheapest AI provider, every model discounted’.

Those two paragraphs are the entire filter. Everything I learned tuning Jev Lens is in them: name topics, name anti-patterns, give literal examples, and never write “advanced AI content” because it means nothing to a model that reads literally. When I want the feed to change, I edit the paragraphs and re-judge. Re-judging four hundred posts costs about two cents.

Then the verdict, in code:

if (kind === "listicle_or_bait" || ai < 0.4 || fit < 0.3)            return "skip";
if (kind === "self_promo_or_job" && fit < 0.6)                        return "skip";
if (fit >= 0.65 && depth >= 1.0 && kind !== "news_repost")            return "read";
if (fit >= 0.8)                                                        return "read";
return "maybe";

READ and MAYBE reach the timeline, SKIP never does. Those thresholds were tuned on 96 hand-graded posts for the extension and agreed with my grades 86 to 91 percent of the time, which is why I have not casually adjusted them since. Ads never go to Jev at all: the page already said they were promoted, and paying to be told again would be silly.

flowchart LR
    P["post text
+ quoted text"] --> J["Jev
4 questions"] J --> A["ai 0..1"] J --> K["kind, one of 9"] J --> D["depth 0..3"] J --> F["fit 0..1
against my two paragraphs"] A & K & D & F --> V{"verdictFor()"} V -- "bait, or ai < 0.4, or fit < 0.3" --> S["SKIP
never shown"] V -- "fit ≥ 0.65 and depth ≥ 1
or fit ≥ 0.8" --> R["READ"] V -- "everything else" --> M["MAYBE"] R & M --> T["timeline"]

Why a System 1 model and not a chat model for this? Three reasons that were obvious the first week and are only more obvious now. Speed: a batch of eight posts with four questions each comes back in about three hundred milliseconds, so a whole run is judged before I have finished reading the log line. Cost: 4.2 cents per million input tokens, which makes “judge every single post, every four hours, forever” a rounding error. And honesty: the probability on the fit question is calibrated, so “READ 92%” on a card and “MAYBE 42%” on another mean what they say, and I can put thresholds on them that behave the same way next month. A chat model would give me a paragraph of reasoning per post and no number I could trust to branch on.

Where the news comes from

Posts tell me what practitioners are doing. They do not reliably tell me what happened, because by the time a release reaches my feed it is three reposts deep and the original announcement is a screenshot. So the second source is the news, from the publishers and the labs themselves, in three tiers.

The first tier is plain HTTPS with no browser at all: the OpenAI, Google AI and Hugging Face blogs, which are marked primary because when a lab writes about its own release that is the source; TechCrunch, The Verge, Ars Technica, MIT Technology Review, Wired and Simon Willison by RSS or Atom; and the Hacker News front page by its API, keeping stories with eighty points or more, with the points and comment count shown on the row.

The second tier is two pages that have no feed or will not serve one. Anthropic’s newsroom has no RSS, and VentureBeat rate-limits feed readers. Those two are opened in the same logged-in Chrome, one tab each, read and closed. Never clicked, never logged in to.

The third tier is the catch-all: the Artificial Intelligence section of Google News, as RSS, which carries sixty-odd stories a day from hundreds of publishers I would never add by hand. Google hides the real links behind redirect tokens. Signalfeed decodes them without a browser, because the article page carries two parameters and one request to Google’s own endpoint returns the publisher’s URL, and never stores a news.google.com address. A short deny list keeps the filler out before Jev looks at it, and the whole tier can be switched off in config. I should say plainly that Google’s feed is licensed for personal, non-commercial use, which is fine for a private app on my own machine and would not be fine for a public one.

Every fetch looks like a browser and gives up after fifteen seconds. A source failing never fails the run; one that fails three runs in a row is flagged on the Settings screen. Only articles from the last forty-eight hours are kept.

One event, one card

The problem with news is that a model release lands in TechCrunch, The Verge, Ars and the lab’s own blog within the hour, with four different headlines, and I want one card, not four. So articles are folded into stories, and this is where Jev does a second kind of work.

flowchart TD
    A["new article"] --> C["candidates: open stories from the last 48 h
whose headlines share two words,
or a proper noun on a short headline"] C -- none --> N["opens a new story"] C -- some --> Q["Jev, one Choice question:
which of these stories is this article reporting on, or none?"] Q -- "top answer ≥ 0.70" --> J["joins that story
card says +1 source"] Q -- "none, or below 0.70" --> N N & J --> T["story triage: 4 news-shaped questions"] T --> S["summary, one claude -p call
only for kept stories, new or grown"]

Two passes. First, code finds candidates cheaply: open stories from the last forty-eight hours whose headlines share enough words with the new article. Then Jev decides, with one Choice question per article where the options are the candidate headlines plus “none of these”, and the instruction says explicitly that two articles belong together when they are about the same event, not merely the same company. The article joins a story when the top answer clears seventy percent; otherwise it opens a new one. A story’s place in the timeline is its earliest article’s publication time and it never moves: when three more outlets cover the release during the day the card does not jump back to the top, it says “+3 sources since you read this” instead.

Getting this wrong costs little, which is why I was comfortable letting a cheap model do it. A wrong merge hides nothing, because every article keeps its own row and link under the story. A missed merge shows two cards. There are two entries in the card menu, “These are different stories” and “Belongs with the one above”, and they are recorded for tuning. The known gap is stories whose headlines share no words at all, which the candidate pass never offers to Jev. An entity pass is on the list.

Each story is then judged with four questions shaped like the post questions but written for news: is it substantively about AI; what kind of coverage is it (a primary announcement by the organisation that did the thing, reporting with the journalist’s own sourcing, analysis or opinion, an explainer written to rank in search, a roundup, or a press release rewritten); would this reader want to know it happened, judged against the same profile with a news clause added to the anti-profile (stock moves, “what it means for investors”, gadget reviews with an AI feature, society op-eds with no technical claim, roundups); and how consequential the event is for someone who builds agents and applies them across the software development lifecycle, from trivia to “changes how you work”. The same verdict rule applies, with “consequential” standing where the substance score stood.

The first run of that judged ninety-seven stories as zero READ. The anti-profile opens with “generic AI news reposts”, and to a model that reads literally, every piece of journalism is a repost of an event. The fix was one sentence at the front of the criteria: reporting an event with the publisher’s own sourcing is not a repost; judge whether the event matters. That is the kind of bug you only find by looking at the verdicts, and it is why the benchmark below exists.

One extra rule: an AI story on the Hacker News front page with two hundred points or more is never SKIPped outright, because that is a strong, independent signal that engineers cared, and the whole point of this exercise is to borrow the judgment of people I trust.

Who writes the summaries, and why they are the point

This is the part I am most pleased with, and it is the one place a language model writes prose.

A Signalfeed story card: Microsoft’s Nadella says AI needs an emergency brake that humans control. Three sources, CNBC, TechCrunch and The Verge. A paragraph summarises what the three publishers report and where their angles differ, then one row per publisher with a one-line angle, an expanded three-line summary for CNBC, and a Read the original link.
One event, three publishers, three angles, one paragraph that says where they differ. Every row links to the original.

A kept story gets one call, through the claude command line in print mode, on the same subscription I already pay for. The system prompt is short and the whole of it is: you summarise news coverage for a hands-on software engineer who builds AI agents and applies them across the software development lifecycle; British English, present tense, plainly, without hype; never em dashes or emojis; describe what each publisher says, naming the publisher when their angles differ; never invent facts that are not in the material; answer only with JSON that matches the schema you are given. The user prompt is the story headline followed by each article’s publisher, title, standfirst and the first fifteen hundred characters of its text, and a request for three things.

One paragraph of sixty to a hundred and ten words summarising what the coverage says, naming publishers where their angles differ. One line of at most twelve words per article, the publisher’s angle rather than its headline. And three or four lines per article, forty to ninety words, of what that article says, for the Show more expander, never the whole article rewritten. The answer comes back as structured JSON validated against a schema, and if it is the wrong shape, too long or too short, the card shows the primary source’s standfirst and the rows show their feed descriptions, and the failure is recorded so it is not retried every run.

Look at the card above and you will see why this matters more than the posts do. CNBC leads with the emergency brake and sets it against warnings from Gates, Amodei, Altman and Musk. TechCrunch concentrates on the trust architecture and separating models from the harness that orchestrates them. The Verge frames it as AI no longer being nested black boxes. Same post by Nadella, three different stories, and the paragraph says so. That is the thing I wanted from the general news too and have never had: the narratives, side by side, with the disagreement named, and the originals one click away.

Two engineering notes on the summaries, because both cost me an evening. First, the default claude -p loads your whole context: my global instructions, project instructions, skills and tool schemas, which was about forty-five thousand input tokens per summary. Run from an empty directory with no tools, no MCP servers, no slash commands, only project settings and a short system prompt, a call is about twelve hundred input tokens and two seconds on the small model. Second, the shell on that machine exports a dead API key from an account with no credits, so the child process has that variable removed from its environment, or the CLI tries the API, fails, and the card shows a standfirst. There is a cap of eighty stories a day, and the Settings screen shows today’s calls and the time spent in them.

Every card has one coral button. “Open on LinkedIn”. “Open on X”. “Open primary source”, which goes to the lab blog, the paper or the release notes when there is one, otherwise to the earliest article. Each news row has “Read the original”. I expected this to be a convenience. It turned out to be the whole design.

A feed you own is only useful if it points away from itself.

When I used the platforms directly, the post was the destination. I read it, I read the comments, I read the next one, and the next one was chosen by someone else. In Signalfeed the card is a pointer. I read the summary or the first paragraph, decide in a second whether I want to go deeper, and if I do I leave for the original, where the author is, where the thread is, where I can reply. Then I come back to the column, not to the algorithm. The platform gets exactly the visits I chose to give it and none of the ones it was engineered to extract.

Getting those links was harder than it sounds on LinkedIn, and I will spare you most of it. The 2026 LinkedIn feed carries a post link on about one card in eight. The post’s id is in the page, but encoded: every component inside a card is keyed with a base64 blob that turns out to be a tiny protocol buffer holding the post’s own numeric id, and the field number tells you which kind of URN it is. Decode that and you can rebuild the permalink for essentially every card without clicking anything, which matters because the only menu that offers “copy link” also offers Hide and Unfollow, and I do not want an automation anywhere near those. A card whose link cannot be resolved says so plainly and goes to the author’s recent activity instead of pretending.

Reading in the tunnel

Most of my reading is on the MRT, where the signal comes and goes. So the app keeps its own copy. Every time it opens on a network it quietly copies the timeline into the phone’s browser storage after the screen has drawn: every post I have not seen, plus the last week of what I have. With no signal, or with Tailscale off, it tries the server for three seconds and then shows the copy, with one honest line under the bar saying so and how old it is. Everything works against the copy except the Open buttons, which go grey and say “Needs network”, because the post lives on X or LinkedIn and that is not copied. Marking posts seen in the tunnel is queued and sent when I am back, with the time I actually looked. Three seconds, by the way, is where waiting starts to feel like a broken app; a phone with one bar hangs rather than fails.

Keeping the gatekeeper honest

Here is the uncomfortable question about any filter you put in front of yourself. If it quietly got worse, how would you know? Good posts would stop reaching the timeline and nothing would say so. You cannot see what was skipped. I wrote in the last post that I agreed with the extension’s verdicts around nine times in ten on posts I graded by hand, but that was one afternoon, on a profile I had just tuned, and I had no intention of grading posts by hand every week.

So once a week a much stronger model does it instead. On Sunday morning the benchmark takes every post and every story collected the previous day, the ones Jev skipped included, which is why skipped posts stay in the database for thirty days, and shows each one to Claude Opus in batches of fifteen, with exactly the text Jev saw, the same two paragraphs, and the three verdicts defined in plain words. Claude is never told what Jev said. It answers with a verdict and a one-line reason per item, through the same claude -p the summaries use, and a report compares the two.

flowchart LR
    DB[("yesterday's posts and stories
skipped ones included")] --> B["batches of 15
same text, same two paragraphs"] B --> O["Claude Opus
never told Jev's verdict"] O --> R["report: exact agreement, keep vs skip,
Cohen's kappa, confusion matrix,
every disagreement with both reasons"] R -- "keep vs skip ≥ 75% over ≥ 20 items" --> P["PASS"] R -- "below" --> F["FAIL, say why"] P & F --> TG["one line to Telegram
+ link to the report"]

The report counts exact agreement on the three verdicts and, more importantly, agreement on keep versus skip, because that is the decision that changes what I see. It gives Cohen’s kappa for both, which is agreement with chance taken out, a confusion matrix, the same numbers per source, and then the list that matters: posts Jev skipped that Claude would have had me read, because that is the direction that loses me something. Every row shows both verdicts, both reasons, the text and the link. A run passes when keep-versus-skip agreement is seventy-five percent or better over at least twenty items, and the paragraph at the top of the report is written by code from the numbers, not by a model, so it cannot flatter anyone.

What I did, in one line: three Saturdays so far, every post and story from that day, Jev against Claude Opus, with Opus never shown Jev’s answer. What I found is that the two agree on keep-or-skip nine times in ten, and the disagreements have a shape I can act on.

Day Items Keep vs skip Exact Kappa Missed Over-kept
3 October 415 90% 85% 0.74 1 2
4 October 385 91% 87% 0.77 1 2
10 October 299 89% 86% 0.69 1 0

All three runs pass the 75 percent bar. The two columns on the right are the ones I read. “Missed” is the direction that loses me something: a post Jev threw away that a much stronger model says I should have read. “Over-kept” is the direction that only costs me a minute: something Jev put in the READ pile that Opus would have binned. Across the three runs there were three of the first kind and four of the second, out of 1,099 items, and here they are, because the pattern matters more than the percentage.

Jev skipped, Opus would have had me read:

The post Jev said Opus said
LinkedIn: six Claude Code token-saving habits from an Anthropic webinar, with the commands listicle or bait, fit 81%. The rule skips every listicle concrete habits, READ
X, Matt Pocock: a “prompt of the day” that mines your last ten agent sessions to make the repo easier to navigate listicle or bait, fit 65%. Same rule concrete prompt, READ
News: Microsoft announces Microsoft-Decision-1, a fast decision model AI probability 12%, skipped before fit was even considered a model release relevant to agent builders, READ

Jev read, Opus would have skipped:

The post Jev said Opus said
LinkedIn: a job post for a Chief Data and AI Officer, written as a story practitioner insight, fit 82%, READ job post, SKIP
LinkedIn: a university satellite launch that mentions on-board inference practitioner insight, fit 76%, READ press release, SKIP
LinkedIn: a vendor post about bulk API changes across four hundred endpoints, ending in a question practitioner insight, fit 76%, READ product promotion, SKIP
News: Sean Parker rebuilding Stability AI around music reporting, consequential 2.2 of 3, READ business story, nothing about agents or tooling, SKIP

Two of the three misses are the same bug: my verdict rule treats “listicle or bait” as a hard skip, and a numbered list of concrete commands is a listicle by shape and a build log by content. The fix is a definition, not a model: the listicle criterion needs to say that a short list of concrete, runnable things is not bait. The third miss is almost funny. A model that reads literally saw “decision-making” and no “AI” in a headline and took it at its word, which for a post about decision models is a point worth conceding. The fix there is one more noun in the first question. The inclusions are all the same shape too: promotion wearing the clothes of a first-hand account, which is exactly the kind of post the profile says to skip and exactly the kind a model judging text alone finds hardest. I have not made either change yet, because I want another two Sundays of evidence first, and because the report will tell me whether the change helped.

This is the part of the project I will keep running. Jev will get new versions, Claude will get new versions, and my profile will keep changing, and the only way I will know the filter is still honest is a weekly number from a judge that is not the filter.

There is also a human loop, banked rather than wired. Every card has “Should be READ” and “Should be SKIP” in its menu. They do not change anything yet. They append to a feedback table, twenty-two rows so far, and when there are enough rows that table becomes the test set for the next profile: change a paragraph or a threshold, re-judge, and count how many of my own disagreements the new wording would have got right.

Ten days of numbers

Signalfeed started collecting on 2 October. As I write this on 12 October, it has collected 2,734 posts from X and LinkedIn. 312 of those were adverts, dropped by rule without a model call. Of the 2,422 that were judged, 195 came back READ, 327 MAYBE and 1,900 SKIP. That is 78 percent of my social feeds that never reached my screen, and a kept rate of 24 percent on LinkedIn and 18 percent on X, which matches my gut: X’s Following tab is noisier for me than LinkedIn is, which surprised me until I looked at what I follow there.

On the news side, 876 articles from thirteen sources were folded into 758 stories, 81 of them holding more than one article, up to four. 39 stories were READ, 143 MAYBE and 576 SKIP, so the news is filtered harder than the posts, which is right: most of what Google News calls artificial intelligence is a stock move or a gadget. 178 summaries were written.

The cost of all of that judging, every post, every story and every clustering decision, ten days, 4.2 million Jev input tokens: seventeen cents. Under two cents a day. The summaries and the weekly benchmark run on a subscription I already have, and are a small share of what my own sessions use. The scraping is my own browser, the feeds are public, the database is one file, and the hosting is a Mac mini that was already on.

And the number I actually care about: the 514 in the header this morning, the unseen backlog, is a number I can clear in ten minutes with a coffee, two or three times a week, and come away from having found the two or three things I would have spent an hour of scrolling to maybe find before.

How it was built, briefly

The whole thing was built with Claude Code, in a handful of sessions, as a sequence of GitHub issues: the app, then the offline cache, then the news, then the benchmark. Each issue was a written spec I had reviewed in an HTML plan with my decisions recorded in it, and each was built by a fresh agent session reading that spec, with a verify suite it had to keep green: unit tests for the pure functions, a build, a collector smoke test against fixtures, a news smoke test, and two end-to-end runs of the web app in headless Chrome, one of them with the network cut to prove the offline path. I will write about that workflow separately, because the interesting part was not the code, it was deciding what to specify.

The reason I mention it here is that the architecture above is exactly what a thoughtful person with a laptop and a weekend can build now. Nothing in it is clever. The model that makes it possible costs cents. The hard part is writing the two paragraphs.

What this is not

It is not a product, and I want to be honest about why. It reads my feeds from my own logged-in browser, at a human pace, read only, and it is personal by construction. Pointed at someone else’s account it would be a scraper, and I have no interest in the ban risk or in being the person who did that to LinkedIn’s terms of service on other people’s behalf. If a public version of this ever exists it will be news only, from feeds the publishers choose to offer, and the social half will stay private. The decision model, the questions, the verdict rule and the profile approach are all in the open source extension if you want to build your own.

It is also not a recommender. It never learns what I click. It judges each post against the words I wrote, and the only way the filter changes is that I change the words. I think that is the right default for anyone who has watched a recommender slowly bend their feed towards whatever they weakly clicked at midnight. The filter should be a document you can read, not a weight you cannot.

What comes next

The obvious next source is the one I started this post with: the general news. America, India, Singapore, the economy. Same architecture, a different pair of paragraphs, and summaries whose whole purpose is to put three narratives about one event next to each other and name the difference. I have no illusion that this produces truth, but I am fairly sure it produces a better-founded opinion than any one outlet’s push notification, and I would rather the model that picks my news answered to a paragraph I can read than to an engagement curve I cannot.

After that, Reddit through its official API, my YouTube subscriptions, and the entity pass for story clustering. And the tuning: the listicle criterion, the news wording, and the day the feedback table is big enough to score a profile against.

The larger claim, which I will stand behind: social media is about to become useful again, but only for the people who put their own filter in front of it. The platforms will not do this for you, because they cannot afford to. A System 1 model that reads literally, answers in a few hundred milliseconds and costs cents per thousand decisions is the first tool cheap enough to make it practical for one person, and I think a personal timeline like this will be as normal for thoughtful readers in a few years as an ad blocker is now. The ad blocker removed what they put in front of you. This removes what they chose to show you.

Everything that reaches you automatically is average or viral. The good stuff, you have to reach.

Signalfeed is how I reach it. If you build your own, write to me about your two paragraphs. That is the part I am still learning.

Keep reading

My Agents Don't Care About My Spelling: Why I Built My Own Voice Dictation App
Oct 17, 2026 · 19 min

My Agents Don't Care About My Spelling: Why I Built My Own Voice Dictation App

Jev and RLCD: The Model That Never Speaks, and Why Testers Should Care
Sep 21, 2026 · 22 min

Jev and RLCD: The Model That Never Speaks, and Why Testers Should Care