Ashesh Srivastava
← home

Orbit — reads my 804 sources and hands me one ranked digest at 7am

Orbit reads everything new from my YouTube subscriptions and X follows, ranks each item on two independent axes, and emails me one browsable digest at 7am — so I focus on what actually matters. Every video gets a timestamped summary: click a moment to jump straight into it. And when there’s no time to read at all, one tap on the digest’s Ask Claude button turns the whole morning into a voice conversation.

0→1Solo buildAgentic architectureLive daily
Role
Designed, built, and shipped solo.
Timeline
June 2026 — present. First commit 18 Jun 2026; delivering daily since.
Status
Live — a launchd agent runs it at 7am every morning. Open source: github.com/as2621-dev/orbit
Tech
Claude agents (claude -p pipeline) · adversarial 3-lens review · LLM classification · two-axis ranking · RAG · transcription (yt-dlp) · voice-mode handoff · Python · SQLite · local-first launchd job · HTML email.
804
sources tracked — 590 YouTube, 214 X
38
items in a median digest — from the ~25 creators who shipped that day
65%
of X posts dropped as noise — a YouTube video is never dropped, only demoted

The product decision

What I built

Orbit delta-fetches new items from the feeds I already follow, classifies each on signal-vs-noise and on-topic-vs-off-topic, ranks it, and renders one HTML page delivered by email. It runs entirely on my local machine — browser-cookie auth, local SQLite, a launchd agent at 07:00, no server.

Reading it is the point. Every video arrives as a summary with timestamped section bullets — click one and land on that exact moment in the video, so I listen to the part that matters and skip the rest. And when reading isn’t an option at all, one tap opens a Claude session that pulls the digest straight out of the email — per-video summaries written from the full transcripts, X posts in full — so I can talk through today’s items in voice mode.

real output — the web ledger for Mon 3 Aug 2026, byte-for-byte as the pipeline rendered it on 2 Aug: 36 items from 23 of 806 tracked sources (the 804 above is an earlier snapshot of a moving list). The email body is this ledger re-rendered email-safe; this page rides attached. Scroll it.open full page ↗

The second mode: don’t read it, talk it through

The brick-red button at the top of the ledger — ASK CLAUDE ABOUT ALL 28 VIDEOS & 8 POSTS — is that second mode. One tap opens a Claude session pre-loaded with a handoff prompt: Claude pulls the digest email over Gmail, reads the markdown body, replies that it’s ready, and I switch to voice mode. The whole morning’s feed becomes a conversation — for the commute, the walk, the stretch of day where reading was never going to happen.

The button is the fast path, not the only one. The same email carries the artifacts themselves — transcripts.md, every video’s full transcript, plus the ledger page — so the handoff also works by hand: attach both to a fresh Claude session and send.

Claude app composer loaded with transcripts.md and today-ledger.html attached as files, the Orbit handoff prompt pasted in full, model set to Opus 5 on high reasoning, send button ready.
real capture — the manual handoff, loaded: transcripts.md and today-ledger.html attached, the shipped prompt pasted verbatim, Opus 5 on high. One tap left.
A live Claude session after sending the handoff: the two attachments and the prompt sit at the top, a status line reads “Used Gmail integration, loaded tools, read a memory”, and Claude’s streaming reply says it read the 3 Aug digest — noting the newer_than:2d query came back empty so it used the most recent Orbit email — before stopping mid-sentence at “ready when you”.
real capture — after send: Gmail integration pulls the digest email, and when the newer_than:2d window has rolled past, Claude says so and falls back to the most recent Orbit digest. The reply ends at “ready when you…” — the session opens at my first question, exactly as instructed.

Who it’s for

It started with wanting to seriously follow AI. I subscribed to everyone worth hearing — 590 YouTube channels, 214 X accounts — and then couldn’t keep up: too much, every day, all at once. I didn’t want fewer sources, I wanted fewer openings. So I built the thing that reads my whole orbit every morning, hands me one ranked summary of everything that moved, and lets me talk it through when there’s no time to read.

Built for me, and honestly so — one user I understand completely is the fastest way to get the judgment calls right. But the shape is anybody’s: busy, and trying to actually follow something — AI, markets, a sport, a scene — same problem, different nouns. Every layer is per-user from the first commit, so a second user is a config change.

How it’s built

Architecture. A Python CLI driving a staged pipeline, with SQLite as the only state. YouTube comes in through yt-dlp; X through a vendored cookie-auth GraphQL client. X rotates the query IDs behind that API, so the client re-resolves them live from the site’s own JS bundle — when X shuffles them, Orbit keeps working.

Cookies, not passwords. Orbit never logs in to the accounts it reads and holds no credential for them — no password in any config, no OAuth app, no third-party service with access to my accounts. I’m already signed in to YouTube and X in my browser, and Orbit borrows that session. On the YouTube side, yt-dlp — the open-source downloader that fetches the videos — reads the browser’s cookie store straight off disk via --cookies-from-browser, so its requests carry my existing login and YouTube serves my subscriptions feed as if I’d opened the tab myself. The X client does the same one layer down: it presents the session cookies the X web app itself runs on, to the same GraphQL endpoints the site calls.

Nothing is typed and nothing is copied into a config: cookies are read fresh at each run, held in memory, and sent nowhere except the two platforms they already belong to. The one secret Orbit does keep is on the sending side — a Gmail app password in a gitignored .env, scoped to mail rather than the whole Google account and revocable on its own, used only to email me the digest. And the setup doc refuses to dress any of this up: it states that an auth token is full account access, in a section headed “do not soften.”

The run. Load sources → fetch YouTube: transcribe, chapterize, classify → fetch and classify X → cluster for trending → rank and tier → summarise → render → deliver → archive. Every stage fails soft in a specified direction — and delivery deliberately runs before the archive push, so a failed push can never cost me the morning email.

804 sources → one email
The 7am run, end to end
judge — delta fetch → classify → trendship — rank → render → 7am emailinjected boundary — fails soft
The 7am runPYTHON CLI · LOCAL-FIRSTLAUNCHD 07:00 · WAKE-PROOFNew uploads pulled per channel via yt-dlp, authenticated with browser cookiesPer-handle timelines pulled via the vendored cookie-auth GraphQL clientOnly unseen items move on — the seen table is the deltaClassify, chapterize, sections, and transcript summaries all ride one boundary: claude -p (Sonnet 4.6) on the Claude Code subscription — no API key on disk, fail-soft per batchClassified items move to overlap clustering and trendingInternal-trending topics are cross-checked with a keyless stdlib web search — only the result count comes back, tagging corroborated vs scoopClusters and trending multipliers feed the rankerTiered items — hero to index band, nothing dropped — go to the rendererEvery stage reads and writes one local SQLite file — sources, seen-item deltas, classifications, carryforward. No server anywhere.The ledger rendered email-safe is the body; the web ledger page and transcripts.md ride as attachments — a failed render degrades to plain text, the email still ships07:00 EMAILAfter the email, the day's pages and notes are pushed to a private GitHub archive via the gh CLI — visibility verified before every push, and a failed push never costs the emailFAIL-SOFTYouTube — 590 subscriptions · yt-dlp · cookies · from the browserSOURCESYouTube590 subscriptionsyt-dlp · cookiesfrom the browserX — 214 following · vendored GraphQL · cookie-auth clientSOURCESX214 followingvendored GraphQLcookie-auth clientSQLite — orbit.db · only state · sources · seen · classifications · carryforwardSTATESQLiteorbit.db · only statesources · seenclassificationscarryforwardClaude — Sonnet 4.6 · claude -p · classify · chapters · sections · summariesMODELClaudeSonnet 4.6 · claude -pclassify · chapterssections · summariesKeyless search — stdlib urllib · no key · result count onlyCROSS-CHECKKeyless searchstdlib urllib · no keyresult count onlyPrivate GitHub — gh CLI · 1 commit/run · verified private firstARCHIVEPrivate GitHubgh CLI · 1 commit/runverified private firstYour inbox, 7 am — Gmail SMTP · app password · ledger as the body, · chat link → claude.aiDELIVERYYour inbox, 7 amGmail SMTP · app passwordledger as the body,chat link → claude.aiDelta fetch — new items only (seen), shorts <10 min dropped, transcripts + chaptersDelta fetchnew items only (seen)shorts <10 min droppedtranscripts + chaptersClassify — signal vs noise, on- vs off-topic, + category, per itemClassifysignal vs noiseon- vs off-topic+ category, per itemCluster & trend — short↔long overlap, internal trending, scoop detectionCluster & trendshort↔long overlapinternal trendingscoop detectionRender & note — Tiles + Final ledger, email-safe body, transcripts.md notesRender & noteTiles + Final ledgeremail-safe bodytranscripts.md notesRank & tier — creator-own baseline, X capped at 8, tiers — none droppedRank & tiercreator-own baselineX capped at 8tiers — none dropped
the run as it executes in scripts/orbit.py — solid lanes are the pipeline stages; dashed strokes are the injected boundaries (claude -p, keyless web search, SQLite, the gh archive push), each specified to fail soft.

What happens to each item

Classification. Every item that survives the delta fetch gets two independent verdicts — 2,028 items judged in the first 42 days. Signal vs noise: is there a real claim here, or is it shorts bait, promo, platitudes, a hot take with nothing specific? On-topic vs off-topic: is this what I follow the source for? Two axes instead of one relevance score, because the axes disagree on 333 items (16.4%) — a sharp video on the wrong topic and a lazy video on the right one both land mid-scale on any single number, so a sixth of my feed would be misfiled.

The same noise verdict then lands differently by platform, on purpose. It deletes an X post outright — a tweet costs nothing to lose, and 65% of them go this way — but it only demotes a YouTube video into the “they also posted” strip at the bottom of the digest. A channel I chose deserves a line even on an off day; deleting its video would be the digest silently second-guessing my subscriptions.

classify → rank → tier
What happens to one item
ships full-slot — passed both axesdemoted, not deleted — “they also posted”dropped — the only deletions
Every unseen item enters triage; the seen table is the deltaFull-length items go on to classification — one claude -p call per itemKnown-short clips are dropped before the classifier ever sees them — no model call spentYT SHORTSPasses both axes — signal AND on-topic — so it goes to the ranker for a full slotFails an axis on YouTube — noise, or off-topic: demoted straight into the index band (“they also posted”), regardless of score. Never deleted.X-only gate: a post judged noise (gm, platitudes, engagement bait) is deleted outright — 65% of X posts go this way. A tweet costs nothing to lose.X NOISE · 65%Both platforms: an item whose category falls outside the fixed topic taxonomy (“other”) is dropped — a garbled category defaults to keep, so a prompt regression can’t empty the digestOFF-TAXONOMYScored items are tiered by rank position over the passing distribution — the ranker drops nothingTIERED BY RANKKEPT —DEMOTEDEverything the delta fetch found that I haven't seen before — 2,028 items reached the classifier in the first 42 daysINPUTEvery new item2,028 judged · 42 daysa YouTube uploador an X postThe one pre-classify gate: known-short YouTube clips are dropped before any model call — unknown durations are kept, fail-openGATELength gateYouTube only:Shorts droppedbefore classifyThe full list of what gets deleted: YouTube Shorts, items outside the fixed topic taxonomy (both platforms), and X posts judged noise — 65% of X. A full-length video from a subscribed channel can never end up here.THE ONLY DELETIONSDropped, never rankedShorts · off-taxonomy (“other”) · X noisea chosen channel’s video never lands hereOne claude -p call per item returns two independent verdicts plus a topic category. Keeping the axes separate is empirical: they disagree on 333 of 2,028 items (16.4%) — any single relevance score would misfile a sixth of the feed.ONE CLAUDE -P CALL PER ITEMThe two-axis judgeAXIS A — signal or noise?shorts bait · promo · platitudeshot takes with no claimAXIS B — on-topic or off?off-topic ≠ bad — just notwhat I follow them for+ a topic categoryAXES DISAGREE: 333 / 2,02816.4% — why one score failsPassing items are scored against each creator's own baseline (lib/rerank.py) — a small channel's breakout beats a big channel's routine upload. The one drop at this stage is scoped: X only, top 8 kept, because an unbounded X feed drowns a digest in a way YouTube doesn't.RANK & SCOREOwn-baseline rankengagement vs thecreator’s own norm ×1.0uniqueness floor ×1.0quote tweet ×0.5X only: top 8 keptTHE DIGESTThe top 10% of passing items by rank position: a full card with summary and chaptersHERO — top 10% · full cardThe next 25% by rank position: a medium cardSTANDARD — next 25%medium cardThe next 35% by rank position: title plus one lineCOMPACT — next 35%title + one lineThe remainder of scored items, plus every demoted item — forced here regardless of score. Still in the digest, one line each.INDEX — the rest + demotedthe “they also posted” stripEnforced as a test in lib/density.py: len(assign_density_tiers(scored)) == len(scored). Things shrink, they don't disappear.RANK CONTROLS DENSITY,NEVER INCLUSIONa test: output length = input length
one item's path through the judge. The gray, dashed routes are the only deletions — Shorts, off-taxonomy items, X posts judged noise; everything else ships, full-slot or demoted.

Measuring quality. The test suite measures correctness, not quality. The real work came from per-item score components in structured logs, a three-lens adversarial review that produced 17 “what I deliberately did not fix, and why” write-ups, and an audit that caught the product drifting from its own spec. The gap: 20 user stories, zero target metrics, no precision bar.

What I learned

Following the process proved most of my feed is fluff. About 75% of what I follow is dead — in 42 days only 97 of 214 X accounts and 104 of 590 YouTube channels produced a single item — and even inside the videos that do ship, the genuinely new moments are maybe 5% of the runtime. The rest is intros, recaps, sponsor reads, takes with nothing specific in them. No platform will surface either number — a follow count is a number they want going up. Ranked summaries with timestamped jumps collapse hours of watching into minutes of reading; that returned time is the product.

The world runs on APIs — even where none is offered. Every site is a frontend over an API its own pages call. Orbit never scrapes HTML: it borrows the sessions already in my browser, so YouTube serves my subscriptions feed and X answers the same GraphQL endpoints its web app runs on. The constraint I’d filed as a tax — cookies can’t leave the machine, so this must run locally — turned out to be the moat: it’s why no hosted product can read a feed this way. And the insight compounded: three days after Orbit’s first commit, I pointed the same move at my electricity smart meter — a CLI that signs in the way the portal itself does and calls the API its frontend calls. No email round-trip, no scraping.

Shipping the affordance is not shipping the loop. Overrides, creator weights, carryforward — built, and all three inert in production. Building the mechanism felt like progress and produced zero rows of data. Measurement needed its own milestone and acceptance bar, not a by-product of the features that enable it.