We can't choose what you pull, and you don't have to take our word for it.
Each pull is decided by a hash of three values. One of them is fixed and published in advance, one of them is yours, and the third just counts. Once we release the first, you can re-run any pull we decided for you and check that the card you got is the card the math gives.
How it works
- 1
We commit, before you play
Your account holds a random server seed you can't see. What you can see is its SHA-256 hash, and you can see it before you open anything. That hash is the promise: it fixes the seed, and it can't be worked backwards.
- 2
You supply half the input
Your client seed is yours to set, to anything you like, whenever you like. Because it goes into every pull, we would have to know your choice in advance to steer an outcome — and you can change it after we have already committed.
- 3
A counter keeps every pull distinct
The counter goes up by one each time. It never resets, not even when you change your client seed, so no two pulls ever share the same three values.
- 4
You rotate, and we publish
Retire the seed whenever you want. We publish it, you check that it hashes to the promise from step one, and then you can re-run every pull it decided, using the tool below. Check one, or check all of them.
The algorithm
Whole numbers throughout. Working in decimals would let our arithmetic and yours disagree in the last digit, and a pull landing exactly on the boundary between two cards would then verify to the wrong one — so weights are scaled to integers before anything is compared.
nonce below is that counter from step three. It keeps its name here because that is what the field is called in your pull record and in the code we published — renaming it on this page would leave you matching two different words to the same number.
message = client_seed + ":" + nonce
hash = HMAC_SHA256(key = server_seed, message)
roll = first 6 bytes of hash, big-endian // 0 .. 2^48-1
cursor = floor(roll * total_weight / 2^48)
winner = first card, in the recorded order,
whose running weight total exceeds cursorThe same thing, as code you can run
const bytes = new Uint8Array(
await crypto.subtle.sign(
"HMAC",
await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(serverSeed),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
),
new TextEncoder().encode(`${clientSeed}:${nonce}`),
),
);
let roll = 0n;
for (let i = 0; i < 6; i += 1) roll = roll * 256n + BigInt(bytes[i]);
const total = BigInt(dropTable.total_weight);
const cursor = (roll * total) / 281474976710656n; // 2^48
let cumulative = 0n;
for (const card of dropTable.cards) {
cumulative += BigInt(card.w);
if (cursor < cumulative) return card.id;
}Check a pull
Everything below runs in your browser and is sent nowhere. Your seeds are under Fairness in your account menu, and you can check a pull once you have rotated the seed that decided it, because that is when we publish the seed.
These fields fill themselves in. Open your vault, pick a card you pulled, and use Check this pull — it arrives here with everything already in the boxes.
The drop table especially. It is the pool and the weights exactly as they stood for that one pull, recorded on the pull itself — not the pack's odds today, and not written down anywhere else. So there is nothing to type into it by hand, and no pull can be checked against odds that changed after it happened.
