Tarang Deep Goel

Off the clock · Mar 2026

Impact News

An engine that traces each news story to what it changes in one person's life. It spent three weeks walking a hand-built map of cause and effect before a head-to-head test showed Gemini reasoning over the same vocabulary did better, and the map was deleted.

impact-news

When something happens in the news, what else does it move?

A crude oil spike is an energy story. For someone in Bangalore it might mean a pricier commute next week and dearer groceries next month. A repo rate decision is a markets story, but for someone with a floating-rate home loan it is an EMI change. Most feeds sort news by topic and by what people click, and I wanted to see if I could sort it by consequence instead.

This grew out of my news digest, which only ever asked whether an article matched my interests. I'd started caring about a different question: what a piece of news actually does to someone's life.

Today that is Impact News. It reads Indian economy, markets and world news, asks a cheap Gemini model whether an article describes a real shift in the world, asks a stronger one what that shift does to ordinary people, keeps a running picture of each story as it develops, and scores it all against a person's life on Telegram. Getting here took one big pivot: I spent three weeks hand-building a map of cause and effect, then tested an LLM against it, the LLM won, and I deleted the map.

How it works today

flowchart TD
    A["12 news feeds: economy, markets, business, world"] --> B["Drop duplicate stories"]
    B --> C["Quick check, cheap Gemini: is this a real shift?"]
    C -->|"no"| R["Set aside, with the reason"]
    C -->|"yes"| D{"Part of a story already running?"}
    D -->|"no"| E["Deep think, Gemini Pro: who feels it, 1st to 3rd order"]
    D -->|"yes"| F["Update that story with what changed"]
    E --> G[("Each story: its shifts and their impacts on people")]
    F --> G
    H["Telegram onboarding: city, work, transport, housing, family"] --> I[("The person's profile")]
    G --> J["Score against the profile"]
    I --> J
    J -->|"high"| K["Push now"]
    J -->|"medium"| L["Daily digest"]
    K --> M["Relevant / Not relevant"]
    L --> M
    M --> I

Everything rests on a vocabulary I split into two worlds, with one test for which side a thing goes: can a single citizen directly experience it in their body, wallet, schedule or environment? If yes, it's an impact, like retail petrol, home loan EMI or air pollution. If no, it's a trigger, like Brent crude, the RBI's policy or the monsoon. The two trees hold 381 nodes, with aliases in English and Hinglish ("Ghar ka Kharcha", "atta"), and every answer the model gives has to name one of them or it's dropped. I wanted that because free text can't be matched to a person, and a fixed list can.

I put a cheap model in front because most news doesn't change anyone's life. The quick check decides whether anything real happened and matches the article to a running story by root cause rather than topic, so a textile export piece and an energy supply piece can belong to the same Middle East crisis. Only what passes reaches the deep think, which I ask to reason in three orders: direct (crude up, petrol up), knock-on (diesel, freight, packaged food) and systemic (margin squeeze, wage pressure). Each story becomes its own small causal graph, built by the model rather than by me, and new articles revise it instead of overwriting it.

On the person's side, five onboarding questions switch on the parts of life that matter to them, on top of 17 that everyone gets (staples, LPG, electricity, medicines, heat, scams and so on) and defaults for their city. A thumbs-down makes the bot ask a follow-up like "not my industry?" rather than just lowering a score. To keep myself honest I test both stages: the quick check on 190 hand-reviewed articles (0.75 precision, 0.91 recall), and the deep think on 20 real articles where Claude writes the expected answers and Gemini predicts, so the model isn't marking its own style.

How I got here

1. A causal graph

The first version of Impact News was a bet against the LLM. I wrote it down in one line: "Don't ask the LLM to reason causally. Ask it to classify events. Let pre-built templates handle the causal chains." I thought an LLM would be fairly trustworthy on the first step of a chain (crude up, petrol up) and much less so by the third, so I'd build the chains myself and let the LLM only say where an article starts.

The map was a graph: nodes for things in the world (crude oil, diesel, freight, a monthly grocery bill) and links for "this moves that". I wrote five domain graphs, one of them built on India's CPI basket, and merged them into 1,688 nodes and 2,464 links. Gemini tagged each article, the tags were matched to nodes, and the software walked outwards to see whether it reached anything in the user's life. It reached everything. Nearly a third of the links only said "this belongs to that category", and a walk with no weights lit up the whole graph. So I gave every link a weight, a direction and a delay.

2. Two worlds and PageRank

A few days in, I threw that graph away and started again from a cleaner idea. TODO(tarang): why a clean slate rather than patching the merged graph? This is where the trigger and impact split came from, as rule 2 of what grew into a 21-rule rulebook. Gemini 3.1 Pro generated each tree a branch at a time, and on top went typed links (produces, supplies, disrupts, regulates and so on).

The walk was still flooding: from Brent crude it reached about 450 nodes, so I switched to PageRank, which brought it down to 33. The order was still wrong, though. I measured it with NDCG, a 0 to 1 score of whether the right impacts are near the top, and on a handful of historical events it was 0.16, because the walk favoured things in the middle of the chain over the end effects a person actually feels. Boosting those end effects took it to 0.41, and I took that as a sign the graph could get there with enough care.

3. Edge surgery and shock propagation

Late in March I lost the graph locally and restored it from an iCloud archive. TODO(tarang): what happened there? With it came a proper yardstick: 16 real Indian events, each with the ten impacts it should rank highest. And a new rule, direct causation: no link from A to C if the effect really travels through B. Most of the next four days went into applying it. A Gemini review killed 133 links, which broke scenarios that then needed new in-between nodes, like jet fuel between crude and flight tickets.

I also realised PageRank was the wrong idea. It imagines a walker splitting its chances between every link going out, but a real shock hits all of them at once, and it leaked a fifth of the signal at every hop, so anything one hop away beat the effects on people. I replaced it with something that just followed the strongest chain to each node. RBI to home loan EMI went from sixth to first, and Brent crude to flight tickets from 22nd to first.

The graph got a lot right. Every result came with a path I could read hop by hop, and it never produced an impact the tests said it mustn't. It forced me to define what an impact is, and to break one into probability, magnitude, delay and direction. What I couldn't see from inside was that the real number had stopped moving. On the 16 events it sat between 0.31 and 0.33 however much I cut and stitched, while the scenarios I'd written myself kept improving. Abstract triggers like the repo rate had no clean place to enter the map, and every article reached the graph: it had no idea of an article that simply doesn't matter.

4. The LLM beats the graph

By April the graph felt rigid and expensive to maintain, and I wanted to know if I'd been right to distrust the LLM in the first place. So I ran a fair fight: I gave Gemini both trees as plain text, asked it the same question the graph answered, and graded both on the graph's own tests.

On the hand-written cases the LLM found 60% of the impacts it had to find against the graph's 30%, and 80% once I fixed some node names in the cases. The graph kept a perfect record on impacts it mustn't produce while the LLM got 83%, but the LLM's misses looked like not knowing the person rather than bad reasoning, and it said far less, about 6 impacts per case against 27. On the 16 real events the LLM scored 0.454 to the graph's 0.313, won 10 and lost 2, and handled the repo rate without trouble. Even the cheap Flash Lite model matched it.

That was the pivot. My whole bet had been that an LLM can't be trusted past the first hop, and on my own test set the opposite was true. I wrote it down as "Taxonomy as vocabulary, not engine": the trees stayed as the words the model is allowed to use, and the links between them went. With a way to say "no impact", only 23 of 105 articles came back relevant, where the graph had passed every one. Under two hours later I deleted the graph engine and every causal link file, 193 files and about 91,000 lines. TODO(tarang): one honest line on how that felt after three weeks of edge work.

5. Two tiers, stories and profiles

That single call sent about 19K tokens of vocabulary with every article, even the ones it rejected, so I split it into the quick check and the deep think, roughly 5x cheaper per article. I also started thinking in stories rather than articles, because people care about "what's happening with oil prices", not one article about it. My first version hung each impact under one trigger, which I realised was a habit left over from the graph: retail petrol goes up because of crude, the rupee and a subsidy change together. So I let shifts and impacts link many-to-many.

I shrank the impact tree from 321 end nodes to 160, which almost halved the prompt, but kept it specific, because personalising needs "petrol" and "LPG cylinder", not "fuel". Asking the deep think to reason through three orders lifted its score from 0.54 to 0.67 at the cost of precision, so I put a confidence filter after it. TODO(tarang): why that trade-off was acceptable. Last came the person: profiles, the follow-up questions, and one runner from collection to Telegram.

Looking back

All of this was built with Claude Code as a pair, with Gemini 3.1 Pro doing a different job in the graph days: generating the trees and links and reviewing them, often with me arguing back. I don't think the graph was wasted. It gave me the vocabulary, the shape of an impact and the test set that beat it, and I only trusted the LLM because I had something hard to measure it against. What I gave up is a path I can read hop by hop, and a system that gives the same answer twice. And the irony isn't lost on me: both tiers read only RSS summaries, and the full-text scraping I fought n8n for in the news digest never came across.