Skip to content

Rakeback

Definition. Rakeback is a percentage of the platform's gross gaming revenue (GGR) returned to the player. The percentage is set by the player's loyalty (VIP) level. Rakeback is split into four buckets — instant, daily, weekly, monthly — and is denominated in the bet's own currency. Players claim each bucket separately from the player UI; an admin can compute their total claimable in USD.

1. Math

1.1 GGR — the base of every rakeback computation

GGR is computed per-bet from the bet's wager amount and the game's RTP:

// apps/api/src/utils/ggr.utils.ts:4
const houseEdge = new Decimal('100').minus(args.rtp);   // 100 − RTP
return args.amount.mul(houseEdge).div('100');           // amount × (100 − RTP) / 100

In algebraic form:

GGR = wager × (100 − RTP) / 100

The RTP is per-game, persisted in Game.rtp (see ../data-model/) and fetched via GamesService.getGameRtp (apps/api/src/casino/games/service/games.service.ts).

1.2 Cashback — applying the loyalty percent

// apps/api/src/utils/cashback.utils.ts:12
const ggr = GgrUtils.calculate({ amount, rtp });
const cashback = ggr.mul(args.percent);                 // GGR × loyalty %
return cashback;

Note. The block that previously discounted provider games by PROVIDER_GGR_COMMISSION_PERCENT = 0.15 (apps/api/src/rakeback/const.ts:12) is commented out in cashback.utils.ts:17-26. Today, provider and house games are treated identically. The constant is still exported but unused. {{TBD: confirm with product team whether this is permanent or pending re-enable.}}

1.3 Loyalty percent table

The percent comes from UserUtils.getLoyaltyPercent (apps/api/src/user/user.utils.ts:32), keyed by VIP level name:

Loyalty level loyaltyPercent Rakeback as % of GGR
Wood 0 0%
Metal 0.25 25%
Bronze 0.275 27.5%
Silver 0.4 40%
Gold 0.5 50%
Platinum 0.6 60%
Diamond 0.7 70%
Beast 0.8 80%

Level mapping (apps/api/src/user/loyalty/const.ts:15) is by accumulated XP; see vip-program.md for the full ladder.

1.4 Bucket split

After the per-bet rakeback is computed, it is split into four buckets at fixed weights:

// apps/api/src/rakeback/const.ts:7-10
INSTANT_RAKEBACK_PERCENT = 0.1
DAILY_RAKEBACK_PERCENT   = 0.2
WEEKLY_RAKEBACK_PERCENT  = 0.3
MONTHLY_RAKEBACK_PERCENT = 0.4

Sum = 1.0 — the full rakeback is fully bucketed, no truncation. Split happens in RakebackService.splitRakeback (apps/api/src/rakeback/rakeback.service.ts:57).

1.5 End-to-end formula

rakeback_total = wager × (100 − RTP) / 100 × loyaltyPercent

instant   = rakeback_total × 0.10
daily     = rakeback_total × 0.20
weekly    = rakeback_total × 0.30
monthly   = rakeback_total × 0.40

2. Worked example

Assume: - Wager 1000 DBC on a 99% RTP house game (house edge = 1%) - Player at Gold (50% loyalty)

GGR        = 1000 × (100 − 99) / 100  = 10.000 DBC
rakeback   = 10 × 0.5                 = 5.000 DBC

instant    = 5 × 0.10 = 0.500 DBC   ← claimable immediately
daily      = 5 × 0.20 = 1.000 DBC   ← unlocked at next 00:00 UTC
weekly     = 5 × 0.30 = 1.500 DBC   ← unlocked at week boundary
monthly    = 5 × 0.40 = 2.000 DBC   ← unlocked at 1st of month

Each bucket accrues per currency (@@unique [userId, currencyId] on the rakeback row, RakebackRepository.increment at apps/api/src/rakeback/rakeback.repository.ts:48). A player wagering in BTC and USDT has two parallel rakeback rows.

3. Settlement cadence

Rakeback accrues per bet, on the BullMQ side-effect path:

sequenceDiagram
    participant Bet as BetService
    participant Q as BullMQ (BET_SETTLED_QUEUE)
    participant Proc as BetQueueProcessor
    participant Rb as RakebackService
    participant Repo as RakebackRepository
    participant DB as Postgres

    Bet->>Q: pushBet (status=SETTLED)
    Q->>Proc: processJob
    Proc->>Rb: handleBet(bet)
    Rb->>Rb: calculateRakeback(bet, game, user)
    Rb->>Rb: splitRakeback(rakeback)
    Rb->>Repo: increment(instant, daily, weekly, monthly)
    Repo->>DB: UPSERT rakeback row<br/>instantClaimable += instant<br/>dailyAccumulated  += daily<br/>weeklyAccumulated += weekly<br/>monthlyAccumulated+= monthly

Edges in plain text: - BetService.processSideEffects (apps/api/src/bet/bet.service.ts:105) pushes a settled bet onto BullMQ. - BetQueueProcessor.processJob (apps/api/src/bet/queue/bet.queue-processor.ts:106) calls RakebackService.handleBet. - RakebackService.handleBet is wrapped with @HandleBetOnce (apps/api/src/bet/utils/utils.ts:16) — a 15-minute idempotency lock keyed on bet-side-effect:RakebackService-handleBet:<betId>. A second delivery of the same bet is a no-op.

3.1 Bucket promotion crons

Buckets graduate from "accumulated" to "claimable" on a schedule (apps/api/src/rakeback/rakeback.service.ts:161-174):

Bucket Cron Action
Instant none Always claimable; written directly to instantClaimable
Daily EVERY_DAY_AT_MIDNIGHT dailyClaimable := dailyAccumulated; dailyAccumulated := 0
Weekly EVERY_WEEK Same swap on weekly_* columns
Monthly EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT Same swap on monthly_* columns

The swap is one raw SQL UPDATE per period (RakebackRepository.updateRakebackFields at rakeback.repository.ts:222) wrapped in an @IdempotencyLock with a 1-hour TTL — if the cron fires twice in the same window, the second run is a no-op. Cron times are platform UTC; see apps/api/src/rakeback/rakeback.repository.ts:115 for the same boundaries used in the player-facing nextOpenAt field.

4. Claim flow

The player calls POST /rakeback/claim with { type: INSTANT | DAILY | WEEKLY | MONTHLY, double?: boolean }. Internally:

  1. RakebackService.claim (apps/api/src/rakeback/rakeback.service.ts:140) takes a per-user, per-bucket mutex and runs in a Prisma transaction.
  2. For each currency row with a non-zero <bucket>Claimable, the repository:
  3. Optionally runs double-or-nothing: DoubleLoyaltyService.play (apps/api/src/casino/house/double-loyalty/double-loyalty.service.ts:21) — provably-fair coin flip with payout betAmount × 2 × DOUBLE_RAKEBACK_RTP. The env knob DOUBLE_RAKEBACK_RTP defaults to 1 in EnvDto (libs/shared/src/env-config/env.dto.ts:198); local .local.env ships 0.99. INSTANT claims cannot be doubled — the flag is forced to false (rakeback.service.ts:143).
  4. Zeroes the corresponding <bucket>Claimable field.
  5. Emits one Transaction { type: DEPOSIT, tag: RAKEBACK } per currency to the player's wallet.
  6. Cache key rakeback:<userId> is invalidated; the player's UI poll re-fetches.

4.1 Double-or-nothing

// apps/api/src/casino/house/double-loyalty/double-loyalty.service.ts:31
const didWin = randomValue === 1;
const payout = didWin
  ? betAmount.mul(2).mul(new Decimal(this.loyaltyRtp))   // 2× wager × DOUBLE_RAKEBACK_RTP
  : new Decimal(0);

With the production default DOUBLE_RAKEBACK_RTP = 1, double-or-nothing is mathematically fair: EV = 2 × P(win) × 1 × wager = wager (P(win) = 0.5). On staging 0.99 it gives the house a 1% edge per double attempt.

5. Edge cases

Case Behavior Source
Game RTP = 100 houseEdge = 0 → GGR = 0 → rakeback = 0 ggr.utils.ts:6
Player at Wood loyaltyPercent = '0' → rakeback = 0 user.utils.ts:35
Bet refund Refund creates a WITHDRAW transaction with tag = BET. Rakeback already credited is not clawed back. {{TBD: confirm with product team whether refund should reverse accumulated rakeback.}} bet.service.ts:637
Bet rollback Same as refund — rakeback handleBet only fires on BetStatus.SETTLED (bet.queue-processor.ts:100). Rollbacks never enter the rakeback path. bet.queue-processor.ts:100
Double-claim @HandleBetOnce (15-min idempotency) on accrual; per-(userId, type) mutex on claim rakeback.service.ts:93,134
Cron double-fire @IdempotencyLock 1-hour TTL keyed on rakeback:refresh:<period> rakeback.repository.ts:234
Provider game vs house game Same percent today. Code path forks but the provider branch is commented out. cashback.utils.ts:17-26
Currency boundary Rakeback is per-currency; switching wallets does not aggregate. RakebackRepository.increment
Multi-currency claim One claim call settles all currencies for the bucket; per-currency transactions rakeback.repository.ts:172

6. Admin overrides

Path Effect Code
GET /admin/rakeback/users/:userId/claimable-usd Returns the user's claimable in USD across all buckets and currencies admin.rakeback.controller.ts, rakeback.service.ts:180
GET /admin/rakeback (paginated findMany) Returns raw rakeback rows for any filter rakeback.service.ts:176
Adjust user XP / VIP level Indirectly changes future rakeback percent user.repository.ts:432 (incrementUserExp)
Force a cron run Manually invoke RakebackRepository.refresh(period) via debug endpoint apps/api/src/debug/

There is no admin-side credit/debit of accrued rakeback — adjustments must happen via Transaction with tag = RAKEBACK (accounting.service.ts). {{TBD: product team to confirm whether direct override is on the roadmap.}}