Bonus Engine
Bonus issuance, wagering requirement tracking, balance locking and auto-expiry
Purpose
Bonus issuance
Grant deposit match bonuses when a qualifying deposit is made and the player has opted in. Creates a bonus record and credits locked balance.
Wagering requirement tracking
Track how much the player has wagered towards the requirement. Each qualifying bet increments progress based on game category contribution rate.
Balance locking
Bonus funds are held in a locked balance that cannot be withdrawn. Only unlocked (converted to real money) when wagering is complete.
Withdrawal block
Integrates with Limits & Rules engine to block withdrawal while an active bonus has an unfulfilled wager requirement.
Auto-expiry
Bonuses expire if wagering is not completed within the configured period. Expired bonuses are cancelled and locked balance is deducted.
Lifecycle audit
Every status transition is logged with timestamp, reason, and actor. Required for compliance and dispute resolution.
Where it sits and how information flows
Position in the stack
Three main flows
On deposit confirmed
On each qualifying bet
On withdrawal request (called by Limits & Rules)
Input data and sources
user_id, brand_id, deposit amount, payment method
from Wallet Engine on deposit confirmed
Active bonuses for user: status, wager progress, expiry
player_bonuses table (Redis cache)
Bonus templates: match %, min deposit, max bonus, wager multiplier, expiry
bonus_templates table (per brand)
Bet amount, game category, game_id (for wagering contribution)
Game provider webhook / Wallet Engine on each bet
Withdrawal request amount (for wager lock check)
from Limits & Rules engine (withdrawal flow)
Required at launch
Bonus lifecycle
PENDINGACTIVECOMPLETEDCANCELLEDEXPIREDFORFEITEDPENDINGBonus created, deposit confirmed, awaiting activationACTIVEWagering in progress -- player is playingCOMPLETEDWager requirement met -- bonus funds converted to real moneyCANCELLEDPlayer withdrew before completing wager, or ops cancelledEXPIREDExpiry date passed before wager was completedFORFEITEDPlayer explicitly opted out and forfeited bonus + winningsRules required for launch
| Rule | Details | Required? |
|---|---|---|
| Deposit match bonus | Trigger: deposit ≥ min_deposit + player opted in. Credit: deposit × match_pct, capped at max_bonus. | Yes |
| Wager requirement tracking | Every qualifying bet increments wager_progress by bet_amount × contribution_rate. When progress ≥ requirement, auto-complete. | Yes |
| Bonus balance lock | Bonus funds are held in a separate locked balance in Wallet Engine. Cannot be withdrawn until COMPLETED. | Yes |
| Withdrawal block integration | Limits & Rules engine calls Bonus Engine before approving withdrawal. If active bonus with incomplete wager → WAGER_NOT_COMPLETE. | Yes |
| Bonus auto-expiry | Scheduled job runs daily: mark expired bonuses as EXPIRED, deduct locked balance from Wallet Engine. | Yes |
| One active bonus per player | Phase 3: no bonus stacking. New bonus offer is blocked if player already has an ACTIVE or PENDING bonus. | Yes |
| Bonus cancellation by ops | Operations team can cancel any bonus. On cancel: set CANCELLED, deduct bonus balance, log reason. | Yes |
| Configurable contribution rates | Rates per game category stored in bonus_templates.game_contributions. Not hardcoded. | Yes |
| Wagering on real money first | Real money balance is wagered before bonus balance. Prevents bonus abuse where players spin only with bonus funds. | Recommended |
| Max win cap per bonus | Total winnings from bonus funds capped at max_win_multiplier × bonus_amount. Excess deducted on completion. | Recommended |
Wagering contribution by game category (typical defaults)
| Category | Contribution | Note |
|---|---|---|
| Slots | 100% | -- |
| Live casino | 10% | High RTP games contribute less |
| Table games (RNG) | 20% | -- |
| Sports betting | 10% | Only bets with odds ≥ 1.5 count |
| Video poker | 10% | -- |
| Scratch cards / Keno | 50% | -- |
| Jackpot slots | 0% | Excluded from wagering by default |
Error codes
| Code | HTTP | Description | Player message |
|---|---|---|---|
WAGER_NOT_COMPLETE | 422 | Active bonus has an unfulfilled wagering requirement | "Complete your wagering requirement to withdraw. Progress: X%" |
BONUS_ALREADY_ACTIVE | 422 | Player already has an active or pending bonus — cannot issue another | "You already have an active bonus" |
BONUS_EXPIRED | 422 | Bonus expired before player completed wagering | "Your bonus has expired" |
BONUS_CANCELLED | 422 | Bonus was cancelled by ops team | "Your bonus has been removed. Contact support for details." |
DEPOSIT_TOO_LOW | 422 | Deposit amount below min_deposit threshold for bonus eligibility | "Minimum deposit for this bonus is $X" |
Minimum DB schema for Phase 3
-- Bonus offer templates (configured per brand by ops)
bonus_templates (
id, brand_id, name, type, -- 'deposit_match'
match_pct, min_deposit, max_bonus,
wager_multiplier, -- e.g. 30 means 30× bonus amount
expiry_days,
max_win_multiplier, -- null = no cap
game_contributions -- JSON: { slots: 1.0, live: 0.1, ... }
is_active
)
-- Active bonuses per player
player_bonuses (
id, user_id, brand_id, bonus_template_id,
status, -- PENDING | ACTIVE | COMPLETED | CANCELLED | EXPIRED | FORFEITED
bonus_amount, -- credited to locked balance
wager_requirement, -- bonus_amount × wager_multiplier
wager_progress, -- incremented on each qualifying bet
deposited_amount, -- the deposit that triggered this bonus
expires_at,
completed_at, cancelled_at, cancel_reason,
created_at
)
-- Append-only wager event log
wager_transactions (
id, player_bonus_id, transaction_id,
game_category, bet_amount,
contribution_rate, contribution_amount,
wager_progress_after,
created_at
)Best practices
Separation of concerns
- Wallet Engine tracks balances (real, locked, bonus). Bonus Engine tracks eligibility, progress, and lifecycle. Never mix these concerns.
- Wagering progress is tracked per-bonus (not per-player) to support multiple bonuses in future phases without refactoring.
- Bonus Engine never modifies Wallet balances directly -- it sends commands to Wallet Engine (credit_locked, unlock, debit_locked).
Phase 3 scope -- what to build now
- One bonus type for Phase 3: deposit match bonus. Free spins, cashback, referral bonuses -- Phase 5+.
- No bonus stacking for Phase 3. One active bonus per player per brand is sufficient for launch.
- Wagering tracking can be event-driven (bet webhook) or batch (nightly reconciliation of bets). Event-driven is preferred.
Performance & reliability
- Cache active bonus per user in Redis (TTL 60s) -- checked on every withdrawal request.
- Wagering updates are high-frequency (every bet). Use optimistic locking or atomic increment to avoid race conditions on wager_progress.
- Expiry job must be idempotent -- safe to run multiple times without double-deducting balances.
Compliance & audit
- Log every state transition with reason: PENDING→ACTIVE, ACTIVE→COMPLETED, ACTIVE→CANCELLED(reason), ACTIVE→EXPIRED.
- Bonus terms (wager multiplier, expiry, max win) must be shown to player at opt-in -- regulatory requirement in most jurisdictions.
- On withdrawal with active bonus: present player with a clear choice -- forfeit bonus and withdraw, or keep bonus and continue wagering.