This is a technical cryptographic audit. It is not a casino review, not a recommendation, and not an advertisement. No affiliate links, no promotional language, no “sign up” buttons. This document exists for one purpose: to answer the question “does this platform’s provably fair system actually work?” — with evidence.
We extracted every fairness-related JavaScript module from Duel.com’s production frontend, decompiled the algorithms, and verified their cryptographic correctness against established standards. The creator of Duel.com (known as Monarch) gave us explicit permission to publish this analysis.
Platform: Duel.com
Category: In-house games (“Originals”) only — third-party provider slots and live dealer are out of scope
Games audited: 12 (Dice, Mines, Keno, Plinko, Blackjack, Video Poker, Cross Road, PF Slots, Coinflip, Crash, Castle Roulette, Rock Paper Scissors)
Method: Static analysis of production JavaScript bundles extracted from the live site
Date: May 2026
Auditor: ProvablySmart Research Lab
Duel.com is a single-page application built with Vue.js 3.5.17 and bundled with Vite. The entire frontend — including all fairness algorithms — is delivered as client-side JavaScript. This means the code that determines game outcomes runs in the player’s browser and is fully inspectable.
We identified and downloaded four JavaScript modules containing all fairness logic:
| Module | Size | Games Covered |
|---|---|---|
blackjackFairness-qTk8WM6N.js | 5.6 KB | Blackjack + shared HMAC-SHA256 primitives (hexToBytes, bytesToHex, generateHMAC_SHA256) |
videoPokerFairness-Drkyg7jr.js | 15.2 KB | Dice, Mines, Keno, Plinko, Cross Road, Video Poker |
verify-DuBSsjaY.js | 60.9 KB | Coinflip, Crash, Castle Roulette, Rock Paper Scissors, PF Slots + verification UI |
FairnessNextSeed-Bsx8KEeZ.js | 3.6 KB | Seed lifecycle management, rotation, guest mode, localStorage persistence |
All modules contain developer-written comments explaining the algorithms. The fairness code is not obfuscated — only the surrounding Vue.js component code is minified by Vite’s production build.
For each game, we verified:
Duel.com employs three distinct fairness models, selected based on the trust topology of each game type. This is unusual — most platforms use a single model for all games.
| Model | Trust Basis | Games | Randomness Source |
|---|---|---|---|
| A: Seed Triple | Commit-reveal of server seed + player-chosen client seed | Dice, Mines, Keno, Plinko, Blackjack, Video Poker, Cross Road, PF Slots | HMAC-SHA256(serverSeed, clientSeed:nonce:cursor) |
| B: drand Beacon | External decentralized randomness (League of Entropy) | Coinflip, Crash, Castle Roulette | HMAC-SHA256(serverSeed, hexToUtf8(drandSeed):0) |
| C: Commit-Reveal | Mutual commitment between two players | Rock Paper Scissors | SHA-256(choice|clientKey) per player |
All games ultimately depend on a single HMAC function. Extracted from blackjackFairness:
async function generateHMAC_SHA256(keyHex, message) {
const keyBytes = hexToBytes(keyHex);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, message);
return bytesToHex(new Uint8Array(signature));
}Assessment: Correct. Uses the Web Crypto API (crypto.subtle), which is:
The key is passed as raw hex bytes (not UTF-8 string), and the message is a Uint8Array. Both are correct for HMAC-SHA256.
function hexToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
function bytesToHex(bytes) {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}Assessment: Standard implementation. padStart(2, '0') ensures consistent zero-padding for bytes 0x00–0x0F. No edge cases.
function hexToUtf8String(publicSeed) {
const bytes = hexToBytes(publicSeed);
return new TextDecoder('utf-8').decode(bytes);
}Assessment: The drand beacon’s randomness value is received as a hex string and decoded to UTF-8 before being used as the HMAC message. This is a design choice — it means the effective entropy comes from the UTF-8 representation of the drand bytes, not the raw bytes themselves. This does not reduce security because the drand output is already cryptographically random and the HMAC operation preserves entropy.
After generating an HMAC hash, the platform must convert the 256-bit output into a game-specific result within a bounded range. This is where most provably fair implementations introduce subtle biases. We identified three distinct extraction methods.
Used by: Dice, Mines, Keno, Blackjack, Video Poker, Cross Road, PF Slots.
The canonical pattern, from the Dice module:
const MAX_UINT32 = 0xFFFFFFFF; // 4,294,967,295
const RANGE = 10001;
const MAX_FAIR = MAX_UINT32 - (MAX_UINT32 % RANGE);
while (offset + 8 <= hash.length) {
const value = parseInt(hash.slice(offset, offset + 8), 16);
if (value < MAX_FAIR) {
return (value % RANGE) / 100;
}
offset += 8;
}Mathematical analysis:
MAX_UINT32 = 2³² − 1 = 4,294,967,295RANGE = 10,001, so MAX_FAIR = 4,294,967,295 − (4,294,967,295 mod 10,001) = 4,294,967,295 − 7,294 = 4,294,960,0017,295 / 4,294,967,296 ≈ 0.00017%(7,295/4,294,967,296)⁸ ≈ 1.24 × 10⁻⁴⁶Assessment: Correct. This is the standard rejection sampling technique recommended by NIST SP 800-90A for converting uniform random bits into a uniform value in a non-power-of-two range. The rejection probability is negligible, and the hash provides 8 independent 4-byte chunks as fallback.
Used by: Coinflip (% 2), Plinko bounces (% 2).
// Coinflip
const value = parseInt(hash.slice(0, 8), 16);
const coinflipResult = (value % 2) + 1;
// Plinko (per bounce)
position += (value % 2);Assessment: Correct. Since 2³² is exactly divisible by 2, value % 2 has zero modulo bias. No rejection sampling is needed. This is optimal.
Used by: Castle Roulette (% 48).
const RANGE = 48;
const value = parseInt(hash.slice(0, 8), 16);
return (value % RANGE).toString();Assessment: This is the one exception. 2³² mod 48 = 16, which means values 0–15 have a probability of ⌈2³²/48⌉/2³² = 89,478,486/4,294,967,296 while values 16–47 have a probability of ⌊2³²/48⌋/2³² = 89,478,485/4,294,967,296. The bias is:
|P(k) − 1/48| = 1/4,294,967,296 ≈ 2.33 × 10⁻¹⁰ for the 16 favored values.
This is approximately 0.000000023% bias per outcome. Over 1 billion rounds, this would produce approximately 0.23 extra hits on favored positions — statistically undetectable in practice. However, it is a deviation from the otherwise perfect bias-elimination pattern across all other games.
Recommendation: Apply rejection sampling for consistency. The performance cost is negligible.
Used by: Mines (25 positions), Keno (40 positions), Video Poker (52 cards), Cross Road (variable grid).
All four games use an identical algorithmic pattern. Extracted from the Mines module:
const positions = Array.from({ length: gridSize }, (_, i) => i);
for (let i = gridSize - 1; i > 0; i--) {
const range = i + 1;
const maxFair = MAX_UINT32 - (MAX_UINT32 % range);
let cursor = gridSize - 1 - i;
while (true) {
const hash = await generateHMAC_SHA256(
serverSeedHex,
new TextEncoder().encode(`${clientSeed}:${nonce}:${cursor}`)
);
let found = false;
for (let off = 0; off + 8 <= hash.length; off += 8) {
const value = parseInt(hash.slice(off, off + 8), 16);
if (value < maxFair) {
const j = value % range;
[positions[i], positions[j]] = [positions[j], positions[i]];
found = true;
break;
}
}
if (found) break;
cursor++;
}
}
return positions.slice(0, minesCount).sort((a, b) => a - b);Properties verified:
gridSize - 1 down to 1 (Durstenfeld variant of Fisher-Yates). This is correct — iterating upward would produce a Sattolo cycle (not a uniform permutation).i, the swap index j is chosen uniformly from [0, i] (inclusive). This produces each of the n! possible permutations with equal probability.(i + 1). This maintains uniformity even as the range shrinks during iteration.Assessment: This is a textbook-correct implementation. The combination of Fisher-Yates with per-swap HMAC entropy and rejection sampling produces a cryptographically uniform permutation.
Unlike the shuffle-based games, Blackjack generates cards independently rather than from a shuffled deck:
function generateBlackjackCard(hashHex) {
const hashBytes = hexToBytes(hashHex);
for (let i = 0; i <= hashBytes.length - 4; i += 4) {
const view = new DataView(hashBytes.buffer, hashBytes.byteOffset + i, 4);
const value = view.getUint32(0);
const max = 52 * Math.floor(0x100000000 / 52);
if (value < max) {
return CARDS[value % 52];
}
}
throw new Error('Failed to generate unbiased card value from hash');
}
async function getCard(serverSeed, clientSeed, nonce, cursor) {
const message = new TextEncoder().encode(`${clientSeed}:${nonce}:${cursor}`);
const hash = await generateHMAC_SHA256(serverSeed, message);
return generateBlackjackCard(hash);
}Note: This uses DataView.getUint32(0) (big-endian) instead of parseInt(hex, 16). The result is identical — both extract a 32-bit unsigned integer from 4 bytes — but the DataView approach is slightly more performant as it avoids string operations.
Implication: Cards are drawn with replacement from a 52-card set. This means the same card can appear multiple times in a single round. This is different from a physical shoe where cards are dealt without replacement. The platform generates up to 50 cards per round (cursors 0–49) to handle splitting and complex multi-hand scenarios.
Assessment: Correct for the stated model (infinite deck). The rejection threshold 52 × ⌊2³²/52⌋ = 52 × 82,595,524 = 4,294,967,248 produces zero bias. Rejection probability is 48/2³² ≈ 0.0000011% per chunk.
Slots use a fundamentally different architecture than other games. Instead of generating HMAC hashes per random value, slots seed a deterministic PRNG and draw all values from it.
class Pcg32 {
constructor(seed32) {
this.increment = 0x5851f42d4c957f2dn;
let seed64 = BigInt(seed32 >>> 0);
// SplitMix64-style seed expansion
seed64 = (seed64 + 0x9e3779b97f4a7c15n) & 0xFFFFFFFFFFFFFFFFn;
seed64 = ((seed64 ^ (seed64 >> 30n)) * 0xbf58476d1ce4e5b9n) & 0xFFFFFFFFFFFFFFFFn;
seed64 = ((seed64 ^ (seed64 >> 27n)) * 0x94d049bb133111ebn) & 0xFFFFFFFFFFFFFFFFn;
seed64 ^= (seed64 >> 31n);
this.state = seed64;
}
nextUint32() {
const oldState = this.state;
this.state = (oldState * 6364136223846793005n + this.increment) & 0xFFFFFFFFFFFFFFFFn;
const xorShifted = Number(((oldState >> 18n) ^ oldState) >> 27n) >>> 0;
const rot = Number(oldState >> 59n) & 31;
return ((xorShifted >>> rot) | (xorShifted << ((32 - rot) & 31))) >>> 0;
}
}Analysis:
0x5851f42d4c957f2d is hardcoded (not configurable), which is acceptable — it must be odd, and this value is taken from O'Neill's reference implementation.Assessment: Sound choice. PCG32 is a well-studied, high-quality PRNG appropriate for this use case. The determinism property (same seed → same output) is what makes verification possible.
// Step 1: HMAC → 32-bit outcome index
async function generateOutcomeIndex(serverSeed, clientSeed, nonce) {
const message = clientSeedHex + ':' + nonce;
const signature = await crypto.subtle.sign('HMAC', key, encode(message));
return new DataView(signature).getUint32(0, false); // big-endian
}
// Step 2: Mix outcome index with round/tumble indices
function deriveTumbleSeed(outcomeIndex, roundIndex, tumbleIndex) {
let x = outcomeIndex >>> 0;
x ^= Math.imul(roundIndex + 1, 0x9e3779b9) >>> 0; // golden ratio
x ^= Math.imul(tumbleIndex + 1, 0x85ebca6b) >>> 0; // MurmurHash3 constant
x ^= x >>> 16;
x = Math.imul(x, 0xc2b2ae35) >>> 0; // finalizer
x ^= x >>> 16;
return x >>> 0;
}Analysis: The tumble seed derivation uses integer mixing with well-known hash constants (golden ratio 0x9e3779b9, MurmurHash3 finalizer constants). The +1 offset on indices ensures that round 0 and tumble 0 don't zero out the XOR. The final xmxm pattern (xor-shift, multiply, xor-shift, multiply, xor-shift) is a standard avalanche technique.
Assessment: Correct. Each unique combination of (outcomeIndex, roundIndex, tumbleIndex) produces a statistically independent PCG32 seed, enabling deterministic verification of cascading/tumbling slot mechanics.
function generateGridFromPrng(prng, reels, rows, settings) {
for (let col = 0; col < reels; col++) {
const weights = getWeightsForReel(col, settings);
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
const maxFair = MAX_UINT32 - (MAX_UINT32 % totalWeight);
for (let row = 0; row < rows; row++) {
let rnd;
do {
rnd = prng.nextUint32();
} while (rnd >= maxFair);
const pick = rnd % totalWeight;
let cumulativeWeight = 0;
for (let i = 0; i < weights.length; i++) {
cumulativeWeight += weights[i];
if (pick < cumulativeWeight) {
symbolIndex = i;
break;
}
}
reel.push(symbolIndex);
}
}
}Assessment: Correct. Rejection sampling is applied even within the PCG32 output stream. The cumulative weight lookup is a standard technique for weighted discrete sampling.
The symbolWeights and reelConfigs arrays are provided by the server API, not embedded in client code. This means: the random number generation is verifiable, but the weight configuration requires trust. A player cannot independently verify that the advertised symbol weights match the actual weights used during gameplay without collecting a large statistical sample and performing chi-squared goodness-of-fit testing.
This is an inherent limitation of weighted slot systems, not a Duel-specific issue. It applies to all provably fair slot implementations.
In standard provably fair (Model A), the player contributes a client seed that influences the outcome. This requires a per-player seed exchange before each round — impractical for multiplayer games like Crash or Roulette where all participants share the same outcome.
drand (Distributed Randomness Beacon) solves this by providing a publicly verifiable, tamper-proof source of randomness from an external network. The League of Entropy consortium includes Cloudflare, Protocol Labs, EPFL, University of Chile, and others. Each drand round is:
const validateCrashResult = async (serverSeed, drandSeed) => {
const NONCE = 0;
const randomness = hexToUtf8String(drandSeed);
const message = new TextEncoder().encode(`${randomness}:${NONCE}`);
const hash = await generateHMAC_SHA256(serverSeed, message);
const value = parseInt(hash.slice(0, 8), 16);
const MAX = 2 ** 32;
const houseEdge = 0.001;
const result = (MAX / (value + 1)) * (1 - houseEdge);
return Math.max(1.0, result);
};Mathematical analysis of the crash distribution:
V be the 32-bit hash value, uniformly distributed in [0, 2³² − 1]C = (2³² / (V + 1)) × 0.999m is: P(C ≤ m) = P(V ≥ 2³² × 0.999 / m − 1) = 1 − (0.999 / m) for m ≥ 0.999P(V ≥ 2³² × 0.999 − 1) ≈ 0.1%m: EV = m × (0.999/m) − 1 = 0.999 − 1 = −0.001This confirms a flat 0.1% house edge regardless of cashout strategy. The Math.max(1.0, result) clamps the minimum multiplier, creating the "instant crash" scenario when the hash value is very large.
const NONCE = 0;
const randomness = hexToUtf8String(drandSeed);
const message = new TextEncoder().encode(`${randomness}:${NONCE}`);
const hash = await generateHMAC_SHA256(serverSeed, message);
const value = parseInt(hash.slice(0, 8), 16);
const coinflipResult = (value % 2) + 1;Assessment: 2³² mod 2 = 0. Zero bias. Result is 1 (Crown) or 2 (Swords) with exactly 50/50 probability. The serverSeed is combined with the drand beacon via HMAC, ensuring neither the casino nor the beacon alone determines the outcome.
const RANGE = 48;
const value = parseInt(hash.slice(0, 8), 16);
return (value % RANGE).toString();Assessment: As analyzed in Section 4.3, this has a negligible bias of ~2.33 × 10⁻¹⁰ per outcome. The absence of rejection sampling here is inconsistent with the rest of the codebase.
const CHOICES = ['rock', 'paper', 'scissors'];
async function generateCommitHash(choice, clientKey) {
const message = `${choice}|${clientKey}`;
const messageData = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', messageData);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0')).join('');
}
async function findChoiceForHash(clientKey, commitHash) {
for (const choice of CHOICES) {
const hash = await generateCommitHash(choice, clientKey);
if (hash === commitHash) return choice;
}
return null;
}Security analysis:
Assessment: Correct implementation of the commit-reveal paradigm. The server acts only as a mediator — it cannot influence or predict outcomes.
Server-side seed management via API. The flow:
server_seed_hashedclient_seed (default: random 16-char alphanumeric)nonceFrom FairnessNextSeed.vue:
const guestServerSeed = useLocalStorage('duel:guest_server_seed', generateRandom(64));
const guestClientSeed = useLocalStorage('duel:guest_client_seed', generateRandom(16));
const guestNonce = useLocalStorage('duel:guest_nonce', 0);
const guestPreviousServerSeed = useLocalStorage('duel:guest_previous_server_seed', '');
function hashSeedToHex(seed) {
const hasher = new jsSHA('SHA-256', 'TEXT');
hasher.update(seed);
return hasher.getHash('HEX');
}
function rotateGuestSeeds(newClientSeed) {
previousServerSeed.value = serverSeed.value;
serverSeed.value = nextServerSeed.value;
clientSeed.value = newClientSeed;
nonce.value = 0;
nextServerSeed.value = generateRandom(64);
return { success: true };
}Assessment: Guest seeds are generated and stored entirely in the browser's localStorage. The hashing uses jsSHA (a well-known JavaScript SHA library) rather than Web Crypto API — likely because crypto.subtle.digest is async and the hashing here is done synchronously for UI responsiveness. This is acceptable; jsSHA is well-tested and SHA-256 is not timing-sensitive for this use case (the input is the platform's own seed, not a secret).
Limitation: Guest seeds persist in localStorage. If the user clears browser data, all verification history is lost. There is no export/backup mechanism visible in the code.
// Games can register themselves as "active" to prevent mid-game seed rotation
function registerActiveGame(gameName, isActiveCallback) {
activeGames.set(isActiveCallback, gameName);
return () => activeGames.delete(isActiveCallback);
}
// Before rotation, check if any game is in progress
function rotateGuestSeeds(newClientSeed) {
for (const [callback, gameName] of activeGames) {
if (callback()) {
return { success: false, gameName };
}
}
// ... proceed with rotation
}Assessment: Good defensive design. Prevents seed rotation during an active game, which would invalidate in-progress verification. Games register a callback that returns true if a round is in progress.
| Game | Model | Entropy Source | Extraction | Range | Bias |
|---|---|---|---|---|---|
| Dice | A | HMAC(ss, cs:n) | Rejection sampling | 0–10000 | Zero |
| Mines | A | HMAC(ss, cs:n:c) | Fisher-Yates + RS | 25 positions | Zero |
| Keno | A | HMAC(ss, cs:n:c) | Fisher-Yates + RS | 40 positions → 10 | Zero |
| Plinko | A | HMAC(ss, cs:n:c) | Direct modulo | {0, 1} per row | Zero |
| Blackjack | A | HMAC(ss, cs:n:c) | Rejection sampling | 0–51 (52 cards) | Zero |
| Video Poker | A | HMAC(ss, cs:n:c) | Fisher-Yates + RS | 52 cards | Zero |
| Cross Road | A | HMAC(ss, cs:n:c) | Fisher-Yates + RS | Variable grid | Zero |
| PF Slots | A | HMAC → PCG32 | Weighted RS from PRNG | Per-reel weights | Zero |
| Coinflip | B | HMAC(ss, drand:0) | Direct modulo | {1, 2} | Zero |
| Crash | B | HMAC(ss, drand:0) | Inverse transform | [1.0, ∞) | Zero* |
| Castle Roulette | B | HMAC(ss, drand:0) | Direct modulo | 0–47 (48 slots) | ~2.3×10⁻¹⁰ |
| Rock Paper Scissors | C | SHA-256(choice|key) | Commit-reveal | {R, P, S} | Zero |
* Crash applies a 0.1% house edge via multiplicative factor 0.999. This is transparent, not a bias in the RNG.
Key: ss = serverSeed, cs = clientSeed, n = nonce, c = cursor, RS = rejection sampling
crypto.subtle API. No custom cryptographic implementations.const houseEdge = 0.001), not hidden in opaque arithmetic.value % 48 without rejection sampling. Bias is ~2.33 × 10⁻¹⁰ per outcome — negligible in practice but inconsistent with the otherwise rigorous bias elimination. Severity: Low.symbolWeights and reelConfigs for PF Slots come from the server API. The RNG is verifiable, but the weight configuration is a trust point. Severity: Medium (inherent to weighted slot systems).localStorage have no export mechanism. Browser data clearing permanently destroys verification capability for past bets. Severity: Low.| ID | Recommendation | Priority |
|---|---|---|
| R1 | Add rejection sampling to Castle Roulette (maxFair = MAX_UINT32 - (MAX_UINT32 % 48)) for consistency | Low |
| R2 | Implement a server seed hash chain to strengthen forward-commitment guarantees | Medium |
| R3 | Publish slot symbol weight tables in documentation or source code for independent verification | Medium |
| R4 | Document the Blackjack with-replacement model explicitly in the fairness UI | Low |
| R5 | Add seed export functionality for guest accounts | Low |
Duel.com's provably fair system demonstrates a level of cryptographic engineering that exceeds the industry standard for crypto casinos. The code is clean, correctly structured, and uses established primitives (HMAC-SHA256 via Web Crypto API, Fisher-Yates shuffle, PCG32 PRNG, drand beacon) in their canonical forms.
The identified weaknesses (W1–W5) range from negligible to architectural, and none represent exploitable vulnerabilities. The most impactful improvement would be implementing a server seed hash chain (R2) to further reduce the trust surface.
This audit covers only the client-side fairness algorithms. Server-side seed generation quality (CSPRNG usage, entropy sources) and operational security (key storage, access controls, deployment integrity) are outside the scope of this analysis and would require a separate infrastructure audit.
This audit was conducted independently by ProvablySmart Research Lab. The platform creator granted permission for source code analysis and publication. No compensation was received. No affiliate or commercial relationship exists between ProvablySmart and Duel.com. This document is provided for educational and research purposes only. It is not financial advice, not a gambling recommendation, and not an endorsement of any platform.
For Model A (solo) games: the server seed is committed via SHA-256 hash before the player bets. Changing the seed would change the hash, which the player can detect. For Model B (multiplayer) games: the randomness comes from the drand beacon, which neither the casino nor any single party controls. For Model C (PvP): the server is not involved in outcome determination — both players commit hashes independently. In all three models, manipulation would require either breaking SHA-256 preimage resistance (computationally infeasible) or compromising the drand network (requires corrupting a threshold number of independent operators).
When converting a uniform random 32-bit integer into a value within a range R that does not evenly divide 2³², the naive approach (value % R) gives slightly higher probability to values 0 through (2³² mod R) − 1. Rejection sampling discards values ≥ R × ⌊2³²/R⌋ and redraws, producing a perfectly uniform distribution. The bias from naive modulo is small (typically < 0.001%), but it is unnecessary and avoidable. Duel.com's implementation uses rejection sampling in all applicable games except Castle Roulette.
A single slot spin may require 15–30+ random values (one per cell in a 5×3 or 6×4 grid, plus multiplier selections). Generating a separate HMAC-SHA256 hash for each value would be computationally expensive and create very long verification code. PCG32 is a deterministic PRNG that produces statistically excellent output and can generate unlimited values from a single 32-bit seed. The seed is derived from HMAC-SHA256, so the cryptographic commitment chain is preserved.
A hash chain is a structure where serverSeed[n] = SHA-256(serverSeed[n+1]). The casino generates seeds in reverse order and reveals them forward. This proves the entire seed sequence was committed before any gameplay began. Without a hash chain, the casino could theoretically generate many seed candidates and selectively use unfavorable ones (though the player's client seed still prevents this if it has sufficient entropy). A hash chain adds defense-in-depth.
After rotating your seed pair: (1) obtain the revealed serverSeed, your clientSeed, and the nonce for the bet; (2) open your browser's developer console (F12); (3) in the Duel.com fairness modal, click "Copy Code" to get the game-specific verification script; (4) paste and run it in the console. The script will compute the outcome from your inputs using the same algorithm analyzed in this audit. Compare the computed result to the actual result you experienced.
No. This audit covers only the mathematical correctness of client-side fairness algorithms. It does not cover: server-side implementation fidelity (whether the server actually uses the committed seeds), operational security, financial solvency, regulatory compliance, withdrawal reliability, or any other aspect of the platform's trustworthiness. A provably fair system guarantees outcome verifiability — nothing more.