In traditional online casinos, random number generators (RNG) remain a black box—players have to trust the operator blindly. This undermines trust and raises doubts about payout fairness. Provably fair solves this: every game outcome is published on the blockchain with cryptographic proof. Players can independently reproduce the calculation and verify that a dice roll, spin, or crash result wasn't rigged. We have been implementing provably fair since the technology emerged and have delivered over 30 projects in on-chain gambling. Our stack: smart contracts on Solidity 0.8.x, off-chain engine on Node.js, and verification UI on React. Assess your project—contact us to discuss.
How Provably Fair Works in Practice
There are two main approaches: on-chain VRF and off-chain commit-reveal. We choose based on load, gas cost, and required trust level.
Chainlink VRF — On-Chain Verifiable Randomness
VRF generates a random number together with a cryptographic proof. The contract requests randomness, the oracle returns a number and proof, the contract verifies the proof and accepts the number. The result is published in an event—anyone can verify: requestId → blockHash → randomness. In one project we used Chainlink VRF with subscription ID, reducing gas costs by 30% compared to pay-as-you-go.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
contract ProvablyFairCasino is VRFConsumerBaseV2Plus {
struct BetRecord {
address player;
uint256 amount;
uint8 gameId;
bytes betParams;
uint256 vrfRequestId;
uint256 randomResult;
bool settled;
uint256 payout;
}
mapping(uint256 => BetRecord) public betRecords;
event BetPlaced(
uint256 indexed requestId,
address indexed player,
uint8 gameId,
uint256 amount,
bytes betParams
);
event BetSettled(
uint256 indexed requestId,
uint256 randomness,
uint256 payout,
bool isWin
);
function placeBet(uint8 gameId, bytes calldata betParams)
external payable returns (uint256 requestId)
{
require(msg.value >= MIN_BET[gameId], "Below minimum");
require(msg.value <= MAX_BET[gameId], "Above maximum");
require(msg.value <= getMaxBet(), "Exceeds bankroll limit");
requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: KEY_HASH,
subId: SUBSCRIPTION_ID,
requestConfirmations: 3,
callbackGasLimit: 200_000,
numWords: 1,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
)
})
);
betRecords[requestId] = BetRecord({
player: msg.sender,
amount: msg.value,
gameId: gameId,
betParams: betParams,
vrfRequestId: requestId,
randomResult: 0,
settled: false,
payout: 0
});
emit BetPlaced(requestId, msg.sender, gameId, msg.value, betParams);
}
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
BetRecord storage bet = betRecords[requestId];
require(!bet.settled, "Already settled");
bet.randomResult = randomWords[0];
bet.settled = true;
uint256 payout = _resolveGame(bet.gameId, bet.amount, bet.betParams, randomWords[0]);
bet.payout = payout;
bool isWin = payout > 0;
if (isWin) {
payable(bet.player).transfer(payout);
}
emit BetSettled(requestId, randomWords[0], payout, isWin);
}
}
Commit-Reveal for Off-Chain Casinos
The classic scheme: the casino publishes hash(server_seed) before the game, the player adds client_seed, after the game the casino reveals server_seed. Result = HMAC-SHA256(server_seed, client_seed + nonce). Anyone can reproduce. Chainlink VRF is faster for verification time, but commit-reveal saves gas.
class ProvablyFairEngine {
async createSession(userId: string): Promise<Session> {
const serverSeed = crypto.randomBytes(32).toString("hex");
const serverSeedHash = crypto
.createHash("sha256")
.update(serverSeed)
.digest("hex");
const session = await db.createSession({
userId,
serverSeed: this.encrypt(serverSeed),
serverSeedHash,
nonce: 0,
createdAt: new Date(),
});
return { sessionId: session.id, serverSeedHash };
}
async resolve(
sessionId: string,
clientSeed: string,
gameType: string,
betAmount: number
): Promise<GameResult> {
const session = await db.getSession(sessionId);
const serverSeed = this.decrypt(session.serverSeed);
const nonce = ++session.nonce;
const combinedSeed = `${serverSeed}:${clientSeed}:${nonce}`;
const hashHex = crypto
.createHmac("sha256", serverSeed)
.update(`${clientSeed}-${nonce}`)
.digest("hex");
const rawResult = parseInt(hashHex.slice(0, 8), 16);
const gameResult = this.applyGameLogic(gameType, rawResult);
await db.updateSession(sessionId, { nonce });
await db.saveGameRecord({
sessionId,
nonce,
clientSeed,
serverSeedHash: session.serverSeedHash,
gameType,
result: gameResult.outcome,
betAmount,
payout: gameResult.payout,
});
return gameResult;
}
async rotateSession(sessionId: string): Promise<void> {
const session = await db.getSession(sessionId);
const serverSeed = this.decrypt(session.serverSeed);
await db.revealServerSeed(sessionId, serverSeed);
await this.createSession(session.userId);
}
}
Player-Side Verification
Every player can verify the result using a public verifier. We embed a React UI that, given serverSeedHash and the revealed serverSeed, reproduces the calculation and compares it with the result. Metrics supported: totalBetsCount, totalWagered, actualRTP. Bankroll transparency provides an additional level of trust. The verification UI allows players to export history for their own audit.
What's Included in Turnkey Development
| Component | Details |
|---|---|
| Smart contracts | Solidity 0.8.x, Chainlink VRF v2.5, ERC-20 payout |
| Off-chain engine | Node.js + TypeScript, PostgreSQL, AES-256-GCM |
| Verification UI | React component for result verification |
| Audit | Internal audit + gas optimization recommendations |
| Documentation | API, data schema, player instructions |
| Deployment | VRF subscription setup, contract deployment |
| Team training | 2-3 sessions on system maintenance |
We also deliver source code, test scripts, and a dashboard for RTP monitoring. We guarantee support for 6 months after launch.
Why Choose VRF Over Commit-Reveal?
Comparison of approaches:
| Criteria | Chainlink VRF | Commit-reveal |
|---|---|---|
| Verification | On-chain, trustless | Off-chain, requires trust in seed reveal |
| Gas cost | Medium (call + callback) | Low (only hashes) |
| Speed | 2-3 blocks | Instant |
| Transparency | Full | High, but depends on server honesty |
| Auditability | Easy (events) | Medium (logs) |
For projects with strict transparency requirements — VRF. For fast games with low fees — commit-reveal.
Development Timeline
- Basic games with VRF (Dice, Coinflip, Crash): 4-6 weeks
- Commit-reveal off-chain engine: 3-4 weeks
- Verification UI: 2-3 weeks
- Scaling to 5-8 games: +4-8 weeks
- Security audit: 4-6 weeks (recommended)
- Total: 3-5 months turnkey
Timelines may vary depending on game logic complexity and payment system integration.
Common Mistakes in Provably Fair Implementation
- Weak server_seed entropy: generate with
crypto.randomBytes(32), never withMath.random. - Unpublished seed hash before the game: otherwise players cannot verify the seed wasn't changed post factum.
- Lack of verification UI: without it, provably fair is an empty marketing phrase.
- Insufficient bankroll protection: use a pool with bet limits and public RTP metrics.
- Ignoring gas optimization: every bet is a transaction; optimize variables and use packed storage.
Contact us to discuss your project—we'll assess the architecture and propose the optimal solution. The concept of provably fair is described on Wikipedia. More about Chainlink VRF in the Chainlink VRF documentation.







