Skip to content

Affiliate program

Definition. A flat single-level revenue-share program. Every affiliate has up to 3 referral codes; new signups attach to a code via User.affiliateCodeId. On every settled bet by a referral, the platform credits the affiliate with a percentage of the GGR generated. Tier (commission %) is determined by the affiliate's referrals' lifetime USD wager. Earnings accrue per-currency and are claimed by the affiliate on demand — gated by an "active referrals in the last 14 days" check.

1. Tier ladder

// apps/api/src/affiliate/const.ts:12
AFFILIATE_LEVELS = [
  { id: 1, name: 'Tier 1', commission: '0.1',  minWageredUsdAmount: 0,           minReferralCountForClaim: 0   },
  { id: 2, name: 'Tier 2', commission: '0.15', minWageredUsdAmount: 25_000,      minReferralCountForClaim: 3   },
  { id: 3, name: 'Tier 3', commission: '0.2',  minWageredUsdAmount: 100_000,     minReferralCountForClaim: 10  },
  { id: 4, name: 'Tier 4', commission: '0.25', minWageredUsdAmount: 250_000,     minReferralCountForClaim: 25  },
  { id: 5, name: 'Tier 5', commission: '0.3',  minWageredUsdAmount: 1_000_000,   minReferralCountForClaim: 100 },
];
Tier Commission Minimum lifetime wager (referrals, USD) Active referrals to claim
Tier 1 10% $0 0
Tier 2 15% $25,000 3
Tier 3 20% $100,000 10
Tier 4 25% $250,000 25
Tier 5 30% $1,000,000 100

Tier resolution at findAffiliateLevel (affiliate/const.ts:65):

const level = AFFILIATE_LEVELS.findLast(
  level => wageredUsdAmount.gte(level.minWageredUsdAmount)
);

findLast walks the array in reverse and returns the first match → the highest tier the affiliate qualifies for. Wager amount is the sum of all referrals' lifetime USD wager (getUsersStats at affiliate-user-stats.service.ts:55), not the affiliate's own wager.

1.1 Custom override

User.customMinAffiliateLevel (affiliate.utils.ts:17) lets an admin set a floor: if customMinAffiliateLevel.id > currentLevel.id, the player gets the higher commission immediately, without meeting the wager threshold. Used for: - Streamer / partner contracts negotiated outside the standard ladder - Boost windows / special incentives

// apps/api/src/affiliate/affiliate.utils.ts:24
return customMinAffiliateLevel && customMinAffiliateLevel.id > currentLevel.id
  ? customMinAffiliateLevel
  : currentLevel;

2. Commission math

Reuses the same CashbackUtils as rakeback (see rakeback.md §1.2):

// apps/api/src/affiliate/stats/affiliate-user-stats.service.ts:143
const cashback = CashbackUtils.calculate({
  bet:    args.bet,
  game:   args.game,
  percent: affiliateLevel.commission,
});

Algebraically — and identical in shape to rakeback:

GGR        = wager × (100 − RTP) / 100
commission = GGR × tier.commission

commission is denominated in the bet's own currency. The repository writes both per-currency claim balances and a USD-aggregate row used for tier evaluation:

// apps/api/src/affiliate/stats/affiliate-user-stats.repository.ts:22
const claimableAmountUsd = ExchangeRatesService.toUsd(args.claimableAmount, args.currencyId);
await prisma.affiliateUserStatsUsd.upsert({...claimableAmount: { increment: claimableAmountUsd }});
await prisma.affiliateUserStats.upsert({...claimableAmount: { increment: args.claimableAmount }}); // per-currency

3. Worked examples

3.1 Tier 1 affiliate, single referral

  • Affiliate has 0 referrals' lifetime wager → Tier 1, 10%.
  • Referral wagers 1000 USDT at 99% RTP house game.
GGR        = 1000 × (100 − 99) / 100 = 10 USDT
commission = 10 × 0.10               = 1 USDT  → claimable to affiliate

The affiliate's affiliateUserStats row for currency USDT increments claimableAmount += 1. The USD-aggregate row increments by ExchangeRatesService.toUsd(1, USDT)1 USD.

3.2 Tier 3 affiliate, mixed-currency book

  • Affiliate's referrals have collectively wagered $150,000 USD-equivalent → Tier 3, 20%.
  • Today, one referral wagers 0.005 BTC at 98% RTP. BTC ≈ $60,000.
wagerUsd     = 0.005 × 60000             = $300 USD
GGR          = 0.005 × (100 − 98) / 100 = 0.0001 BTC
commission   = 0.0001 × 0.20            = 0.00002 BTC  ≈ $1.20 USD

The affiliate's affiliateUserStats row for BTC increments by 0.00002; the USD aggregate by ~1.20.

3.3 Tier 2 affiliate fails the active-referral gate

  • Referrals' lifetime USD wager: $30,000 → qualifies for Tier 2 (commission 15%, requires 3 active referrals).
  • Active referrals (any referral with a bet in the last 14 days): only 2.

canClaim (affiliate-user-stats.service.ts:156) returns false. claim throws AFFILIATE_CONDITIONS_NOT_MET. Earnings continue to accrue; the affiliate has to wait for a third referral to wager (or for one of the 2 active ones to wager again so the count holds).

4. Active referral definition

// apps/api/src/affiliate/affiliate.utils.ts:30
static isReferralActive(lastWageredAt?: Date | null) {
  if (!lastWageredAt) return false;
  const cutoff = DateTime.now()
                  .minus({ days: AFFILIATE_ACTIVE_REFERRAL_PERIOD_DAYS })
                  .toMillis();
  return lastWageredAt.getTime() > cutoff;
}

AFFILIATE_ACTIVE_REFERRAL_PERIOD_DAYS = 14 (affiliate/const.ts:78). A referral counts as active if their most recent bet is within the last 14 days. The count is maintained on AffiliateAggregatedInfo.activeReferralsCount (see apps/api/src/affiliate/aggregated-info/); recomputed periodically. {{TBD: confirm with product team the recompute cadence and whether refunds reverse lastWageredAt.}}

5. Claim flow

sequenceDiagram
    participant Affiliate as Affiliate (player)
    participant Ctl as AffiliateController
    participant Svc as AffiliateUserStatsService
    participant Repo as AffiliateUserStatsRepository
    participant Acc as AccountingService
    participant DB as Postgres

    Affiliate->>Ctl: POST /affiliate/claim
    Ctl->>Svc: claim(WithUser)
    Note over Svc: Mutex<br/>affiliate:stats:claim:<userId>
    Svc->>Svc: getUsersStats + aggregatedInfo
    Svc->>Svc: AffiliateUtils.findLevel
    Svc->>Svc: canClaim(activeRefs >= minReferralCount)
    alt canClaim = false
        Svc-->>Ctl: AFFILIATE_CONDITIONS_NOT_MET
    else
        Svc->>Repo: findManyClaimable
        loop per (referral, currency) with claimableAmount > 0
            Svc->>Repo: claim ({userId, currencyId, amount})
            Note over Repo: stats: claimableAmount → claimedAmount<br/>statsUsd: same in USD
            Svc->>Acc: createTransaction (DEPOSIT, AFFILIATE_CLAIMED)
            Acc->>DB: insert tx + update affiliate balance
        end
    end

@Mutex key affiliate:stats:claim:<userId> (affiliate-user-stats.service.ts:69) prevents concurrent claims on the same affiliate. The whole claim is wrapped in PrismaTransactional.execute — if any per-currency leg fails, the whole batch rolls back.

6. Multi-level marketing

There is no MLM depth. The model is single-level: an affiliate earns only on bets by users with affiliateCodeId pointing at one of the affiliate's codes (affiliate.service.ts:114). A referred user can themselves become an affiliate, but their commissions accrue independently — the original referrer does not earn a "second-level" cut.

If product wants tiered-MLM in the future, the cleanest extension point is AffiliateService.handleBet (affiliate.service.ts:112) plus a recursive walk of User.affiliateCodeId → ownerId → User. {{TBD: confirm with product team this is on the roadmap or out of scope.}}

7. Codes — generation, limits, attribution

Property Value Source
Format ^[a-zA-Z0-9]{3,38}$, lower-cased affiliate/code/const.ts:7
Auto-generated length on signup 10 chars affiliate/code/const.ts:8
Per-affiliate cap 3 codes affiliate/code/const.ts:9 (REFERRAL_CODE_MAX_COUNT)
Default code on KYC LEVEL_1 username (lowercased) kyc.service.ts:88affiliateService.createDefaultCode
Forbidden words NO_FORBIDDEN_WORDS_REGEX libs/shared/src/security/word-blacklist-patterns.ts
Self-referral A user with their own code as affiliateCodeId would generate self-commissions. {{TBD: confirm whether code blocks this.}} n/a

The default code is created when the player crosses LEVEL_0 → LEVEL_1 (kyc.service.ts:88), so unverified accounts cannot operate as affiliates.

8. Streamer accounts

apps/api/src/affiliate/streamer/ — a separate program for content creators with bespoke admin tools, a streamer page, and per-stream stats. Operates alongside the standard tier system; commissions are still computed from User.customMinAffiliateLevel if set. {{TBD: product team to confirm streamer-specific commission overrides.}}

9. Edge cases

Case Behavior Source
Referral has no affiliateCodeId handleBet returns early; no commission accrued affiliate.service.ts:114
Affiliate code deleted after attribution User.affiliateCodeId retains the FK; commissions continue accruing affiliate-code.repository.ts
Refund of a commission-generating bet Commission is not reversed. {{TBD: confirm with product team.}} n/a
Bet status != SETTLED handleBet only fires on SETTLED (BetQueueProcessor at bet.queue-processor.ts:108) bet.queue-processor.ts:100
Tier downgrade after recompute Only future commissions are at the lower rate; accrued earnings are unaffected tier resolved per bet at affiliate-user-stats.service.ts:138
Custom level higher than computed customMinAffiliateLevel wins affiliate.utils.ts:24
Custom level lower than computed currentLevel wins (custom is a floor, not a ceiling) same
100% RTP game GGR = 0 → commission = 0 ggr.utils.ts:6
Concurrent claim attempts @Mutex serializes; second attempt waits affiliate-user-stats.service.ts:68

10. Admin overrides

Capability Code
Set customMinAffiliateLevel on a user Admin user-edit; field customMinAffiliateLevel
Create/delete affiliate codes for any user AffiliateCodeService.createAffiliateCodeByAdmin / deleteAffiliateCode
Inspect any affiliate's stats / referrals AffiliateService.findOneAffiliateByAdmin / findManyAffiliateUsersByAdmin
List affiliate claim history findManyClaims (Transaction tag = AFFILIATE_CLAIMED)
Adjust active-referral window Edit AFFILIATE_ACTIVE_REFERRAL_PERIOD_DAYS (compile-time const)
Manually credit Transaction { type: DEPOSIT, tag: AFFILIATE_CLAIMED, adminUserId: <admin>, payload: { userId } }