["Emma","Liam","Sofia","Noah","Ava","Ethan","Mia","Lucas"]This page defines, byte for byte, how a NameWheel certified draw is committed, computed, signed and verified. It exists so that nobody has to trust our implementation: everything below can be reproduced independently from the public record of any draw, in any language, using only standard cryptographic primitives.
An open source reference verifier in Python and Node, using only standard libraries, lives at github.com/namewheel/truespin-verifier. The protocol is free for anyone to implement, audit or reimplement.
Every certified draw publishes a JSON record at https://namewheel.org/api/draw/CODE, where CODE is the draw code shown on the proof page and burned into the video. The fields relevant to verification:
entries — the full entry list, an array of strings, in draw orderentriesHash — SHA-256 of the serialized entry list, hexcommit — the pre-spin commitment, hexserverSeed — the revealed server seed, 64 hex characters, null until the draw completesclientEntropy — the browser's contribution, 32 hex characterswinnerIndex, winner — the recorded outcomevideoHash — SHA-256 of the hosted MP4, hexsignature — Ed25519 signature over the canonical record, base64protocol — the protocol version this record was created under, currently 1The entry list is serialized exactly as JavaScript's JSON.stringify produces it: an array of strings, compact separators, no whitespace, standard JSON string escaping, non-ASCII characters kept literal, encoded as UTF-8 bytes.
entriesHash = SHA-256( UTF8( JSON.stringify(entries) ) )
In Python the byte-identical serialization is json.dumps(entries, separators=(",", ":"), ensure_ascii=False). A weighted entry keeps its weight suffix inside the string, for example "Alice:3", so the hash covers the weights too.
Before the wheel moves, the server generates 32 random bytes and encodes them as 64 lowercase hex characters. That hex text, as ASCII bytes, is what gets committed:
commit = SHA-256( ASCII( serverSeedHex ) )
Note the input is the hex string itself, not the decoded bytes. Hashing ASCII("93d8...") and hashing the raw 32 bytes give different results. The reference verifier and the worked example below make this unambiguous.
The commitment is stored with the record at creation time, before the spin animation begins, and the seed field stays null until the draw finishes. Once the seed is revealed, anyone can recompute the hash and confirm it matches the commitment published first.
The host's browser generates 16 random bytes using crypto.getRandomValues, the cryptographically secure generator, encoded as 32 lowercase hex characters. This value is sent to the server in the same request that creates the draw, which means it is fixed before the server reveals its seed, and the server's seed is committed before it could know how the entropy affects the outcome. Neither side alone controls the result.
The winner is computed the moment the draw is created, deterministically, from three inputs:
message = ASCII( clientEntropy + "|" + entriesHash )
key = HexDecode( serverSeedHex ) // the raw 32 bytes
digest = HMAC-SHA-256( key, message )
n = BigEndianUint64( digest[0..7] ) // first 8 bytes
tickets = [] // weighted expansion
for i, entry in entries:
w = clamp( suffixNumber(entry, ":N"), 1, 99 ) or 1
append i to tickets, w times
winnerIndex = tickets[ n mod len(tickets) ]
winner = entries[winnerIndex] with any ":N" suffix removed
The wheel animation is then steered to land on this predetermined index. A guard in the client refuses to publish any video whose landing disagrees with the committed winner, and the record, not the animation, is authoritative.
Reducing a 64-bit integer modulo a small ticket count is not perfectly uniform. The bias favors low indices by at most T / 2^64, where T is the ticket count. For a draw with 100 tickets that is about 5.4 × 10-18, one part in 184 quadrillion. We state this openly rather than hide it: the bias is astronomically below any practical significance, and if a future protocol version adopts rejection sampling for formal perfection, the protocol field on each record guarantees that old draws verify under the rules they were created with.
After the recording is processed, the final hosted MP4 is hashed byte for byte:
videoHash = SHA-256( bytes of the hosted .mp4 )
The file is served from https://cdn.namewheel.org/v/CODE.mp4 when the record's cdn flag is true, otherwise from https://namewheel.org/v/files/CODE.mp4. Downloading it and hashing it yourself must reproduce the recorded value exactly. Any edit to any frame changes the hash.
Each completed record is signed with Ed25519. The public key is served in PEM format at namewheel.org/api/draw-key and does not change. The signed message is the following fields joined by a single newline character, in this exact order:
code \n title \n entriesHash \n commit \n serverSeedHex \n clientEntropy \n winner \n winnerIndex \n drawSeq \n createdAt
winnerIndex and drawSeq are decimal strings, and createdAt is the record's creation timestamp exactly as the API returns it. To verify with OpenSSL: save the public key as key.pem, write the canonical string to msg.txt with no trailing newline, base64-decode the signature to sig.bin, then:
openssl pkeyutl -verify -pubin -inkey key.pem -rawin -in msg.txt -sigfile sig.bin
Draw NT49TV59 is a live public record. Its values, which you can confirm against the API right now:
["Emma","Liam","Sofia","Noah","Ava","Ethan","Mia","Lucas"]2940378f4ef325f2ffb43303d714261e34965c66c5ce576dac7c314d721510ce9aaa418b091de20182f68c6fc4cd6cbf9f389eae3ec293e8e84981030c0a95b593d89f69704b8ac9384b37fefa6d3d907a49831052c248b7a3ff3ba71c490fad40b420671286b829642afbcfd1384c47winnerIndex 0 → EmmaVerify all of it in one command with the reference tool:
python verify.py NT49TV59 # or: node verify.js NT49TV59
Expected result: four PASS lines covering the entries hash, the commitment, the winner and the video hash.
A full pass proves the draw ran over exactly the listed entries, that the winner follows mathematically from randomness sealed before the spin, and that the hosted video is unedited. It cannot prove the entry list was collected fairly, and it cannot prove what a host did off the record; the proof page lists every certified draw the host has started against this same entry list, including ones started and never published, precisely so that selective re-running is visible rather than hidden. Records are never edited or deleted after completion.
Every record carries a protocol field. This document describes protocol 1. If any rule on this page ever changes, the version number increments, this page will document both versions, and existing records continue to verify under the version they were created with. A record is judged by the rules of its own era, permanently.
A proof that can quietly disappear is a weak proof. Two independent mechanisms keep certified draws checkable even in a future where NameWheel does not exist:
.ots proof files are public at /api/anchors. Anyone can confirm with the standard ots verify tool that a given draw record existed, byte for byte, no later than the day its manifest was anchored. Rewriting history would require rewriting Bitcoin.web.archive.org, outside our servers entirely.To check a draw against an anchor: fetch the manifest for any date on or after the draw, find the draw's code, rebuild the canonical string from the public record, hash it, and compare. Then verify the manifest's .ots proof against Bitcoin. Both steps use public data and public tools only.
A host can optionally lock the entry list before entries close, hours or days ahead of the spin. A freeze is a record with status: "frozen", no seed and no winner. Its commit_hash equals its entriesHash: the commitment is the list itself, serialized and hashed exactly as in section 2, published with a server timestamp at https://namewheel.org/f/CODE and https://namewheel.org/api/freeze/CODE.
The link between a freeze and a later draw is content, not a pointer: a proof page shows the freeze whenever a frozen record from the same account carries a byte-identical entriesHash and an earlier creation time. There is no field to edit that could forge the association, because changing a single entry changes the hash.
Frozen records enter the daily Bitcoin anchor manifests (section 11) with the recipe SHA-256("freeze" + LF + code + LF + title + LF + entriesHash + LF + createdAt), each field joined by a single line feed, so the freeze time itself becomes independently provable.
Commitment and reveal stop a host from choosing a winner inside one draw. They do not stop a host from running several draws and publishing only the one they like. That is the honest limit of any protocol where the host is the party who starts draws, so rather than claim it is impossible, the record makes it visible.
Every draw record carries an attempts object, also rendered on the proof page:
attempts: {
total: number of draws this account started with this exact entriesHash,
published: how many were revealed and published,
abandoned: how many were started and never published,
list: [ { code, createdAt, published, isThisOne } ]
}
Grouping is by entriesHash and account, across all time, not by a rolling time window. A draw started and never revealed stays in the list permanently as published: false. There is no endpoint that deletes or hides an attempt, and abandoning a draw does not remove it, so discarding results leaves a trail that a viewer can follow to every discarded code.
When total is 1 the proof page states plainly that this is the only draw the host ever started with that list. When it is greater than 1, the page names the count, the split between published and abandoned, and links each other attempt.
This is disclosure, not a change to the cryptography. The canonical signing string in section 7, the winner derivation in section 5 and every signature are unchanged, so records created before attempt disclosure existed still verify byte for byte with the same reference verifier. The protocol field stays at 1 for exactly this reason.
Questions, audits and disagreements are welcome: contact us. The fastest way to test any claim on this page is to pick any proof page and try to break it.
See verified spins in action