Skip to content

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

MemberReturnsDescription
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

CodeError classHTTP statusTrigger conditionRetryable
ENG_TOURNAMENT_NOT_FOUNDTournamentNotFoundError404Unknown tournamentId (opt-in or leaderboard)No
ENG_TOURNAMENT_NOT_ACTIVETournamentNotActiveError409optIn on a non-ACTIVE tournamentNo (until it starts)
ENG_ALREADY_OPTED_INAlreadyOptedInError409optIn when already joinedNo

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 isMe row; 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

ValueTypeDefault / rangeEffect
Schedules (startDate/endDate)per tournamentoperator-definedLifecycle windows
prizePool + distribution sharesnumber + bandsoperator-defined (shares sum to 100)Prize amounts per rank
rules.pointsPerCurrencyWagerednumberoperator-defined (e.g. 10)Score per unit wagered
rules.minBetnumberoperator-definedMinimum scoring wager
rules.eligibleGameIdsstring[]all gamesWhich games score
Tournament type and catalogper tournamentoperator-definedWhich 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