Sumo App
Real-time sumo tournament tracker — a newspaper desk that files same-evening reports, live SSE updates, and hand-prompted sumi-e illustrations
Live basho standings, wrestler profiles, a build-time pipeline of sumi-e ink illustrations, and The Dohyō Times — an editorial desk that writes the day's report from computed facts, with a language model allowed to touch the prose and never the truth.
- statuslive
- modelside project
- since2026
- > solo, weekends
Sumo App started as “I want to follow a basho and the official sites are kind of awful,” and quickly turned into “what if the entire visual identity were generated from prompts.” It tracks live tournament standings (via sumo-api.com), wrestler rankings, day-by-day matches, and the 86 finishing techniques (kimarite) that decide them.
Stack is Next.js 16 + React 19 with TanStack Query doing the server-state caching and a thin SSE layer pushing webhook updates from the upstream API straight into the query cache (with HMAC signature verification, because that’s table stakes).
$ tplocic stats
- lines of typescript 34 065
- editorial versions to get it honest 14
- LLM calls in the request path 0

The visual identity (the original reason this exists)
Nothing here calls a model while you wait. Every visual asset is pre-generated at build time by a small Python pipeline in /scripts. Two pipelines, both producing sumi-e ink-brush illustrations because that’s the only style that wouldn’t look ridiculous next to centuries-old sumo terminology.
Kimarite illustrations (86 techniques):
- A 291-line hand-written prompt file, one entry per technique, describing the pose, contact point, and intent
- A unified style suffix appended to all 86 prompts so the set holds together visually
- Generated through FLUX.1 Dev locally (28 inference steps) or OpenAI gpt-image-1 as the cloud option
- White background → transparent post-process so they composite cleanly into the UI






Rikishi portraits (191 wrestlers):
- Source photo from sumo.or.jp, watermark cropped
- IP-Adapter v2 extracts facial identity from the source
- Rank-conditional prompting: yokozuna get the tsuna rope and a katana; everyone else gets a kesho-mawashi (ceremonial apron)
- Kanji ring-name rendered in calligraphy in the corner
--ip-scaleknob trades likeness for artistic freedom — settled around 0.65 after a lot of A/B testing


The result is a growing custom illustration set that would have cost real money to commission, generated for the price of a few GPU hours and a lot of prompt iteration.
The Dohyō Times
The app now has a newspaper desk. After each day of a tournament, The Dohyō Times files a short report — the upset that headlined the day, how the yūshō race moved, who went kyūjō and the record they carried out — and at the end of a basho it files the wrap-up. The front page carries the desk’s hanko.
The interesting part is who’s allowed to know what. The report is composed deterministically from facts the app already computes — day digests, upsets by rank, the leaderboard, withdrawals — and that draft is complete and correct before any model sees it. A hosted gpt-oss-120b is then offered the finished draft with exactly one job: change the wording, change nothing else. Its output is diffed against the draft before anything is stored — every digit, every finishing technique, every proper noun must already exist in the draft, or the rewrite is discarded and the plain draft is published instead. Spelled cardinals are normalised to digits first, so “twelve” can’t slip past a check looking for 12.
It took fourteen versions and one complete removal of the model to arrive at that arrangement. The full story — including the twelve-version stretch where a small local model kept calling a 12-3 record perfect — is in the notebook: the model may not source a fact.
Same-evening reports and a newspaper that looks like one
The desk only works if the facts arrive while anyone still cares. A Bun worker computes the derived aggregates for each completed day and persists them as immutable snapshots — a finalized day is never recomputed, so once a basho is caught up, each pass is one cheap “which days are already final?” query and no fetching at all. Day recaps and day stats surface live, the evening they happen, without a reload; W–L–A records run app-wide; kyūjō withdrawals read as the story they are rather than a losing record padded by absences.
The UI followed the writing. The whole app was re-skinned as newsprint — square corners, flattened shadows, a tabbar built to look like a masthead’s — because once there was a newspaper in the product, the chrome around it had to stop looking like a dashboard.
The editorial layer has opinions about when a fact is a story, too. The yūshō race is held back until day 10: through the first two-thirds of a basho the field is still bunched, a shared top record isn’t a race, and reporting it as one is the kind of true-but-misleading sentence a template will happily write forever.
Other bits
- Hybrid SSR + client-side TanStack Query — server fetches, hydrates
initialData, client takes over with stale-while-revalidate - A
cmdkcommand palette for jumping between tournaments, wrestlers, and techniques (because everything is named in romanized Japanese and nobody can spell anything correctly on the first try) - Real-time webhooks that invalidate exactly the right queries —
matchResultsinvalidates["matches", bashoId]and["banzuke", bashoId], not the world - Per-wrestler style profiles and a clutch pace read — how a rikishi’s record moves across the thirds of a tournament, bucketed by division, because “he starts strong and fades” deserves to be a number
- It runs on my own hardware now. The app ships as a Next.js standalone build to the sovereign stack — preview on every push to
main, production on a tag. No Vercel in the loop.