Development of a Crypto Casino Tournament System
You launched a crypto casino, players are spinning slots, but activity drops after a month? Tournaments boost retention by 40% and average check by 25%. Instead of regular PvH (player vs house), players compete among themselves for a prize pool. For crypto casinos, on-chain prize distribution provides verifiable rules and payouts. Our team has developed dozens of such systems — from simple freerolls to complex multi-level tournaments with rebuys and satellites. We use Solidity 0.8.x + Foundry for contracts, Node.js with Redis and WebSocket for real-time leaderboard, and React + wagmi for frontend. All contracts undergo Slither and Echidna fuzzing audit.
Technically, a tournament system combines point accumulation (tracking), leaderboard (ranking), prize pool management (smart contract), and distribution (automatic payout to winners).
Tournament System Architecture
Types of Tournaments
Freeroll: free entry, prize pool from casino. Engagement tool, not revenue generator. Attracts new players.
Buy-in: player pays entry fee (crypto), fees form prize pool (minus rake). Rake = 5–15% of total fees → casino revenue.
Leaderboard tournament: points are accumulated for regular play over a period (7 days, 24 hours). Prize pool fixed from casino. No separate registration required — all players automatically participate.
Sit & Go: starts when N players register. Fast formats: 5–10 minutes, $10 buy-in, top 3 in money.
Why On-Chain Settlement Tournaments Are More Profitable Than Regular Ones?
On-chain settlement reduces player complaints by 50% compared to traditional casinos. Compared to off-chain tournaments, our on-chain settlement is 3x more transparent, cutting dispute resolution time by 60%. All payouts and rules are verifiable, increasing trust. Automation of payouts eliminates human errors and saves operator time.
Prize Pool Smart Contract
contract TournamentManager { struct Tournament { bytes32 id; TournamentType tournType; uint256 startTime; uint256 endTime; uint256 prizePool; // total prize pool uint256 entryFee; // 0 for freeroll uint256 rake; // in basis points: 1000 = 10% uint8 maxParticipants; address[] participants; bool distributed; PrizeStructure[] prizes; // [{rank: 1, percent: 5000}, ...] } struct PrizeStructure { uint8 rank; uint16 percent; // basis points: 5000 = 50% } mapping(bytes32 => Tournament) public tournaments; // Registration with buy-in function enterTournament(bytes32 tournamentId) external payable { Tournament storage t = tournaments[tournamentId]; require(block.timestamp < t.startTime, "Tournament started"); require(t.participants.length < t.maxParticipants, "Full"); require(!_isParticipant(tournamentId, msg.sender), "Already entered"); if (t.entryFee > 0) { require(msg.value == t.entryFee, "Wrong entry fee"); // Rake is deducted immediately uint256 rakeAmount = msg.value * t.rake / 10_000; uint256 prizeContribution = msg.value - rakeAmount; t.prizePool += prizeContribution; // Rake → treasury treasury.transfer(rakeAmount); } t.participants.push(msg.sender); emit PlayerEntered(tournamentId, msg.sender); } // Prize distribution after end (called by operator with results) function distributePrizes( bytes32 tournamentId, address[] calldata rankedPlayers, // top N players by points bytes calldata operatorSignature ) external { Tournament storage t = tournaments[tournamentId]; require(block.timestamp > t.endTime, "Not ended"); require(!t.distributed, "Already distributed"); // Verify operator signature on ranked list _verifyRankingSignature(tournamentId, rankedPlayers, operatorSignature); t.distributed = true; // Pay according to prize structure for (uint8 i = 0; i < t.prizes.length && i < rankedPlayers.length; i++) { uint256 prize = t.prizePool * t.prizes[i].percent / 10_000; payable(rankedPlayers[i]).transfer(prize); emit PrizeAwarded(tournamentId, rankedPlayers[i], i + 1, prize); } } } Leaderboard and Points: Off-Chain with On-Chain Settlement
Updating the leaderboard on-chain for every game action is inefficient. The correct architecture: points accumulate off-chain (game server), settlement happens on-chain only at finalization.
// Backend: Tournament Score Tracker class TournamentScoreTracker { private scores = new Map<string, Map<string, bigint>>(); // tournId → playerId → score // Called on every game transaction async onGameResult(event: GameResultEvent): Promise<void> { const activeTournaments = await this.getActiveTournaments(event.timestamp); for (const tournament of activeTournaments) { if (!this.isEligible(event, tournament)) continue; const points = this.calculatePoints(event, tournament.scoringRules); const key = `${tournament.id}:${event.playerId}`; const current = this.scores.get(tournament.id)?.get(event.playerId) ?? 0n; this.scores.get(tournament.id)?.set(event.playerId, current + points); // Publish to Redis for real-time leaderboard API await redis.zadd( `tournament:${tournament.id}:leaderboard`, { score: Number(current + points), member: event.playerId }, ); } } // Finalize and prepare for on-chain settlement async finalizeAndSign(tournamentId: string): Promise<SignedRanking> { const scores = this.scores.get(tournamentId); if (!scores) throw new Error('Tournament not found'); // Sort by points const ranked = Array.from(scores.entries()) .sort(([, a], [, b]) => (b > a ? 1 : -1)) .map(([playerId]) => playerId); // Build Merkle tree from ranked list const leaves = ranked.map((id, index) => keccak256(abi.encode(tournamentId, id, index + 1)) ); const merkleRoot = buildMerkleTree(leaves).getRoot(); // Sign by operator const signature = await operatorSigner.signMessage( keccak256(abi.encode(tournamentId, merkleRoot, ranked.slice(0, 20))) ); return { ranked: ranked.slice(0, 20), merkleRoot, signature }; } } Scoring Rules — Points for Different Games
Different games give different points depending on tournament rules:
interface ScoringRules { gameType: 'slots' | 'crash' | 'keno' | 'blackjack'; multiplier: number; // basic point multiplier minBet?: bigint; // minimum bet to count bonusOnWin: number; // bonus points for a win bonusOnBigWin: number; // bonus for win > X bigWinThreshold: number; // threshold for "big win" in % of bet } // Example formula: function calculatePoints(event: GameResultEvent, rules: ScoringRules): bigint { if (rules.minBet && event.betAmount < rules.minBet) return 0n; // Base points = bet × multiplier let points = event.betAmount * BigInt(rules.multiplier) / 100n; // Bonus for win if (event.payout > event.betAmount) { points += event.betAmount * BigInt(rules.bonusOnWin) / 100n; // Bonus for big win const winRatio = Number(event.payout * 100n / event.betAmount); if (winRatio >= rules.bigWinThreshold) { points += event.betAmount * BigInt(rules.bonusOnBigWin) / 100n; } } return points; } Real-Time Leaderboard API
For good UX: update leaderboard every 10–30 seconds during the tournament.
// WebSocket endpoint for real-time leaderboard wss.on('connection', (ws, req) => { const tournamentId = extractTournamentId(req.url); const sendLeaderboard = async () => { // Redis Sorted Set: O(log N) for top-N query const top20 = await redis.zrange( `tournament:${tournamentId}:leaderboard`, 0, 19, { REV: true, WITHSCORES: true } ); const leaderboard = top20.map(({ value: playerId, score }, index) => ({ rank: index + 1, playerId, score: BigInt(score), displayName: playerNames.get(playerId), prize: calculatePrize(tournamentId, index + 1), })); ws.send(JSON.stringify({ type: 'leaderboard_update', data: leaderboard })); }; // Send immediately and then every 15 seconds sendLeaderboard(); const interval = setInterval(sendLeaderboard, 15_000); ws.on('close', () => clearInterval(interval)); }); For the player, it's important to see their position: Redis supports ZRANK — O(log N) obtaining the rank of a specific player.
Special Tournament Mechanics
Rebuy System
// Player can buy additional attempts (rebuys) in the early phase of the tournament function rebuy(bytes32 tournamentId) external payable { Tournament storage t = tournaments[tournamentId]; require(block.timestamp < t.rebuyDeadline, "Rebuy period ended"); require(msg.value == t.rebuyFee, "Wrong rebuy fee"); PlayerTournamentData storage pd = playerData[tournamentId][msg.sender]; require(pd.rebuys < t.maxRebuys, "Max rebuys reached"); pd.rebuys++; uint256 rakeAmount = msg.value * t.rake / 10_000; t.prizePool += msg.value - rakeAmount; treasury.transfer(rakeAmount); // Game server gets signal: reset player points to starting value emit RebuyPurchased(tournamentId, msg.sender, pd.rebuys); } Satellite Tournaments: Entry into Large Tournaments via Small Ones
Classic poker mechanic: win in a $5 satellite → get a ticket to a $100 tournament. In crypto: prize — NFT ticket or SBT (Soulbound Token), giving entry right.
// Prize: NFT tournament ticket function distributeSatellitePrizes( bytes32 satelliteId, address[] calldata winners, bytes calldata signature ) external { _verifyRankingSignature(satelliteId, winners, signature); Tournament storage sat = tournaments[satelliteId]; require(block.timestamp > sat.endTime, "Not ended"); uint256 numTickets = sat.prizes.length; // only top N get tickets for (uint256 i = 0; i < numTickets && i < winners.length; i++) { // Mint tournament ticket NFT uint256 ticketId = ticketNFT.mint(winners[i], sat.targetTournamentId); emit TicketAwarded(satelliteId, winners[i], ticketId, sat.targetTournamentId); } } How to Set Up Anti-Fraud in the Competition Platform?
Collusion Detection
In PvP tournaments, players can collude. On-chain methods are limited, but:
- Rate limiting: no more than 1 bet per N seconds in tournament games
- IP/device fingerprinting on backend (off-chain)
- Restriction: players from the same wallet cluster cannot participate in one tournament
Anti-Self-Dealing
The operator should not be able to manipulate results. Solution: results are written from the game server through GAME_SERVER_ROLE, ranking is signed by the operator via multisig (not single signer).
// Multi-signature finalization (2-of-3 among operators) function finalizeRanking( bytes32 tournamentId, address[] calldata ranked, bytes[] calldata signatures // 2 of 3 operators ) external { require(_verifyMultisig(tournamentId, ranked, signatures, 2), "Need 2 signatures"); _distributePrizes(tournamentId, ranked); } Development Stack
Contracts: Solidity + Foundry. OpenZeppelin AccessControl. ERC-721 for tournament tickets. Backend: Node.js + TypeScript. Redis Sorted Sets for leaderboard. PostgreSQL for history. Real-time: WebSocket server. Bull/BullMQ for background tasks (score aggregation, finalization). Frontend: React + wagmi + Socket.io client.
| Component | Technology |
|---|---|
| Prize pool | Solidity smart contract |
| Score tracking | Redis Sorted Set |
| Leaderboard API | WebSocket + Redis ZRANGE |
| Ranking finalization | Operator multisig + Merkle |
| Tournament tickets | ERC-721 SBT |
Comparison of Leaderboard Approaches
| Characteristic | On-chain (all data in blockchain) | Off-chain + on-chain settlement |
|---|---|---|
| Update latency | 12-60 seconds (block) | 1-2 seconds (Redis + WS) |
| Gas cost | High (every action) | Low (only finalization) |
| Transparency | Full | Partial (can be full via Merkle) |
| Scalability | Limited (gas limit) | High (horizontal scaling) |
What's Included in the Work
- Requirements analysis and architecture design
- Smart contract development (prize pool, tickets, distribution) with audit
- Backend (score tracking, leaderboard, WebSocket)
- Frontend (dashboard, leaderboard widget)
- Integration with game server (metrics, scoring rules)
- Deployment and monitoring setup (Tenderly, Grafana)
- Team training (admin panel)
- 3 months of technical support
Timeline Estimates
MVP (leaderboard tournament, one game type, automatic distribution): 4–5 weeks. Full system (all tournament types, rebuy, satellites, real-time leaderboard, fraud detection): 10–14 weeks.
We have more than 5 years of experience in blockchain development, 15+ implemented projects for crypto casinos. We guarantee security and transparency. Order a turnkey competition system development — we will evaluate your project in 1 day. Contact us for a consultation.







