Skip to content
Javier Hengda

Lyra

SignalML

The lyre — listening for signal in the noise.

Role
Solo — architecture & operational design, agent-assisted build
Timeline
April 2026 · in progress (phases 0–2 of 7)
Stack
Next.js 16 · React 19 · TypeScript · Supabase · Postgres 17 · pg_cron · Edge Functions · OpenAI Responses API

Problem

arXiv publishes hundreds of ML papers a day; nobody reads a firehose. The long-term product is a reader that surfaces what matters. But a reader is only as good as its data layer, so the build order was deliberate: the ingestion and classification pipeline first, with production-grade operational behavior, before a single pixel of UI. This case study covers that pipeline — the product surface on top is future work, and this page says so plainly.

A second, more personal goal: measure how far disciplined agent-assisted development can go. The entire working pipeline below went from written spec to deployed in one evening (~86 minutes of wall clock, two commits) — with the human doing architecture and review, and the agent doing hands.

Approach

Two Deno edge functions on Supabase, chained by pg_cron:

Ingestion (06:00 UTC). Pages the arXiv Atom API across five ML categories with polite backoff (3s between pages, 3-attempt exponential retry, 30s timeouts, an identifying User-Agent), normalizes arXiv IDs so paper revisions collapse onto one row, dedupes in memory, and lands everything in Postgres with a single batched upsert. The cron job’s URL and shared secret are resolved from Supabase Vault at execution time — secrets never appear in migration SQL.

Classification (06:30 UTC). Picks up pending papers and asks gpt-5-nano (Responses API) for structured flags — domain, innovation level, key architecture, topic tags — under a strict JSON schema, then re-validates every field by hand anyway: belt and braces against the refusal and truncation cases strict mode doesn’t cover. A prompt_cache_key is set and cached-token counts are extracted and summed per run — cost is instrumented, not estimated.

supabase · postgres 17 · pg_cron · vaultarXivingestpapersclassifyatom · 5 categoriescron 06:00 · dedupeupsert on arxiv_idcron 06:30 · gpt-5-nanostrict schema → flags
Two crons, two functions, one honest data layer

The design center is operational legibility. Both functions write to run-log tables (ingestion_runs, classification_runs) with a running → succeeded | failed state machine — the run row is inserted before any external I/O, so even a hard crash leaves a findable orphan. Each paper carries its own status machine (pending | processing | succeeded | failed, with attempt counts and error text), so one bad abstract can’t poison a batch. Both functions take a dryRun that shows exactly what would be selected without spending a token. Cron migrations are idempotent (unschedule-then-schedule). Indexes mirror the actual queries — the classifier’s selection has its exact composite index — rather than being sprinkled generically. RLS is on for every table, including deny-all policies on the operational logs so only the service role can touch them.

Result

What runs end to end today: cron fires → arXiv paged and upserted → classifier picks up pending rows → schema-validated flags land → both runs logged, with failure email via Resend. Deployed on Supabase + Vercel and exercised against production at least once.

What doesn’t exist yet, on purpose: everything a user would see. The feed, reader, auth, bookmarks (the schema is ready — bookmarks FKs auth.users with owner-scoped RLS) are phases 3–5. There is no ranking or embedding layer yet either; today “signal” means one LLM-judged categorical per paper.

An honest audit of the fast build, recorded here because it’s the actual lesson: the edge functions ship under @ts-nocheck (1,200 untypechecked lines — the price of that evening), classification is serial and won’t finish a 200-paper batch inside an edge-function budget, papers orphaned in processing are never reaped, and there are no tests. That’s the trade agent-speed bought, and it’s the fix list for phase 3 — written down before any feed work begins.