Inspiration
There are only two ways to learn something on YouTube today, and both are bad.
You can commit to a 45-minute lecture — a real cost when you are not yet sure the video even answers your question. Or you can scroll Shorts, where the ranking signal is rewatch and loop behavior, not whether you understood anything. One demands too much before it earns your attention; the other is engineered never to earn it at all.
That gap is worst for exactly the people most drawn to short-form video. If you bounce off long-form content — and many of us do, ADHD or not — the algorithm hands you junk food instead of a smaller plate of real food.
The insight that started this project: no creator has any incentive to point you at a rival's better explanation. A channel's own Shorts exist to promote that channel's own long-form videos. So the single best 90 seconds on "what a derivative actually is" might be buried 12 minutes into a conference talk that nobody will ever clip — because the person who could clip it gains nothing by doing so.
A topic-first, creator-agnostic index has no such conflict of interest. That is the thing that could not exist before, and the thing we built.
What it does
You give it a topic. Behind the scenes it:
- Searches YouTube for long-form educational videos on that topic (4–25 minutes, captioned, embeddable)
- Pulls each candidate's timestamped transcript — caption text only, never a single byte of video or audio
- Sends each transcript to an OpenAI open-weight reasoning model (
gpt-oss-120b) with one job: find the most information-dense, self-contained 30–60 second segment, and write an honest title for it - Stores only
video_id + start_time + end_time + topic + generated_title + source_url - Renders each result as a card in a vertical, scroll-snapped feed using YouTube's official IFrame Player API, cued to those exact timestamps
The result is a feed you scroll like TikTok, where every card is the densest 45 seconds someone ever recorded on your topic. Clips autoplay, advance when they end, and remember what you have already watched so the feed keeps moving forward. Every card credits the channel and links back to the full video.
How we built it
Frontend — React (Vite). A scroll-snap vertical feed where each slide owns a YouTube IFrame player. Only the card in view plays; everything else pauses. Clips start muted and unmute on the first user gesture, because every mobile browser blocks audible autoplay until you touch the screen. When a clip ends, the feed auto-advances. We later replaced YouTube's native chrome with our own tap-to-play/pause, mute, and click-to-seek controls to make it feel like a feed instead of an embed.
Backend — Node/Express, organized by pipeline stage:
youtubeSearch.js— YouTube Data API v3 search plus caption fetching via InnerTubesegmentSelector.js— the intelligence layer: transcript in,{startTime, endTime, generatedTitle}outdb/schema.js— SQLite (better-sqlite3), metadata onlyingest.js— orchestrates search → transcript → segment selection → storage, fanning candidates out in parallel
The constraint that shaped everything: never download, transcode, or rehost video. We store timestamps and play through YouTube's own player. This is not a limitation we tolerated — it is the reason the product is legitimate. Ads are served by YouTube's player, so we never have custody of the ad decision and structurally cannot intercept a creator's revenue. Views count. Revenue flows. We are a pointer, not a pirate.
How we worked with coding agents — we split the backend into three tracks and ran two different agents against them in parallel:
| Track | Scope | Agent |
|---|---|---|
| A — Search & Ingest | YouTube search, candidate filtering, caption fetching | Claude |
| B — Segment Intelligence | Prompt design, transcript → best-segment selection | Codex |
| C — API & Data | Express routes, SQLite schema, feed endpoints | Claude |
Track B — the actual intelligence layer, and the part that decides whether a clip is worth watching — was built with Codex, and its branch history (codex/track-b-segment-intelligence) is where the prompt iterated toward the version we shipped.
The coordination trick that made this work: each agent got its own instruction file at the repo root — AGENTS.md for Codex, CLAUDE.md for Claude — but both encoded the same non-negotiables (metadata only, official embed, always credit the creator). Every session inherited the constraints instead of us re-explaining them, and neither agent ever proposed downloading a video file. Three tracks, two agents, parallel branches, one set of rules.
Challenges we ran into
1. Our "parallel" pipeline was not parallel.
A fresh topic took 60–90 seconds and we could not see why. So we instrumented every stage with per-candidate timing. The log was damning:
transcript in 1994ms transcript in 2061ms transcript in 2110ms transcript in 2147ms
segment chosen in 13385ms
segment chosen in 27677ms
segment chosen in 53328ms
Transcripts finished together — genuinely concurrent. But the model calls finished in a stair-step, roughly 14 seconds apart. We were firing them in parallel; the inference provider was queuing them and running them one at a time. Our wall time was the sum, not the max:
$$T_{\text{serialized}} \approx \sum_{i=1}^{n} t_i \qquad \text{instead of} \qquad T_{\text{parallel}} \approx \max_{i} \; t_i$$
This is the lesson that stuck: concurrency in your code is a request, not a guarantee. Whether it actually happens is a property of the service on the other end. We would never have found this by reading the code — only by timing it.
2. Reasoning effort, not input size, was the latency knob.
We assumed long transcripts were making the model slow, and nearly went down the path of chunking them. We measured instead. On the same inputs:
| reasoning effort | latency per call |
|---|---|
| high | ~28s |
| medium | ~13s |
| low | ~5–7s |
Input size barely moved the needle by comparison. Picking a timestamp range out of a transcript is a retrieval and judgment task, not a deep-reasoning task — so we spend the tokens where they matter and get a 4–5× speedup for quality that held up in testing. We also added a 20-second timeout and one retry, replacing the SDK's 10-minute default that had let a single hung call stall an entire request for four minutes.
3. Returning early instead of waiting for stragglers.
We only need three clips, but we were waiting for every candidate to settle. Now the pipeline resolves the moment enough usable segments have arrived and lets slow candidates finish on their own. Results are re-sorted by search relevance before storage, so returning early never costs us ordering.
4. YouTube rate-limited our transcript fetching.
This one bit us mid-demo-prep. Transcript requests started failing en masse — and the giveaway was that a video whose transcript we had successfully fetched an hour earlier now returned HTTP 429. It was not the videos; it was us. Feed deepening compounded it: each scroll-to-the-end triggered another ingest with a larger candidate set (5 → 7 → 9 → 11), so browsing a single topic quietly fired dozens of caption requests. The honest lesson is that a "background refill" that feels free to the user is not free to the API you depend on.
5. The embed does not do what the docs imply.
The IFrame Player API's end parameter is only reliably honored on the play immediately following a cue call — not on every autoplay path. We poll currentTime as a safety net. Separately, active-card detection via IntersectionObserver ratio thresholds broke at non-100% browser zoom and played two clips at once; we replaced it with scroll-position math.
6. A native module lied to us.
better-sqlite3 failed with Could not locate the bindings file and a list of a dozen paths it had searched. The file was there the whole time — it was compiled for NODE_MODULE_VERSION 115 (Node 20) while we were running Node 24 (137). The error message described a symptom three layers away from the cause. Loading the binary directly gave us the real message in one line.
Accomplishments that we're proud of
We shipped a product, not a demo. You type a topic you have never searched before, and 5–7 seconds later you are scrolling real clips from real creators, cued to timestamps a model chose by reading the actual transcript. Nothing is staged, and there is no fixture data behind it.
We made it fast by measuring, not guessing. Fresh-topic ingest went from 60–90 seconds to roughly 5–7 — a 10× improvement — and every step of that came from instrumentation that told us we were wrong about where the time was going. Twice.
We never compromised the non-negotiable. Not one byte of video or audio is downloaded, transcoded, or re-served. Everything plays through YouTube's official embed, every creator is credited and linked, and the architecture makes revenue interception structurally impossible rather than merely promised. It would have been faster to cut corners here. We are proud that we can explain exactly why we did not have to.
We built a feed that feels native. Custom tap-to-play, mute that survives scrolling, click-to-seek, auto-advance, and watched-tracking so the feed keeps moving forward — all on top of an embed API that was never designed to feel like this.
We diagnosed something invisible. The provider-side request queueing that was silently erasing our parallelism does not appear anywhere in the code. Finding it took building the timing harness first and reading the shape of the numbers second. That habit paid for itself repeatedly.
What we learned
- Measure before you optimize. Every one of our biggest wins came from instrumentation, not intuition. We nearly rewrote transcript chunking to fix a problem that was actually a reasoning-effort setting.
- Treat model output as untrusted input. Every segment is clamped to the real video bounds, checked for a sane 30–60s duration, and rejected if degenerate. A model confidently returning a timestamp past the end of a video is a normal Tuesday.
- Constraints can be a feature. "Never rehost video" started as a legal guardrail and became the clearest thing about the product: creators keep 100% of views and ad revenue, and we can say exactly why that is structurally true rather than a promise.
- Latency has a shape. "It's slow" is not a bug report. "It's slow in a stair-step, 14 seconds apart" is a diagnosis.
- Someone else's rate limit is part of your architecture. A background feature that feels free to the user is not free to the API underneath it.
What's next for Education Scroller
- Windowing the feed so only the visible player is mounted. Today every card builds its own iframe — fine on a laptop, rough on a phone.
- Caching transcripts so a re-ingested video never hits YouTube's caption endpoint twice, which removes the rate-limiting problem at its root.
- Personalized segment selection. The same video should yield a different 45 seconds depending on what you already know. The transcript does not change; the question we ask about it should.
- Concept sequencing — "you understood this, here's what unlocks next" — which is where an AI-curated curriculum stops being a metaphor and starts being the product.
Built With
- codex
- css
- express.js
- firework-ai
- gpt-oss
- javascript
- node.js
- openai
- react
- sqlite
- vite
- youtube
Log in or sign up for Devpost to join the conversation.