Expert verdicts on what AI builds.
Blog · Engineering

How a benchmark score is made

Our leaderboard is live, and it's empty. That's deliberate — nothing on it is simulated, and it fills only when real panelists score real runs. This post documents the pipeline that produces those numbers: the data model, the lifecycle of a run, the aggregation math, and the design decisions that make the board hard to corrupt. If you're going to trust a number, you should be able to read how it was manufactured.

The data model

Five tables and a view, in Postgres:

The lifecycle of a run

Everything enters through one admin-gated ingest endpoint that behaves like a small state machine:

# each step is an authenticated POST to /api/benchmark/ingest register-model slug, name, vendor register-environment slug, name, spec create-run model + environment → run (status: pending) record-score run + panelist + ratings{} + comment + minutes finalize-run run → status: scored # refused if no scorecards exist

Two gates matter here. First, who can score: record-score resolves the panelist by email against the applications table and refuses anyone who isn't an approved panelist. A pending applicant, a rejected applicant, and a stranger all get the same refusal. The panel application process isn't just recruiting — it's the access-control list for the benchmark.

Second, what can reach the board: finalize-run refuses to mark a run as scored if it has no scorecards, and the leaderboard only joins runs with status = 'scored'. There is no path for an unplayed artifact to produce a number.

The aggregation math

Scorecards are partial by design — a systems designer scores game-economy, a game-feel specialist doesn't have to. The aggregation handles this in two stages, both in SQL:

Stage one: per-run, per-category median. The ratings JSONB is unnested and each category's score for a run is the median across the panelists who scored it:

create view run_scores as select s.run_id, cat.key as category_slug, count(*) as n_panelists, percentile_cont(0.5) within group (order by (cat.value)::numeric) as median_score from scores s cross join lateral jsonb_each_text(s.ratings) as cat(key, value) group by s.run_id, cat.key;

Median, not mean, is the load-bearing choice. If three panelists score a mechanic 7, 7, and 1 — because one rater hit a bug the others didn't, or one is simply hostile — the mean says 5 and the median says 7. One outlier, in either direction, moves a median-aggregated score by at most one rank position of the panel, never by the magnitude of the outlier. For a small panel judging subjective quality, that robustness is worth more than the extra information a mean would preserve.

Stage two: per-model rollup. A model's overall score is the mean of its per-category run medians across all of its scored runs, with the per-category breakdown aggregated alongside:

create view leaderboard as select m.slug, m.name, m.vendor, count(distinct r.id) as n_runs, count(distinct s.panelist_id) as n_panelists, round(avg(rs.median_score), 2) as overall_score, jsonb_object_agg(rs.category_slug, round(rs.median_score, 2)) as category_scores from models m join runs r on r.model_id = m.id and r.status = 'scored' join scores s on s.run_id = r.id join run_scores rs on rs.run_id = r.id group by m.id, m.slug, m.name, m.vendor;

Then quorum. The API serving the public board splits the view's rows: models scored by at least three independent panelists are ranked; anything below that is held as provisional, reported as a count but never as a number. The threshold exists for one reason — below three raters, a single person's judgment is the score, and we won't publish a number one reviewer could have set alone. The median needs at least three points before it means anything.

Integrity is schema, not policy

Most benchmark-integrity promises are policies: "we don't accept payment for placement." Policies depend on the people enforcing them. We tried to push as much of the promise as possible down into places where breaking it is a schema violation rather than a decision:

What this rules out

Walking the pipeline backwards, each common way benchmarks go bad hits a wall: a model can't judge another model (only approved humans can hold a scorecard); a vendor can't buy a rank (there's no writable rank); a single enthusiastic or hostile reviewer can't set a score (median + quorum); an unplayed build can't produce a number (finalize refuses empty runs); and a leaked API key can't touch the data (deny-all RLS). What remains is the thing we actually want to measure: what shipped game developers concluded after playing what a model built.

The one-line version: every number on the board is the median judgment of at least three vetted humans who played the build, computed by a view nobody can write to.

What we'd add next

Two known limitations, on the roadmap in order: confidence intervals — with small panels, a score of 7.2 vs 6.9 may not be a real difference, and the board should say so rather than imply false precision; and pairwise preference aggregation — head-to-head judgments on same-environment runs, fed through Bradley–Terry, as a second ranking that doesn't depend on absolute-scale calibration at all. Both slot into the same schema: more columns derived from scores, still no table anyone can edit.

If you're training models that generate games and want your outputs inside this pipeline — or you've shipped games and want to be one of the humans behind the medians — talk to us.