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:
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 incashback.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:
RakebackService.claim(apps/api/src/rakeback/rakeback.service.ts:140) takes a per-user, per-bucket mutex and runs in a Prisma transaction.- For each currency row with a non-zero
<bucket>Claimable, the repository: - Optionally runs double-or-nothing:
DoubleLoyaltyService.play(apps/api/src/casino/house/double-loyalty/double-loyalty.service.ts:21) — provably-fair coin flip with payoutbetAmount × 2 × DOUBLE_RAKEBACK_RTP. The env knobDOUBLE_RAKEBACK_RTPdefaults to1inEnvDto(libs/shared/src/env-config/env.dto.ts:198); local.local.envships0.99.INSTANTclaims cannot be doubled — the flag is forced tofalse(rakeback.service.ts:143). - Zeroes the corresponding
<bucket>Claimablefield. - Emits one
Transaction { type: DEPOSIT, tag: RAKEBACK }per currency to the player's wallet. - 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.}}
7. Related docs¶
vip-program.md— Where the loyalty percent comes frombet-settlement.md— WhereRakebackService.handleBetis invoked from the bet pipelinewallet-and-balance.md— How theRAKEBACK-tagged transactions land in the wallet../flows/dropbet-bet-place.md— Player-facing bet → rakeback flow../runbooks/bullmq-job-stuck.md— Recovery if rakeback accrual stalls