VIP program (loyalty ladder)¶
Definition. A wager-driven XP ladder. Every settled bet increases a player's
User.expbyusdAmount × EXP_MULTIPLIER; once XP crosses the next threshold, the player'svipLevelis bumped, an optional one-time DBC bonus is auto-credited, and a notification is emitted. The level is the single input that drives the rakeback percent (seerakeback.md) and is the gating signal for the manual VIP-application program (perks negotiated 1:1 with the host).
1. Levels — the ladder¶
There are eight loyalty bands with a total of 31 sub-levels, generated from the configs at apps/api/src/user/loyalty/const.ts:15 (LOYALTY_LEVEL_CONFIGS). Five of the eight bands are split into five sub-levels (Metal 1-5, Bronze 1-5, …); Wood and Beast each have one threshold.
| Band | Sub-levels | XP thresholds (USD-equivalent wagered) | Level-up bonuses (DBC) |
|---|---|---|---|
| Wood | 1 | 0 |
0 |
| Metal | 5 | 100, 200, 300, 400, 500 |
0.4, 0, 1.2, 0, 2 |
| Bronze | 5 | 1k, 2k, 3k, 4k, 5k |
2, 0, 6, 0, 10 |
| Silver | 5 | 10k, 20k, 30k, 40k, 50k |
15, 0, 45, 0, 75 |
| Gold | 5 | 100k, 200k, 300k, 400k, 500k |
90, 0, 270, 0, 450 |
| Platinum | 5 | 750k, 1M, 1.25M, 1.5M, 1.75M |
525, 0, 875, 0, 1225 |
| Diamond | 5 | 2.5M, 3M, 4M, 5M, 7.5M |
2000, 0, 3200, 0, 6000 |
| Beast | 1 | 10M |
0 (terminal) |
Generation logic at apps/api/src/user/loyalty/const.ts:138 produces the linear LOYALTY_LEVELS array (id 1..31) used everywhere as the canonical level reference.
2. XP — what counts¶
Bet settlement increments XP:
// apps/api/src/user/user.service.ts:1065
const exp = bet.usdAmount.mul(this.configService.get('EXP_MULTIPLIER'));
await this.userRepository.incrementUserExp(bet.userId, exp, delayMs);
bet.usdAmountis the bet's wager converted to USD viaExchangeRatesServiceat the moment the bet was placed.EXP_MULTIPLIERdefaults to1(libs/shared/src/env-config/env.dto.ts:198); test env ships1. Set> 1for promo windows ("double XP weekend"). {{TBD: confirm with product team the canonical production value.}}- Refunds and rollbacks do not deduct XP. Only
BetStatus.SETTLEDentersUserService.handleBetfrom the BullMQ side-effect path (apps/api/src/bet/queue/bet.queue-processor.ts:100,105).
3. Level-up — what happens when XP crosses a threshold¶
Bookkeeping is in UserRepository.incrementUserExp (apps/api/src/user/user.repository.ts:432):
- Read the user's current
expandvipLevel. - Increment
expatomically. - Look up the new band via
UserUtils.getLoyaltyLevel(newExp)(user.utils.ts:18). - If the new band's id differs from the stored
vipLevel, persist the new id. - Return both old and new levels.
Then UserService.handleBet (user.service.ts:1061) calls handleNewLoyaltyLevelUp for every level skipped in a single bet — important for whales who can leap multiple thresholds with one massive wager:
// apps/api/src/user/user.service.ts:1003
for (let index = currentVipLevel.id + 1; index <= newVipLevel.id; index++) {
const loyaltyLevel = UserUtils.getLoyaltyLevelById(index);
...
if (loyaltyLevel.levelUpBonusAmount.lte(0)) {
// notification only — no bonus
} else {
// creditOrFindTransaction(LOYALTY_BONUS, currencyId=DBC, amount=levelUpBonusAmount)
// notification with amount
}
}
The bonus transaction uses an idempotent id user-<userId>-level-up-<levelName> (user.service.ts:1026), so re-running this for the same user/level is a no-op.
4. Worked example — bet that crosses two thresholds¶
Player at Bronze 5 (XP = 4900, level id 11). They place a 1500 USD wager (RTP irrelevant for XP). EXP_MULTIPLIER = 1.
Wait — newExp = 6400 is below Silver's first threshold 10000. Player is now in Bronze beyond the band's last threshold; getLoyaltyLevel returns the same level (Bronze 5). XP > 5000 stays Bronze 5 until newExp >= 10000.
Let's bump the wager to 7000 USD instead:
The for loop at user.service.ts:1003 runs once for index = 12:
- Silver 1.levelUpBonusAmount = 15 DBC
- accountingService.createOrFindTransaction({ id: 'user-<userId>-level-up-Silver-1', amount: 15, type: DEPOSIT, tag: LOYALTY_BONUS, currencyId: DBC })
- Notification LevelUpWithBonus emitted with levelName='Silver 1', amount='15'.
Now consider a 25,000 USD wager from the same starting point:
The loop runs three times: id 12 (Silver 1, +15 DBC), id 13 (Silver 2, +0 DBC, notify only), id 14 (Silver 3, +45 DBC). Net DBC credit: 60 DBC. Three separate notifications emitted, three idempotent transactions written.
5. VIP application — the human-curated tier above the ladder¶
The auto-ladder above maxes out at Beast. The platform also runs a manually-curated "VIP program" that players opt into via a form. This is distinct from vipLevel and lives at:
apps/api/src/vip-program/players/vip-program.service.ts:11(VipApplicationService) — player-side create/update/upload-photo endpointsapps/api/src/vip-program/admin/vip-program-admin.service.ts— admin review, accept/rejectapps/api/src/vip-program/repository/vip-program.repository.ts— Prisma persistence onVipProgramApplication
Photos go to S3 under users/vip-program/ (apps/api/src/vip-program/repository/utils/vip-program.const.utils.ts:1), capped at maxFiles = 4. Per-player perks (custom rakeback boost, dedicated host, withdrawal limit raise, etc.) are negotiated 1:1 and applied via admin overrides on the user record (customMinAffiliateLevel, manual rakeback grants, etc.). {{TBD: confirm with product team the canonical perk matrix.}}
6. Rakeback × VIP — the integration¶
RakebackService.calculateRakeback reads the user's vipLevel and resolves the loyalty percent:
// apps/api/src/rakeback/rakeback.service.ts:71
const loyaltyPercent = UserUtils.getLoyaltyPercent(user.vipLevel);
The full mapping is in rakeback.md §1.3. A player crossing into Silver doubles their rakeback (Bronze 27.5% → Silver 40%), into Gold another +10pp, etc.
flowchart LR
Bet[Settled bet]
BQ[BetQueueProcessor]
UserH[UserService.handleBet]
Inc[incrementUserExp]
Cmp{newVipLevel<br/>> oldVipLevel?}
Loop[for each skipped level:<br/>credit bonus + notify]
Rb[RakebackService.handleBet<br/>uses NEW vipLevel]
Bet --> BQ
BQ --> UserH
UserH --> Inc
Inc --> Cmp
Cmp -- yes --> Loop
Cmp -- no --> Rb
Loop --> Rb
Flow notes:
- UserService.handleBet runs before RakebackService.handleBet in BetQueueProcessor.processJob (bet.queue-processor.ts:105-106), so a level-up takes effect for the same bet that triggered it.
- Both calls are wrapped with @HandleBetOnce — re-delivery is a no-op.
7. Edge cases¶
| Case | Behavior | Source |
|---|---|---|
| Bet status != SETTLED | XP not incremented; no level-up | bet.queue-processor.ts:100 |
usdAmount = 0 (free spin) |
addedExp = 0; no level change |
user.service.ts:1065 |
Negative EXP_MULTIPLIER |
Logically possible but incrementUserExp does not guard; would decrease XP. Treat as misconfig. |
env.dto.ts:198 |
| XP overflow past last band | getLoyaltyLevel returns Beast (id 31) and stays there |
user.utils.ts:25 |
| Beast → no further bonus | Beast.bonuses = [], levelUpBonusAmount = 0 for the single threshold |
loyalty/const.ts:131-135 |
| Multiple skipped levels in one bet | Loop credits each non-zero bonus individually with distinct idempotent tx ids | user.service.ts:1003 |
| Level-up bonus retry | createOrFindTransaction({ returnTransactionIfExists: true }) — second call returns existing |
user.service.ts:1024 |
| Refund of an XP-granting bet | XP is not reversed. {{TBD: product team to confirm.}} | n/a |
8. Admin overrides¶
| Capability | Path | Code |
|---|---|---|
Set arbitrary vipLevel for a user |
Admin user-edit form | apps/api/src/user/user.repository.ts admin update path |
Set arbitrary exp |
Same | same |
| Custom min affiliate level (separate but adjacent) | User.customMinAffiliateLevel |
apps/api/src/affiliate/affiliate.utils.ts:17 |
| Approve/reject VIP application | VipProgramAdminService |
apps/api/src/vip-program/admin/ |
| Trigger LOYALTY_BONUS manually | Transaction { tag: LOYALTY_BONUS } via accounting admin |
accounting.service.ts |
9. Related docs¶
rakeback.md— Where the level's loyalty percent is consumedbet-settlement.md— Where XP is accrued from settled betsaffiliate-program.md—customMinAffiliateLevelis a separate but parallel admin override../flows/dropbet-bet-place.md— Bet flow that triggers level-up../data-model/—User.exp,User.vipLevel,VipProgramApplicationschema