A verifier is easier to inspect when the cryptographic operations, input rules, and game mappings fit within a small module. Version 1.1.0 of provably-fair-verifier takes that approach: one 61-line src/index.js, no dependencies, and tests built on Node's own tools.
The useful architectural question is how those pieces connect. A commitment check answers whether a revealed seed hashes to an expected commitment. A deterministic mapping answers which result follows from that seed and the other inputs. The module returns both, leaving callers responsible for interpreting them correctly.
The repository's working parts are small enough to list:
src/index.js exported ESM implementation
test/index.test.mjs vectors, reference generator, validation tests
package.json entry point, test command, Node requirement
README.md API usage and formula boundaries
CHANGELOG.md version history
LICENSE MIT
package.json declares Node >=18, ESM through "type": "module", and src/index.js as both the main entry and export. There are no runtime or development dependencies. The MIT license permits reuse subject to its notice requirements. Version 1.1.0 adds Mines and Plinko plus the stakeFloats, minesFromFloats, and plinkoFromFloats exports.
To clone and run the tests with Node 18 or newer:
git clone https://github.com/cryptoplayguide/provably-fair-verifier.git
cd provably-fair-verifier
npm test
No dependency installation is needed for this checkout. The test script runs node --test test/. For use as a dependency in another project, the documented installation is:
npm install github:cryptoplayguide/provably-fair-verifier
The package's import name is @cryptoplayguide/provably-fair-verifier. The README says it is not yet published to the npm registry; GitHub is the installation source.
For a local verification, save the following as verify.mjs in the cloned repository and run node verify.mjs. The input declaration is copied verbatim from test/index.test.mjs; the calls exercise the same vectors:
import { webcrypto } from 'node:crypto';
import { fairResult } from './src/index.js';
const input = { game:'dice', server:'e3f1b5c2a9d84f6e7b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6', hash:'1c7bc4411405303e0f84ab57dea9691c6376ee6891b289a65edac408519e6009', client:'example-client-seed', nonce:'3', salt:'example-public-salt' };
const dice = await fairResult(input, webcrypto);
console.log(dice.matches, dice.result);
console.log((await fairResult({ ...input, game: 'limbo' }, webcrypto)).result);
console.log((await fairResult({ ...input, game: 'mines', mines: 3 }, webcrypto)).result);
console.log((await fairResult({ ...input, game: 'plinko', rows: 8 }, webcrypto)).result);
The expected values are true and 42.62 for Dice, 2.32 for Limbo, mine positions [10, 14, 7], and Plinko { path: [0,1,0,0,1,0,0,0], slot: 2 }. These are existing assertions, rather than newly generated fixtures. The tests also contain a separate Crash vector yielding 1.44, a 24-mine vector, and a 16-row Plinko vector.
Web Crypto is the boundary between platform services and deterministic mapping. Both asynchronous entry points accept an injectable provider, defaulting to globalThis.crypto. This signature is verbatim:
export async function stakeFloats(server, client, nonce, count, webCrypto = globalThis.crypto) {
Passing Node's webcrypto explicitly, as above, avoids depending on a global being available. Browser callers can use the default where Web Crypto is available. The implementation delegates SHA-256 and HMAC-SHA256 to subtle.digest, subtle.importKey, and subtle.sign; it does not implement cryptographic primitives itself.
The return objects also support inspection beyond a final number. Dice, Limbo, and Crash expose commitment, matches, bytes, digest, value, and result. Mines and Plinko expose commitment, matches, floats, and result. An integration can therefore inspect the intermediate representation when diagnosing a disagreement, rather than receiving only an opaque success flag. Consumers should account for these different shapes when displaying results.
There is no network request or seed generation in the module. The caller supplies the inputs; the module recomputes their consequences. This keeps browser presentation, storage, and data collection outside the cryptographic code. It also makes the local example reproducible without querying an external service.
Input validation runs before computation in fairResult. It accepts only the five supported game identifiers, requires a 64-character hexadecimal commitment, and additionally requires a hexadecimal server value for Crash. Non-Crash nonces must contain digits and convert to safe integers. Mines accepts counts from 1 through 24; Plinko accepts 8 through 16 rows.
This is validation without input repair: malformed hashes are rejected rather than padded, and invalid counts are rejected rather than clamped. There are explicit conversions, including Number(nonce) when constructing messages and lowercasing the validated hash for comparison. That distinction matters when integrating the API. The validator is not a complete application schema: it does not separately validate every client or salt field, and the exported mapping helpers assume suitable float inputs.
Dice and Limbo use Stake's published formula: HMAC with the server seed as key and clientSeed:nonce:0 as message. Four bytes become a fractional value by summing successive base-256 fractions. Dice maps that value through floor(value * 10001) / 100.
Limbo's reciprocal calculation needs an explicit boundary rule. A zero float would otherwise produce Infinity. The module throws RangeError at zero and also rejects nonnumeric, nonfinite, and out-of-range values. The tests check the dedicated zero message and assert that limboMultiplier(0.5) returns 1.98. This fix predates the new games: the changelog records it under v1.0.1.
Mines and Plinko share the multi-round byte generator. Each HMAC message includes a round counter; each 32-byte digest supplies eight floats, consuming four bytes apiece. More rounds are generated until the requested count is reached. Tests compare this output with a reference implementation using Node's createHmac, providing a separate implementation path for the byte stream.
The suite checks failure behavior alongside known outputs. It rejects malformed Crash seeds, a negative nonce, and an unsupported game. Another assertion supplies an all-zero commitment and expects matches to be false, distinguishing a well-formed mismatch from invalid input. Helper tests exercise tile selection and left/right mapping directly. These checks document the current contract, while the reference generator comparison specifically checks agreement between the two HMAC implementations; it does not independently certify every possible operator's rules.
Mines uses Fisher-Yates selection over 25 tiles. The implementation removes each selected tile from the remaining list, preventing duplicate positions. Here is the complete helper, verbatim:
export function minesFromFloats(floats, mines) {
const tiles = Array.from({ length: 25 }, (_, i) => i), out = [];
for (let i = 0; i < mines; i++) out.push(tiles.splice(Math.floor(floats[i] * (25 - i)), 1)[0]);
return out;
}
fairResult requests 24 floats for Mines regardless of the mine count; the helper consumes the requested number. Tile indices run from 0 through 24, left to right and top to bottom. Keeping float generation separate from selection makes the mapping independently inspectable.
Plinko's helper is equally direct, also quoted verbatim:
export function plinkoFromFloats(floats, rows) {
const path = floats.slice(0, rows).map(f => Math.floor(f * 2));
return { path, slot: path.reduce((a, b) => a + b, 0) };
}
Each row consumes one float: 0 means left, 1 means right. Summing right turns gives the landing slot. There are deliberately no multipliers: payout tables depend on risk settings and the operator. A path verifier cannot infer those tables from the random stream alone.
Crash follows bustabit's published formula and has a different byte contract. The server value is decoded from hexadecimal before hashing for the commitment and before use as the HMAC message. The salt is the HMAC key. The first 13 hexadecimal digest characters provide 52 bits; the result uses Math.max(1, Math.floor(99 / (1 - value)) / 100). Treating the seed as UTF-8 text instead would change the calculation.
Finally, matches confirms only that the computed commitment equals the supplied hash. It does not compare the computed result with a separately reported outcome, establish that the commitment was displayed before play, or prove payout reliability. A caller must preserve the original commitment, select the correct documented formula, and compare the returned result independently. Other implementations can use different rules.
The browser verifier provides another interface for checking results. The repository exposes the underlying operations for local inspection: platform cryptography, explicit validation, deterministic mappings, and reproducible vectors.
18+ only. Educational use. Gambling carries financial risk; verification does not remove the house edge. Gamble responsibly.
Top comments (0)