WikWok

Scroll Wikipedia like TikTok

AuthorSurya Dantuluri
Published
Views180K from San Francisco, Nagpur, New York City

Two projects a year apart, and the second one is the first one pointed at a feed. Both start from the same question: what if the interface is generated at the moment you ask for it, instead of written ahead of time?

June 2025 — Sudo

Sudo generated the page for whatever URL you visited. There was no route table. Ask for /generate/weather/forecast and you got a weather page; ask for /generate/portfolio/projects and you got a portfolio.

The constraint that made it work was refusing to let the model emit freeform HTML. A page came back as a structured object — title, styles, content, script — validated against a Zod schema and streamed with the AI SDK's streamObject on the server into a useObject hook on the client. Because the object is typed, a half-arrived page is still a renderable page, so the layout assembled itself in front of you as tokens landed rather than blocking on a complete document.

April 2026 — WikWok

WikWok is that generator aimed at an infinite feed: a TikTok interface over Wikimedia Commons video, with AI-generated duck and goose dialogue narrating whatever topic comes up. It ships as a single Cloudflare Worker that serves the Vite SPA, the /api/* routes, and four Durable Objects (AppDO, UserDO, InboxDO, GlobalSocialDO), each with its own SQLite. There's no database behind them — every card is generated on demand from Commons plus Gemini, and the DOs make views, likes, bookmarks, and chats live for everyone at once.

The lineage is literal rather than thematic. The file that streams a post is a direct port of Sudo's streaming route, same streamObject, same Zod schemas, now emitting cards instead of pages.

Choosing the boring ranker

The feed needed a ranking policy, so I wrote out nine of them — heuristics, Thompson-sampled heuristics with a nightly LLM rewrite, DSPy-optimized LLM ranking, two-tower retrieval, nightly LightGBM, contextual bandits, session PPO, evolutionary policy-code mutation, and the full two-tower-plus-wide-and-deep production shape — and scored each on whether it learns from behavior, survives cold start, fits in a sub-100ms hot path, stays debuggable, and can be extended without a rewrite.

The seductive one was the LLM mutating the ranking code itself. It lost on the same line that kills most of these: LLM calls, dynamic code loading, and opaque weights belong outside the first serving path. So the LLM sits offline as an analyst that reads the logs and proposes bounded config changes, and the thing that actually serves is deliberately dull — engagement score decayed by 1/√(age + 1), versioned commons_engagement_decay_v0.

Instrument first, learn later

The part I'd defend is what gets written down. Every served video logs a decision row: the candidate snapshot it was chosen from, the per-term score breakdown, the propensity, the weights and embedding versions, and its position in the session. Each row carries a request ID that the client pins a watch outcome back to, so watch time and completion attribute to the exact decision that caused them.

Serving propensity is uniform today, and the code says so in a comment instead of pretending otherwise. The dashboard's softmax is labeled inspection-only, and columns that score all-zero degrade to uniform rather than inventing confidence. That discipline is the whole point: off-policy correction needs logged propensities to be true, so the journal is built to make a REINFORCE controller trainable later without throwing away anything collected before it exists.

The controller isn't written yet. The journal that would train it is, and it's public — every decision, every score breakdown, every candidate considered.