Natecal
An automated daily pipeline that turns AI videos into structured, testable study packets.
Visit live siteNateCal solves a specific information-overload problem for a serious AI learner. Nate B. Jones publishes an AI-strategy video on YouTube almost every single day — a backfill found 233 uploads since March 1, 2026, about 100 of them real long-form briefs. Keeping up daily is hard, and passive watching produces shallow retention.
There is no built-in way to test your understanding of a video, build a personal reference you can search later, or discuss the material — and doing any of it by hand means transcribing, summarizing, and writing your own quiz questions every day.
The target user is someone trying to master AI at a fundamental, conceptual level who wants structured daily study rather than a feed to scroll. So the app has to do four things automatically: never miss the new daily video, turn a 10–20 minute video into something you actually internalize, retain and re-find concepts over time, and require zero manual transcription or summarization.
NateCal is a self-hosted, multi-user web app: a FastAPI backend and a dependency-free vanilla-JS frontend on a single Hetzner VPS, fronted by nginx and Cloudflare. An unattended daily pipeline discovers Nate’s newest upload, fetches its transcript, has Claude generate a structured study packet, and writes it to Postgres, which the calendar UI then renders entirely client-side.
The daily pipeline
A cron job, fired at 07:00 by a systemd timer, discovers Nate’s newest upload via the YouTube Data API, skips Shorts (anything five minutes or under), fetches the transcript through a managed API, calls Claude to produce a structured study packet, and writes it to Postgres — entirely unattended.
The study packet (Claude)
Each packet contains a one-line topic, “why relevant” and “why timely” summaries, a deep five-section “Nate’s Take” analysis (core argument, gems / paradigm shifts, his stance, recommendations, how to go deeper), a glossary of 4–10 key AI terms in plain language, and hard conceptual quiz questions scaled to roughly 0.7 per minute of video — each tagged MECHANISM, FIRST-PRINCIPLES, STEELMAN/WEAKNESS, COUNTERFACTUAL, SYNTHESIS, or PREDICTION. Claude returns a single JSON object stored in JSONB columns.
Discovery & transcripts — the hard part
YouTube’s RSS feed 404s and scraping libraries return nothing for the channel, and YouTube blocks the datacenter IP, so discovery uses the official Data API (channel → uploads playlist → playlistItems → videos for ISO-8601 durations). Transcripts, which YouTube refuses to datacenter IPs, are offloaded to a managed transcript API (Supadata), with youtube-transcript-api kept only as a fallback.
The calendar frontend
A single static index.html of inline CSS and vanilla JS — no framework, no build step — renders four views (month grid, scrollable list, GitHub-style year heatmap, and an aggregated glossary that de-duplicates every term across all videos into one searchable dictionary). All data loads once from the API, so search, view-switching, and glossary aggregation are instant and cost no extra calls. A “copy transcript” button lets users paste the raw transcript into their own AI chat instead of burning API credits.
Accounts & engagement
Email/password auth uses bcrypt hashing and stateless JWTs (HS256, 72-hour expiry), with optional Google OAuth verified server-side against Google’s tokeninfo endpoint. On top sit per-user streak tracking, completion marking, and YouTube-style threaded comments with likes.
Deployment — a stateful single box
Everything runs on one Hetzner VPS: FastAPI under uvicorn bound to localhost, Postgres, and the cron job, all managed by systemd units (service + oneshot + timer). nginx terminates TLS with a Cloudflare Origin Certificate (Full strict), and a ufw script restricts ports 80/443 to Cloudflare’s IP ranges so the origin cannot be reached directly. Cost stays flat regardless of user count.
The reasoning behind the build — and what each choice cost.
Dual database access patterns over one Postgres
The web app uses SQLAlchemy 2.0 async with asyncpg for non-blocking request handling under uvicorn, while the headless cron job uses a synchronous engine over psycopg2 — rewriting the connection string by swapping “+asyncpg” for “+psycopg2” so the same models work in both contexts.
Two driver setups and a string-rewrite to keep in sync, rather than a single uniform access path.
Official YouTube Data API instead of scraping — bound to IPv4
The RSS feed 404s, scraping returns nothing, and the datacenter IP is blocked, so discovery uses the Data API. A subtle catch: the box defaults to IPv6 egress, which the IP-restricted key rejected with 403, so the API client is bound to the IPv4 wildcard to match the key’s allowlist.
API quota and key management to maintain, plus the non-obvious IPv4-binding requirement that only surfaced in production.
Managed transcript API instead of a self-hosted proxy
YouTube returns “TranscriptsDisabled” to datacenter IPs even when captions exist; rather than run a residential proxy, the design offloads the IP problem to Supadata with a single authenticated GET. Volume is ~30 transcripts/month, which fits the free tier with ~3x headroom.
A dependency on a third-party service for a core step, mitigated by keeping youtube-transcript-api as a fallback.
A dependency-free, build-step-free frontend
The entire UI is one static HTML file of inline CSS and vanilla JS served by FastAPI. All data loads once and views, search, and glossary aggregation are computed client-side, so interactions are instant and free of extra API calls.
No component framework or type safety on the frontend, which trades structure for simplicity and speed at this scale.
A stateful single VPS, not serverless
The daily fetch is long-running and needs a persistent process and a real Postgres, which fit a VPS naturally; cost stays flat regardless of user count, and systemd gives automatic restart.
Manual ops — server, TLS, firewall, and updates to manage — versus a managed serverless platform.
Structured JSON output with defensive prompting
Claude is asked for a single JSON object, with the title and transcript appended outside Python’s str.format() so stray braces in a transcript cannot break formatting; max_tokens scales with question count, and nate_take, glossary, and questions are stored as JSONB.
Relies on the model returning valid JSON; the defensive formatting and JSONB storage absorb the variability.
A single backfill run discovered 233 uploads since March 1, 2026 and correctly indexed 101 long-form briefs while filtering out 131 Shorts with the five-minute threshold. The pipeline is verified end to end: a real 20,339-character transcript was pulled through the otherwise-blocked datacenter IP via Supadata, and the 18-minute June 17 video produced a ~3,800-character five-section analysis, 9 glossary terms, and 13 conceptual quiz questions across all six question types.
The mechanics behave as designed. Quiz scaling holds (0.7 questions per minute → 7 for a 10-minute video, 13 for 18–19 minutes), and from just six summarized days the global glossary already aggregates 55 distinct terms, demonstrating the dedup-and-accumulate design as it scales linearly. The whole manual alternative — watch, transcribe, summarize, write a dozen hard questions, extract terms — is replaced by an unattended job costing about one Claude call and one transcript credit per day.
Some of the most telling results are the bugs found and fixed during bring-up: a passlib/bcrypt incompatibility that 500’d registration (fixed by pinning bcrypt to 4.0.1), the IPv6-egress 403 against the IP-restricted API key (fixed by forcing IPv4), and the datacenter-IP transcript block (solved with the managed API). In the interest of honesty: the app is fully functional and live on the VPS, but the public go-live at natecal.paramalabs.com and a full historical backfill were prepared but not yet executed, and the Google OAuth frontend button is still a stub pending configuration.