Every day, millions of people read English Wikipedia, and Wikimedia publishes how many times each article was viewed. I turned that into a small daily game called Fomorrow: five quick questions about what the internet read yesterday.
Which of these four articles had the most readers yesterday?
Which one rose the most against its own 7-day average?
How many people read "Halloween" yesterday?
You answer, and the real numbers show up right away. A round takes about two minutes. There's also one daily "call": a prediction about how an article does today, revealed with the next day's puzzle.
This post covers how it works, the mistake that forced me to rebuild the whole game, and a few things I learned about generating fair questions from real data.
The data
Everything comes from the Wikimedia Analytics API. Two endpoints do almost all the work.
Daily views for one article:
https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/en.wikipedia.org/all-access/user/Halloween/daily/20261024/20261031
The most-read articles for a day:
https://wikimedia.org/api/rest_v1/metrics/pageviews/top/en.wikipedia.org/all-access/2026/10/31
The user agent filter leaves out automated traffic, so the counts are (mostly) real readers.
The detail that ended up shaping the whole game is timing. A day's per-article numbers appear a few hours after that UTC day ends. When I checked, Oct 31 was available at about 05:05 UTC on Nov 1. The top list shows up later than the per-article data. There is no hourly per-article endpoint in the REST API. Hourly data exists only as raw dump files of about 40 MB per hour, published roughly three hours late.
Version 1: predict tomorrow (and wait)
The first version was a forecasting game. Every day you got three questions about tomorrow's pageviews: which topic will rise the most, which of two will get more readers, will this article beat its average.
It worked technically, but it had a fatal flaw: you waited 1.5 to 2 days for every result. A pick made on day D could only be scored after day D+1 ended and Wikimedia published it. First-time visitors saw no result at all. People forgot what they had picked. The share button could only say "my picks are locked in", which nobody wants to click.
I tried to fix it inside the forecasting model:
- Predict today, close picks at 12:00 UTC. Results the next morning, but half the world would find the game closed.
- Use hourly data for a 6-hour window. No hourly per-article API, so it means parsing 40 MB dumps every hour, and you still wait most of a day.
- A personal 6-hour window from each pick. Fast results, but every player gets a different answer to the same question, which breaks the "same puzzle for everyone" feel that daily games depend on.
Then I looked at what the daily games people actually play have in common: an instant result, one shared puzzle per day, a spoiler-free share grid, and a streak. My game had none of the first one.
Version 2: yesterday's puzzle, today's call
So I flipped it.
- The main game is about yesterday. The data is already published, so every answer is instant. A new puzzle opens at 06:00 UTC, after Wikimedia publishes the previous day, and stays the same for everyone for 24 hours.
- The prediction survives as one daily "call" about today. It is always open, closes at midnight UTC, and its answer arrives with the next puzzle. That gives you a reason to come back tomorrow without making you wait to play today.
The previous version still settles its old picks, and its code lives on a git tag.
Generating questions that are actually fair
No LLM is involved. A rule-based generator builds the puzzle each day from two sources: articles from the previous day's most-read list (timely and often surprising) and a curated catalog of well-known articles (so estimation questions are about names people recognize).
The hard part is not building a question. It is building one that isn't trivially obvious or completely random. Some rules that made the biggest difference:
Keep head-to-heads close. A random pair is usually a blowout: one article has 10x the views of the other. I only pair articles whose counts are 1.02 to 1.3x apart.
"Most read" needs a real rival. One of the three alternatives must have at least 55% of the winner's views, so it's never "obviously the famous one".
Trend battles compare growth, not size. "Which rose the most against its own 7-day average?" lets a small article beat a giant. I limit the size spread between the four candidates and prefer races where the runner-up was close.
Ranges use human numbers. Answer bands are cut at roughly 80%, 100% and 125% of the article's weekly average, rounded to two significant figures:
// 12,345 → 12,000; 8,764 → 8,800
export function nice(n: number) {
if (n <= 0) return 0;
const p = 10 ** Math.max(0, Math.floor(Math.log10(n)) - 1);
return Math.round(n / p) * p;
}
Bands where the real number sits right on a cut are skipped, so nobody loses on a rounding hair.
Filter what shouldn't be a game. The most-read list is full of things that don't belong in a casual puzzle: adult sites, "Deaths in 2026", shootings, disasters. A title filter drops those, along with lists, non-article namespaces and very long titles.
Don't repeat yourself. An article used in a puzzle or call can't come back for a week.
Same puzzle for everyone. The random generator is seeded with the date, so the puzzle is deterministic. It can be rebuilt identically, tested against real fixture data, and is the same no matter which request triggers the build.
Some real answers from Oct 31: Halloween drew 202,666 views (+485% against its usual week), Trick-or-treating jumped +584%, and Day of the Dead reached 38,440.
The daily call has its own timing problem
The call about day D opens at 00:00 UTC on D. At that moment, D-1 isn't published yet, so the call can only use data through D-2. Its "7-day average" is D-8 to D-2. Calls rotate through four formats (beat the average, head-to-head, range, trend battle) so consecutive days feel different, and they fall back to the simplest format if the day's data can't support the planned one.
There's also an honest weakness worth admitting: since the puzzle is about yesterday, you could look the answers up. For a casual game that's fine. It's also exactly why any future leaderboard will be built on the calls, which are about a day that hasn't finished yet.
Stack
Deliberately boring:
- Next.js (App Router) + React
-
SQLite via Node's built-in
node:sqlite, on one small Google Cloud VM behind Caddy - An hourly collector job that fetches data, settles calls and builds the next puzzle as soon as the data appears
- GitHub Actions: typecheck, tests, build, then a locked-down SSH deploy
- Verified daily backups copied to Cloud Storage
- No account needed. Optional Google sign-in syncs your streak across devices
Tests include building a full puzzle from a real day's data saved as a fixture, which caught more generator bugs than anything else.
Try it
fomorrow.app: free, no sign-up, new puzzle every day at 06:00 UTC, and every past puzzle is in the archive.
It's brand new, so I'd love feedback: which questions felt too easy or too random, what was confusing in your first minute, and whether you'd come back tomorrow.
Top comments (3)
flipping it to yesterday's numbers is what saves it, since waiting 1.5 to 2 days for a score kills a daily game.
Thanks! It took me embarrassingly long to see it. I kept trying to make the forecast faster instead of asking whether people wanted to wait at all. The prediction still lives on as one daily call, so there's a small reason to come back tomorrow without making today feel empty.
Asking whether people wanted to wait at all is the sharper question. Keeping the prediction as one daily call gives tomorrow a reason to come back without slowing today down.