Appearance
Bonus Engine
The Bonus Engine manages the full lifecycle of player bonuses: typed templates, claiming (via the Promotions Engine), wagering requirements, and completion/expiry/forfeit. Operators configure the values inside typed templates; never free-form rule bags.
Public surface
| Member | Returns | Description |
|---|---|---|
getTemplates() | Promise<BonusTemplate[]> | Active bonus templates (rendering the promotions page) |
getActiveBonuses() | Promise<PlayerBonus[]> | Active bonuses with live wagering progress |
getBonusHistory() | Promise<PlayerBonus[]> | Audit trail: completed, expired, forfeited |
forfeitBonus(bonusId) | Promise<void> | Abandon an active bonus |
Typed structures: discriminated templates
Every bonus type has its own typed variant. Shared fields first, then per-type rules. getTemplates() returns the complete catalog; deterministic ordering: sorted by name ascending, then id, identical across reads.
typescript
interface BonusTemplateBase {
id: string;
name: string;
isActive: boolean;
wageringMultiplier: number; // e.g. 30 => wager 30x the bonus amount
validDays: number; // days active after claim
}
type BonusTemplate =
| (BonusTemplateBase & {
type: 'DEPOSIT_MATCH';
matchRate: number; // e.g. 1.0 = "100% match"
minDeposit: number; // eligibility threshold
maxBonusAmount: number; // cap on the granted amount
})
| (BonusTemplateBase & {
type: 'FREE_SPINS';
spinCount: number; // number of free spins granted
spinValue: number; // per-spin stake (operator-configurable); bonusAmount = spinCount × spinValue
eligibleGameIds?: string[]; // undefined = all games
})
| (BonusTemplateBase & {
type: 'CASHBACK';
cashbackRate: number; // e.g. 0.10 = 10% of net losses
currency: string; // the template's currency: only losses in this
// currency accrue, and the grant is in it
periodDays: number; // accrual window
})
| (BonusTemplateBase & {
type: 'NO_DEPOSIT';
bonusAmount: number; // flat granted amount
});The legacy
CUSTOMtemplate type is not carried forward: any campaign it covered is expressed as one of the four typed variants above. Free-form rule payloads are gone. Custom mechanics are not gone; they arrive as new typed variants on the platform roadmap: when a genuinely new bonus mechanic is needed, the platform adds a new variant with named, typed fields, and every client benefits from full type safety.
typescript
type BonusStatus = 'ACTIVE' | 'COMPLETED' | 'EXPIRED' | 'FORFEITED';
interface PlayerBonus {
id: string;
templateId: string;
name: string;
type: BonusTemplate['type'];
bonusAmount: number; // granted amount, in `currency`
currency: string;
status: BonusStatus;
wageringRequirement: number; // total to wager
wageredAmount: number; // progress
remainingWager: number; // wageringRequirement − wageredAmount
expiresAt: string; // ISO-8601
createdAt: string; // ISO-8601
}Grant rules (per variant)
text
GIVEN a DEPOSIT_MATCH template with matchRate M and maxBonusAmount C
WHEN a qualifying deposit of amount D is claimed
THEN the granted bonusAmount = min(M × D, C), in the deposit's currency,
and wageringRequirement = bonusAmount × wageringMultiplier.
GIVEN a FREE_SPINS template with spinCount N and spinValue V
WHEN claimed
THEN the granted bonusAmount = N × V (the spins' total stake), and
N free spins are granted for the eligible games.
GIVEN a CASHBACK template with cashbackRate R over periodDays P
WHEN the period closes
THEN the bonus is granted AUTOMATICALLY (no claim step) with
bonusAmount = R × L, where L is the player's net losses over the
period; L = (real-money wagers − real-money wins credited to
realBalance) in the template's currency, floored at 0 (a winning
period grants nothing). Wagers and wins in other currencies are
excluded (see the wagering-feed contract's currency rule).
GIVEN a NO_DEPOSIT template with bonusAmount B
WHEN claimed
THEN B is granted immediately, no deposit required.Lifecycle
text
GIVEN an ACTIVE bonus
WHEN the wagering feed advances it and remainingWager reaches 0
THEN status becomes COMPLETED and the bonus money converts to real,
withdrawable cash.
GIVEN an ACTIVE bonus whose expiresAt passes
WHEN the moment arrives (scheduled evaluation) OR any read occurs
(defensive evaluation)
THEN status becomes EXPIRED (a read never returns a stale ACTIVE state);
the remaining bonus money is removed, and the BonusExpiredEvent
fires exactly once, at the actual transition.
GIVEN an ACTIVE bonus
WHEN forfeitBonus(bonusId) is called
THEN status becomes FORFEITED, the bonus money is removed,
and the wagering restriction no longer blocks withdrawal of real cash.All transitions evaluate eagerly (at their trigger moment or driving write) AND defensively on read; events fire exactly once per transition.
Bonus money and the wallet ledger
Every bonus-money movement names its ledger entry type, direction, and amount:
text
GIVEN a bonus granted with bonusAmount B in currency C
WHEN the grant lands
THEN the wallet in currency C receives a CREDIT entry of type BONUS_CREDIT
for amount B: bonusBalance increases by B, realBalance is unchanged
(balanceAfter = the unchanged real balance; bonusBalanceAfter = the new bonusBalance).
GIVEN an ACTIVE bonus reaching COMPLETED with unconverted bonus money of amount B
WHEN the transition executes
THEN the bonus money becomes real, withdrawable cash and the wallet in the bonus's
currency receives a CREDIT entry of type BONUS_CONVERSION for amount B:
realBalance increases by B, bonusBalance decreases by B
(balanceAfter = the new real balance; bonusBalanceAfter = the new bonusBalance).
GIVEN a bonus reaching EXPIRED with unconverted bonus money of amount B
WHEN the transition executes
THEN the wallet receives a DEBIT entry of type BONUS_EXPIRY for amount B:
bonusBalance decreases by B, realBalance is untouched
(balanceAfter = the unchanged real balance; bonusBalanceAfter = the new bonusBalance).
GIVEN a bonus reaching FORFEITED with unconverted bonus money of amount B
WHEN the transition executes
THEN the wallet receives a DEBIT entry of type BONUS_FORFEIT for amount B:
bonusBalance decreases by B, realBalance is untouched
(balanceAfter = the unchanged real balance; bonusBalanceAfter = the new bonusBalance).Real cash is never removed by expiry or forfeit. See Wallet Overview for the balance model and Transaction History for the ledger schema (including balanceAfter / bonusBalanceAfter semantics).
Error contract
| Code | Error class | HTTP status | Trigger condition | Retryable |
|---|---|---|---|---|
ENG_BONUS_NOT_FOUND | BonusNotFoundError | 404 | forfeitBonus with an unknown bonus ID | No |
ENG_BONUS_NOT_ACTIVE | BonusNotActiveError | 409 | forfeitBonus on a non-ACTIVE bonus | No |
Common errors (NETWORK_ERROR, TIMEOUT, UNAUTHORIZED, …) are defined once in the Error Reference.
Events
Fired on the engagement event channel (see Engagement Overview):
typescript
interface BonusCompletedEvent { eventId: string; bonusId: string; }
interface BonusExpiredEvent { eventId: string; bonusId: string; }Operator-configurable values
| Value | Type | Default / range | Effect |
|---|---|---|---|
| Template catalog (per variant) | per template | operator-defined | Which bonuses can be claimed |
wageringMultiplier | number | operator-defined (e.g. 30) | Wagering requirement |
matchRate / cashbackRate / bonusAmount / spinCount | number | operator-defined | Grant size per variant |
minDeposit / maxBonusAmount | number | operator-defined | Match eligibility and cap |
validDays / periodDays | number | operator-defined | Validity windows |
eligibleGameIds | string[] | all games | Wagering/spin restriction |
The grant formulas, lifecycle transitions, and conversion-on-completion are platform-fixed.
Sandbox
Seeds exercise the full lifecycle: templates for all four types; bonuses at 0% and ~40% wagering; one COMPLETED and one FORFEITED in history, plus an EXPIRED seed and a bonus expiring in seconds (near-boundary) so the EXPIRED transition is demonstrable without waiting.
javascript
// Advance the wagering feed; progresses ALL eligible active bonuses
await sdk.engagement.sandbox.simulateWager(100);
const after = await sdk.engagement.bonus.getActiveBonuses();