Technology & Fair Play
How a ROX result is produced, and exactly how much of it you can check.
The race engine
Races on ROX are decided by a deterministic draw. When a race settles, one seed is drawn and stored on the race itself. Every horse's result comes from that seed and nothing else: its roll is md5(seed : id) read as a number between 0 and 1, and its score is that roll plus a rating advantage worked out against the field it is running in. Highest score wins, and horse id breaks any tie, so the order is total and stable. The exact arithmetic is below. It has changed twice — on 18 and 25 August 2026 — so check the race's score_version before you check its arithmetic; the note below says which rule decided which races.
Because the seed is stored and the rule is fixed, the stored result reproduces the race exactly — anyone can recompute the finish and get the same answer. The published record is the seed, the field, the finishing order, each runner's finishing time, and its motion through the race — distance and speed sampled five times a second from start to line.
Race distances and pacing are calibrated to real thoroughbred racing: a 2,200 m race is designed around 2 minutes 10 seconds — 60–66 km/h — with longer trips tapering realistically.
Finishing times and running are both recorded. A settled race stores each horse's finishing time as well as its position, and every racecourse publishes its record — the fastest winning time ever run there. It also stores each horse's motion: distance, speed and lane sampled at 5 Hz for the whole race, which is what the replay plays back. Nothing is re-simulated at viewing time, so every screen shows the same running of the same race.
One result, every screen
The web platform and the coming Unreal Engine 5 client are two views of the same stored race. Because the seed and the finishing order are on record, both show the same winner in the same order — nothing is re-simulated, so nothing can disagree.
The per-horse motion a replay needs is stored, which is what the 2D replay plays back today. What is not built yet is the UE5 client itself — the cinematic 3D view of that same stored run.
Its cinematic mode will render races in high visual quality regardless of the player's hardware, because the outcome needs no computation at all — it is already on record.
The platform
The web application is built on Next.js with server-side rendering, backed by Supabase (PostgreSQL) for race results, entries and accounts, with row-level security guarding every table. Static assets are served through Cloudflare. Every finished race is archived permanently — the seed, the field, each horse's rating at the off, the finishing order, each runner's finishing time, its motion through the race and the payouts — which is what powers the results archive, the rankings, the course records and the replay.
Payouts are computed automatically from the stored result by preset rules — pool, tax, split — with no manual step between the finish line and the money.
Race results
Randomness decides races, mint rolls and breeding outcomes on ROX — so how that randomness is produced, and whether anyone could bend it, is the most important question on the platform. This page is the answer.
Every race is decided by one seed, and that seed is published with the result. The sequence matters:
- A race is announced — class, course, distance, entry fee — and entries open.
- Owners enter and pick gates. Nothing about the race can change during this window.
- Entries close. Only then is the race drawn.
- The seed is stored on the race itself and published with the finishing order.
- Anyone can take that seed and recompute the finish, horse by horse, and get the same answer.
Because the seed is published and the rule is fixed and public, a result cannot be quietly changed after the fact. Reorder a finish and it no longer matches its own seed, and anyone re-running the arithmetic sees it immediately. Checking a result yourself, below, shows you how to do exactly that.
What we do not claim: the seed is drawn by ROX at settlement, so the published result provably follows from the published seed, but you are trusting us on how the seed itself was chosen. We would rather say that plainly than let you assume otherwise.
Mint rolls
Coat colour, pattern and hair are rolled at mint against odds published in these docs before the sale — the same odds at every price tier. Verifiable randomness (such as Chainlink VRF, which the original ROX contracts on Polygon used for exactly this purpose) is the natural fit for on-chain mint rolls, so that even the platform cannot re-roll a coat.
Breeding
A foal's bloodline is drawn under the published inheritance odds — a fixed probability table set by the two parents' bloodlines — with hard structural limits that no random outcome can cross: a pure blood (Hyperion DN0) can only come from the King or the Queen covering another Hyperion — no other pairing reaches DN0 at any odds, and those two horses hold ten covers between them for the life of the game — and every Hyperion that will ever be born, at any rung, takes one of 100 crown numbers, each issued once. The randomness makes every foal a surprise; the structure makes every rarity promise unbreakable.
Checking a result yourself
Every finished race publishes its seed alongside the finishing order, in the result and in the game API. You do not have to take our word for the order:
Take the race's
seedand the list of horses that ran, with each one'srating_at_start— the rating frozen onto its gate when the race went off.For each horse compute
md5(seed + ":" + id), take the first eight hex characters, read them as a number and divide by 2³² — that is its roll, a value between 0 and 1.Use the numeric
id, not the public ROX number. A horse has two identifiers:id, a plain number that is the internal key, and its public handle such asROX-00041, which is what the site shows. We hashid— the number. Hashing the public handle, or any other number, will not reproduce the result. On a race's gates the same number is stored in the column namedhorse_id, so if you are reading gate rows, that column is theidto use.Work out each horse's advantage from its rating, measured against the field:
low = the lowest rating_at_start in the race high = the highest edge = 0.55 × ((high − low) ÷ ((high − low) + 17)) × (rating_at_start − low) ÷ (high − low)If every runner is rated the same,
highequalslow, the edge is zero for everybody and the race is decided by the rolls alone.Its score is
edge + roll.Sort by score, highest first, breaking ties by the lower
id.
That is the finishing order. It is a few lines in any language, and it will match what we published or we have a problem worth hearing about.
Which rule decided your race
The rule above is the third one. It will not reproduce a race settled before 25 August 2026, and following it on an older race will tell you the result is wrong when it is not.
Every race publishes score_version. Read it and pick the matching rule — do not
infer it from a date, and do not assume the current formula applies to an older
race.
score_version |
Rule that decided it | When |
|---|---|---|
3 |
The formula above: edge = 0.55 × ((high − low) ÷ ((high − low) + 17)) × (rating_at_start − low) ÷ (high − low), then score = edge + roll |
from 25 August 2026 |
1, settled on or after 18 August 2026 |
edge = 0.35 × min(1, (high − low) ÷ 30) × (rating_at_start − low) ÷ (high − low), then score = edge + roll |
22–24 August 2026 · 3 races |
1, settled before 18 August 2026 |
score = roll × (rating_at_start + 1) |
no finished race carries it today |
score_version was added on 25 August 2026 and backfilled onto races already
settled, so 1 covers both of the older rules and the settlement date separates
them. Every version from 3 onward names exactly one rule, because the column now
exists before the change rather than after it. There is no score_version 2: a
version 2 was written and never decided a race, so no result carries it.
Why it changed the first time (18 August). The score used to be the roll multiplied by the rating plus one. That scaled a horse's entire range by its rating, so a low-rated runner could not beat a high-rated one at all — in a twelve horse field, half the runners could not win however the draw fell, and we were taking bets on them. Rating now shifts the draw instead of scaling it.
Why it changed the second time (25 August). The min(1, … ÷ 30) was a ceiling.
Once two horses were 30 rating points apart, widening the gap changed nothing: a
horse 30 points better and a horse 324 points better were identical to the engine,
so the entire top of the rating scale decided nothing and campaigning a horse past
that point bought no advantage at all. The min is now a curve that never pins, so
every rating point counts.
We are stating the boundaries this precisely because of what this page promises below: that recomputing will match what we published. For those 3 older races it will only match under the rule that actually ran them. Anyone who checked a race from that window against the formula above and concluded we were rigging results was following our instructions correctly, and the fault was ours.
If you would rather not write the code, the database does it for you:
select * from verify_race('<race id>') returns the recomputed order beside the
recorded one, with a matches column that must be true on every row. The race id
is the one in the race's own link — the event_key the API returns, and the id in
a replay URL.
Two columns can come back empty, and it is worth saying why rather than leaving
you to guess. finish_time and time_matches are blank for races run before we
started versioning race times: those times cannot be recomputed, so we will not
show them as either matching or wrong. The matches column — the finishing
order, which is what the seed actually determines — is checkable on every race we
have ever run.
Today that is 191 finished races and 2,292 runner rows, every one of them checkable this way. The last full audit — 16 September 2026, across all 186 races and 2,232 runner rows — recomputed every finishing order from its published seed and found no mismatches.
What this proves and what it does not. It proves the published finish follows from the published seed — a result cannot be quietly reordered after the fact. It does not prove the seed itself was beyond our reach, because ROX draws it, and we do not claim otherwise.
The principle
Fairness on ROX is not a policy, it is arithmetic: the race is drawn only after entries close, and the seed it was drawn from is published with the result. That is what makes a finish reproducible by anyone who wants to check it, and what makes a quietly altered result impossible to hide.