Game Verdict
A crowdsourced answer to an argument PC gamers have been having forever: is this game better with a controller, or with a keyboard and mouse?
- Verdicts cast
- 2,906live · /api/stats
- Games tracked
- 1,918live · /api/stats
- Commits
- 1,08013 Oct 2026 snapshot · private repo
- Per browse page
- 50 KBwas 1.8 MB
Everyone has an opinion, nobody has data
TL;DR
Steam will tell you a game has “full controller support.” It won’t tell you whether anyone actually prefers playing it that way. Game Verdict asks players directly, dedupes anonymous votes with a browser fingerprint, and surfaces the leading input method as a community badge.
Games are searched across Steam and IGDB in parallel, deduped and relevance-ranked, with metadata, cover art, and Steam library hero images pulled in automatically when a game is added.

Free to vote, hard to fake
TL;DR
The whole thing only works if voting is nearly free, and it only means anything if the votes can be trusted. Those pull against each other, and almost every decision in the app is an answer to that tension.
Quick Vote takes the friction out: no account, one game at a time, skip anything you don’t recognize. A browser fingerprint keeps it honest without putting the friction back, counting each voter once whether or not they ever sign up. Sign in later and the votes you already cast are claimed rather than lost.

A quarter of the traffic is on a phone, and the one-card-at-a-time shape was built for exactly that. Nothing about the loop changes on a small screen.

How do you count a vote from someone who refuses to identify themselves, without letting them vote a hundred times?
You can also vote from the game page itself, and the breakdown updates in place rather than sending you somewhere to see what you just did.


The queue knows what you have played
TL;DR
Asking someone to judge a control scheme only works if they have actually played the game, so the queue is ordered by Steam review count rather than by how many verdicts a game already has. Review count is a proxy for “have you actually played this?”. Verdict count would surface the games that are already well answered, which is backwards.
On top of that it subtracts what you have voted on, skipped, watched, and owned — all resolved in a single parallel round trip — then promotes your own unplayed library games to the front while keeping them in popularity order. A stable partition, not a re-sort.
The fingerprint does double duty here. Because the exclusion set is fingerprint-aware, an anonymous voter’s queue shrinks as they vote. The queue gets personal for someone who never made an account.
| State | Queue order |
|---|---|
| Signed in, library synced | Your unplayed games, then popular ones |
| Signed in, no library | Popular games, minus voted and skipped |
| Anonymous | Popular games, minus what your fingerprint already voted |

Prose generated from data, not from a model
TL;DR
Every game page ends with a plain-language verdict: how many people voted, which way they leaned, and which controller they tend to use. It’s written by a buildVerdictSummary() function, not by a language model. Four branches on the leading choice, an extra clause when the top two land within ten points of each other, and a controller-subtype sentence that only appears once at least two people have reported one.

The same string is also emitted as the FAQPage answer in structured data. So roughly 1,600 pages that would otherwise be near-identical templates each carry unique text answering the literal thing people search for, and the reader and the crawler are served by one sentence rather than two systems.
No latency, no per-call cost, no hallucinated percentages, and the same input always produces the same output. A model would have done this worse, slower, and for money.
The bug that froze the front page
TL;DR
Home page stats stopped moving. Not wrong — frozen, at a number that looked perfectly plausible. The cause was that getAllGames() ran an unbounded SELECT * against the verdicts table, and PostgREST silently caps responses at 1,000 rows. Everything worked exactly right until the site crossed its thousandth verdict, and then it quietly stopped counting.
The failure mode was not an error. It was a number that stayed believable while it stopped being true.
The fix was a game_vote_summary aggregate view doing the counting in Postgres. Which raised the more interesting question: why was the browse page shipping the entire games table to the client at all?
1.8 MB → 50 KB per pageview
TL;DR
Browse was slicing a full table read. It became a games_browse Postgres view with filtering, sorting, and counting pushed into SQL behind LIMIT/OFFSET, fed by a route handler driving infinite scroll. A 24-row page costs about 50 KB. The old one cost about 1.8 MB — every single pageview, and worst during crawler bursts, when concurrent requests each independently pulled the whole list.

Reasons to come back, and somewhere else to be
TL;DR
Twenty badges across six categories are evaluated after every verdict, reaction, and library change, with the first unlock arriving as a toast. Profiles are public, with a verdict history and an input-method tendency bar you can click to filter.

The Discord bot is the part that leaves the site entirely. Five slash commands run on a Cloudflare Worker that verifies Discord’s Ed25519 signatures, returning rich embeds with cover art, the verdict color, and the bar breakdown, so an argument in a group chat can be settled without anyone opening a browser.


And the home page keeps the most contested games up front, because a game the community can’t agree on is a better invitation to vote than one that is already settled.



Invite-only, on purpose
TL;DR
Comment threads on game pages were the small version of this. The community hub is the large one: four boards, markdown posts with a real formatting toolbar, threaded replies, reactions on both, emoji with shortcode typeahead, GIFs, friend requests, and direct messages. Replies are voted Reddit-style, so the best answer to “is this one better on a pad?” rises to the top of the game it is about instead of sitting wherever it landed chronologically.

A post can attach a game, which embeds its cover and its live verdict split into the thread. That is what makes this the site’s forum rather than a forum bolted onto it. An argument about Oblivion carries the current breakdown for Oblivion, moving as people vote, so the discussion and the data it is arguing about can’t drift apart. Author names carry the badges you earned voting, so the identity you build casting verdicts is the one you turn up with.

It is invite-only, and that was the plan rather than a limitation. A forum nobody has posted in is worse than no forum, so early access went to the most active voters to seed real threads before the doors opened. Everyone else gets a warming-up animation and an honest note about what it is. Making something feel worth getting into is a nicer problem to have than making an empty room look busy.

Every post and comment carries a report button with four reasons, alerting mods and admins into a queue that gets worked by hand. Moderators pin and lock inline. Deletes are soft, so a removed post is still there to look at when someone asks why it went. Blocking is mutual and filters the feed rather than just hiding replies. None of that was asked for by the fifteen or so accounts currently in there. It exists because the first person who needs reporting shows up before the first moderator does, and that is not something you can add in a hurry.

The GIF picker got a harder problem than it deserved. Google shut off the Tenor API on 30 June 2026, taking GIF support out of Discord, X, WhatsApp and Bluesky with it, and Giphy had already moved off free access. GIFs landed here two weeks later on Klipy, built by ex-Tenor engineers as a deliberate near-clone of the thing everyone had just lost. Rendering is restricted to an allowlist of CDN hosts rather than trusting whatever URL ends up in a post, and Tenor’s CDN stayed on that list, because its servers outlived its API.
An easter egg you get to keep
TL;DR
Type ↑ ↑ ↓ ↓ ← → ← → B A and hit Enter or Space. Keyboards, mice, and controllers rain down the page, which is a nice five seconds and then it’s over.

What’s actually interesting is what survives the five seconds. The code unlocks CRT mode, and CRT mode isn’t a moment: it’s a real setting, saved to your profile and hydrated on the server, that you can turn back on whenever you like. Three fixed layers do the work, all of them pointer-events: none so nothing underneath stops being clickable.


It also lives in Settings, next to the Steam library import, which is where an easter egg stops being a joke and becomes a feature somebody might actually prefer.

There’s a second code. Finish the sequence, tap Tab for Select, then hit Enter, and you get the Contra two-player variant. It greets you differently, keeps greeting you on every entry rather than only the first, and offers the most secret badge on the site.


All of it works signed out. The rain falls, the CRT turns on, and the badge waits until you feel like claiming it. Which is the same idea as the voting, arrived at from a completely different direction: the good part should never be behind the account.
The code works here too. This page has been listening the whole time.