Appearance
Tournament Engine
The Tournament Engine runs competitive events where players earn points from real-money wagers and climb a live leaderboard for a share of a prize pool. When a tournament completes, prizes settle automatically into the winners' wallets.
Public surface
| Member | Returns | Description |
|---|---|---|
getTournaments() | Promise<Tournament[]> | All tournaments, with this player's opt-in state |
optIn(tournamentId) | Promise<void> | Join an ACTIVE tournament |
getLeaderboard(tournamentId, limit?) | Promise<LeaderboardEntry[]> | Ranked standings (default limit 10, max 50) |
getTournaments() returns the complete catalog; deterministic ordering: sorted by startDate descending, then id, identical across reads.
Typed structures
typescript
type TournamentStatus = 'UPCOMING' | 'ACTIVE' | 'COMPLETED'; // derived from dates; never seeded
type TournamentType = 'SLOTS' | 'LIVE_CASINO' | 'TABLE_GAMES' | 'ALL';
interface TournamentScoringRules {
pointsPerCurrencyWagered: number; // operator-configurable; score per 1 unit wagered
minBet: number; // wagers below this do not score
eligibleGameIds?: string[]; // undefined = all games score
}
/** Closed discriminated union; starts with CASH; non-cash variants are future-ready. */
type PrizeStructure = {
kind: 'CASH';
prizePool: number; // total pool, in the tournament's currency
distribution: Array<{ fromRank: number; toRank: number; sharePercent: number }>;
// contiguous rank bands covering ranks 1..N; sharePercent values sum to 100;
// each winner's prize = prizePool × their band's sharePercent ÷ (band size),
// rounded down to the currency's minor unit
};
interface Tournament {
id: string;
name: string;
status: TournamentStatus;
type: TournamentType;
startDate: string; // ISO-8601; status derives from these dates
endDate: string; // ISO-8601
currency: string; // the pool's currency (prizeStructure.prizePool)
rules: TournamentScoringRules;
prizeStructure: PrizeStructure;
isOptedIn: boolean; // has THIS player joined
}
interface LeaderboardEntry {
rank: number;
playerId: string;
username: string;
score: number;
prize?: number; // present when the player's rank earns a prize
isMe: boolean;
}Lifecycle
Status is always derived from the dates; never stored, never seeded:
text
GIVEN a tournament with startDate S and endDate E
WHEN the current time is before S / between S and E / after E
THEN its status is UPCOMING / ACTIVE / COMPLETED respectively.
GIVEN an ACTIVE tournament and a player who has NOT opted in
WHEN the player wagers real money on an eligible game
THEN their score does not advance; only opted-in players score.
GIVEN a player opted into an ACTIVE tournament denominated in currency C
WHEN they place a real-money wager of amount A ≥ rules.minBet in currency C
on an eligible game
THEN their score increases by A × rules.pointsPerCurrencyWagered.
GIVEN an ACTIVE tournament whose endDate passes
WHEN final scores are fixed
THEN the status flips to COMPLETED and prize settlement runs exactly once
(see [Tournament prize settlement](/wallet/transactions#tournament-prize-settlement)).Error contract
| Code | Error class | HTTP status | Trigger condition | Retryable |
|---|---|---|---|---|
ENG_TOURNAMENT_NOT_FOUND | TournamentNotFoundError | 404 | Unknown tournamentId (opt-in or leaderboard) | No |
ENG_TOURNAMENT_NOT_ACTIVE | TournamentNotActiveError | 409 | optIn on a non-ACTIVE tournament | No (until it starts) |
ENG_ALREADY_OPTED_IN | AlreadyOptedInError | 409 | optIn when already joined | No |
Common errors are defined once in the Error Reference.
Leaderboard
Deterministic ordering: score descending, playerId ascending as the tiebreaker, identical across reads, so pagination never skips or duplicates a row. The current player is always present (flagged isMe), even when ranked below the requested window (the last slot is theirs).
Deliberate exception to standard pagination: leaderboards return a top-N window plus a guaranteed
isMerow; full offset pagination is not offered, because the interesting data is the top of the board and the player's own standing;limit(default 10, max 50) sizes the top-N window.
Prizes are computed from the tournament's typed prizeStructure.distribution; the SDK renders them; settlement is the platform's.
Events
Fired on the engagement event channel (see Engagement Overview):
typescript
interface TournamentCompletedEvent { eventId: string; tournamentId: string; }
interface PrizeSettledEvent {
eventId: string;
tournamentId: string;
playerId: string;
prize: number; // in the tournament's currency
currency: string;
}Operator-configurable values
| Value | Type | Default / range | Effect |
|---|---|---|---|
Schedules (startDate/endDate) | per tournament | operator-defined | Lifecycle windows |
prizePool + distribution shares | number + bands | operator-defined (shares sum to 100) | Prize amounts per rank |
rules.pointsPerCurrencyWagered | number | operator-defined (e.g. 10) | Score per unit wagered |
rules.minBet | number | operator-defined | Minimum scoring wager |
rules.eligibleGameIds | string[] | all games | Which games score |
Tournament type and catalog | per tournament | operator-defined | Which tournaments run |
Status derivation, scoring ownership (backend), settlement, and the leaderboard ordering are platform-fixed.
Sandbox
Seeds cover every lifecycle state, including a tournament starting in seconds, so the UPCOMING→ACTIVE transition is demonstrable without waiting. The wagering feed drives scores:
javascript
await sdk.engagement.sandbox.simulateWager(50, 'USD', 'game_sweet_bonanza');
const board = await sdk.engagement.tournaments.getLeaderboard('tour_active_1');
board.find(e => e.isMe).score; // grew by 50 × pointsPerCurrencyWagered