A working summary — plain English, then the math, then how to verify it yourself. Read top-to-bottom or jump to any section.
Every random-outcome feature — pack opens, spin wheel, mystery boxes, raffles — requires you to trust that we didn't nudge the result. "Sorry, common pull" is a reasonable answer on a 70%-common tier, but it's indistinguishable from a server that quietly down-weighted rares.
"Trust us, we wouldn't do that" isn't a verification. These receipts narrow the trust required; they do not eliminate it. Each listed flow leaves commitment and timing evidence after the roll. New generic draws use opaque public client seeds and store an explicit weight_order array, so the recorded inputs can reproduce the exact outcome. Older generic rows stored weights only as a JSON object; PostgreSQLjsonb did not preserve their original key order, so those rows remain partial even when an owner can see an older account-linked client seed.
Before rolling, our server generates a random server_seed (32 bytes) and stores both the seed AND its sha256(server_seed)— the "commitment" — in the database. This happens BEFORE the rolling logic runs. The row's committed_at timestamp is recorded at this point; it is not an independent timestamp and a database administrator can rewrite it.
The roll itself is deterministic:
roll = sha256(server_seed + ':' + client_seed + ':' + nonce)[0..13] / 2^52 rolled_key = pickWeighted(weights, weight_order, roll)
Then we update the row with the rolled_key and revealed_at. The separate writes record that committed_at < revealed_at in our database. Generic draws are not externally witnessed before the roll, and the server chooses every entropy input, so this sequence does not prove that no favorable tuple was selected before the commitment write.
Visit any pull's verify page ( /verify/pull/[id] or /verify/draw/[id] ) and up to four separate checks run in your browser:
server_seed and re-hash it. The output must equal the commitment stored at committed_at.pickWeightedresult. Generic receipts also need a valid ordered-weight array. Legacy generic rows without that array are reported as partial rather than replayed in the database's present object order.committed_at <= revealed_at — and these are two separate SQL writes, so the ordering reflects real time.The per-draw checks establish internal consistency, but they are only as good as our database integrity. A motivated attacker with write access could edit a row before any external observer captures later digest evidence.
So we add a second layer: every few minutes, a cron takes all newly-revealed draws and hashes them into a Merkle tree. The root lands in fairness_digests and each draw is stamped with its merkle_digest_id and merkle_leaf_index. The leaf format is stable:
leaf = sha256(id + '|' + commitment + '|' + server_seed + '|' + revealed_at_iso)
Once a digest root is published, editing any underlying leaf changes the root — detectable to anyone who cached the old root. The public feed at /api/verify/digests lets auditors (or anyone) snapshot the timeline and compare later.
When you verify a draw, the verifier fetches that digest's leaves, re-hashes the pairs bottom-up, and confirms the root matches the live feed. A changed leaf fails unless the feed is rewritten too; an externally saved earlier root is what exposes that second case.
| commitment (sha256 of seed) | public with receipt | stored before the application roll step; generic draws have no external pre-roll publication |
| server_seed | public AFTER reveal | meaningless before the roll; necessary to verify after |
| client_seed (new opaque format) | public | needed for outcome replay and contains no account id |
| client_seed (legacy account-linked) | owner-only | anonymous replay is partial rather than exposing the account UUID |
| nonce, weights, outcome | public | required values for re-running the math |
| weight_order (new generic receipts) | public | JSON arrays preserve the selection order that jsonb object keys do not |
| user_id | private | not exposed by the verifier APIs |
| timestamps | public | record application write order; they are not independently witnessed |
A single dep-free ES module implementing every check on this page lives at /verify/cambridgetcg-verifier.js. Import it into any browser or Node 18+ script and run our claims through your own code:
import * as v from 'https://cambridgetcg.com/verify/cambridgetcg-verifier.js';
const { verdict } = await v.fetchAndVerifyPull('<pull-id>');
console.log(verdict.allMatch === null ? 'partial' : verdict.allMatch ? 'verified' : 'failed');Covers primitives (sha256, rollFloat, pickWeighted), per-draw verification (verifyDraw), Merkle inclusion (verifyInclusion), and hash-chain integrity (verifyChain). MIT-licensed, ~250 LOC. verifyDraw reports allMatch: null when a client seed is withheld or a generic receipt lacks a valid weight_order; that means partial, not failed. If the file ever diverges from this page, please report the bug.
For scripting or third-party auditing, /api/verify/compute accepts the raw inputs and returns pass/fail without a UI:
curl -s -X POST https://cambridgetcg.com/api/verify/compute \
-H 'Content-Type: application/json' \
-d '{
"commitment": "<hex>",
"server_seed": "<hex>",
"client_seed": "<opaque-client-seed>",
"nonce": 123456789,
"rarity_weights": { "common": 0.7, "uncommon": 0.2, "rare": 0.1 },
"claimed_rarity": "common"
}'CORS-wildcarded, no auth. Supply only a client seed you are entitled to see. The response includes both pass/fail flags and the recomputed roll + hash.
These receipts prove a narrower statement: the revealed seed hashes to the stored commitment, the available inputs reproduce the recorded outcome, and a digest can reveal later rewriting relative to a copy held outside our control. They do not prove unbiased seed selection: generic draws use server-generated server seed, client seed, and nonce, with no external pre-roll witness. A stronger design needs entropy supplied by the participant or an external randomness beacon committed before selection.
They also do not prove that the weights are the weights you would want. Admins can tune tier weights and pools; each receipt records the weights used for that draw.
The observed distribution dashboard compares recorded outcomes with recorded or configured weights. It can flag drift; it cannot establish unbiased seed selection for an individual draw.
/verify/pull/[id]/verify/draw/[id]/api/rewards/raffles/[id]/proof