Appearance
Loyalty Engine
The Loyalty Engine runs the VIP program: a tier ladder, XP accumulation, tier maintenance (decay), and the Loyalty Points shop. Operators configure thresholds, perks, and the shop catalog, inside typed structures.
Public surface
| Member | Returns | Description |
|---|---|---|
getTiers() | Promise<VIPTier[]> | The full ladder, ascending |
getStatus() | Promise<PlayerLoyaltyStatus> | Current tier, XP, points, progress, maintenance |
getShop() | Promise<LoyaltyShopItem[]> | Shop catalog |
redeemItem(itemId) | Promise<RedemptionResult> | Spend points on a shop item |
getRedemptions() | Promise<RedemptionRecord[]> | The player's redemption history; the ONLY way to observe PENDING merch fulfillments |
getTiers() and getShop() return complete catalogs; deterministic ordering: tiers sorted by minXP ascending, shop items by pointsCost ascending, then id, identical across reads. getRedemptions() returns newest first; sorted by createdAt descending, then id.
Dual currency model
| Currency | Purpose | Spent? |
|---|---|---|
| XP | Determines VIP tier. Earned per real-money wager. | Never (except tier decay) |
| Loyalty Points | Spendable shop currency. Earned per real-money wager. | Yes, in the Loyalty Shop |
Multi-currency accrual (1.0.x rule): XP and points accrue at face value regardless of the wager's currency; a 10 EUR wager and a 10 USD wager each earn 10 × the configured rate. This is a documented simplification; converting to a base currency at accrual time is a future option (a breaking change to displayed progress, so it will never change silently).
Typed structures
typescript
interface TierPerks {
withdrawalLimit?: number; // per-period withdrawal ceiling for the tier
cashbackRate?: number; // tier cashback rate (e.g. 0.05)
personalManager?: boolean; // assigned VIP manager
}
interface VIPTier {
id: string;
name: string; // 'Bronze', 'Silver', 'Gold', ...
minXP: number; // XP required to reach this tier
color?: string; // hex color for UI rendering
perks: TierPerks;
}
interface PlayerLoyaltyStatus {
currentXP: number;
loyaltyPoints: number; // shop balance
currentTier: VIPTier;
nextTier: VIPTier | null; // null at max tier
progressToNextTier: number; // 0–100, ready for progress bars
maintainBy?: string; // ISO-8601 maintenance deadline, when applicable
maintainXPNeeded?: number; // XP still needed to keep the tier
}
/** Shop items are typed per fulfillment kind; no free-form payloads. */
type LoyaltyShopItem =
| { id: string; name: string; description: string; pointsCost: number;
kind: 'FREE_SPINS'; gameId: string; spinCount: number; spinValue: number; }
| { id: string; name: string; description: string; pointsCost: number;
kind: 'BONUS_CASH'; amount: number; currency: string; }
| { id: string; name: string; description: string; pointsCost: number;
kind: 'PHYSICAL_MERCH'; imageUrl?: string; };
interface RedemptionResult {
redemptionId: string;
itemId: string;
status: 'COMPLETED' | 'PENDING'; // digital completes instantly; merch awaits fulfillment
remainingPoints: number;
}
interface RedemptionRecord {
redemptionId: string;
itemId: string;
itemName: string;
kind: LoyaltyShopItem['kind'];
pointsCost: number;
status: 'COMPLETED' | 'PENDING'; // PENDING is observable ONLY here (merch awaiting shipping)
createdAt: string; // ISO-8601
shippedAt?: string; // ISO-8601, present on completed merch fulfillments
}Tier progression and maintenance
text
GIVEN a player whose XP crosses a higher tier's minXP
WHEN the wagering feed writes the XP (eager evaluation) OR any status
read occurs (defensive evaluation)
THEN their current tier moves up, and the TierChangedEvent fires exactly
once, at the actual transition.
GIVEN a player in a tier with a maintenance requirement
WHEN the maintainBy deadline arrives (scheduled evaluation) with
maintainXPNeeded XP still unearned
THEN the player drops one tier (decay), and the TierChangedEvent fires
exactly once; a read never returns a stale tier.
An executed decay is never reversed by a defensive read: after the deadline,
the decayed tier is the current tier. The player re-qualifies only through
new XP (a new eager write), which moves them back up through the same
TierChangedEvent path.
GIVEN a real-money wager of amount A
WHEN the wagering feed advances loyalty
THEN currentXP += A × xpRate and loyaltyPoints += A × pointsRate
(operator-configurable rates, default 1 per unit; face value; see above).The shop: wallet and fulfillment mapping
text
GIVEN a redemption of a BONUS_CASH item with amount X in currency C
WHEN redeemItem succeeds
THEN points are deducted, the reward is granted instantly (status COMPLETED),
and the wallet in currency C receives a BONUS_CREDIT ledger entry of X.
GIVEN a redemption of a FREE_SPINS item with spinCount N and spinValue V
WHEN redeemItem succeeds
THEN points are deducted, the spins are granted instantly, and the wallet
receives a BONUS_CREDIT ledger entry of N × V in the reward's currency.
GIVEN a redemption of a PHYSICAL_MERCH item
WHEN redeemItem succeeds
THEN points are deducted and the redemption is PENDING; fulfillment is tracked
by the operator (shipping), with NO wallet ledger entry (nothing financial
moved). The redemption flips to COMPLETED when shipped (observable via
getRedemptions; the shippedAt timestamp records when).Redemption errors: unknown item → ENG_SHOP_ITEM_NOT_FOUND; balance too low → ENG_INSUFFICIENT_POINTS.
Error contract
| Code | Error class | HTTP status | Trigger condition | Retryable |
|---|---|---|---|---|
ENG_SHOP_ITEM_NOT_FOUND | ShopItemNotFoundError | 404 | redeemItem with an unknown item ID | No |
ENG_INSUFFICIENT_POINTS | InsufficientPointsError | 400 | Points balance below pointsCost | No (until earned) |
Common errors are defined once in the Error Reference.
Events
Fired on the engagement event channel (see Engagement Overview):
typescript
interface TierChangedEvent {
eventId: string;
playerId: string;
fromTierId: string;
toTierId: string; // direction is derivable from the ladder's ordering
}Operator-configurable values
| Value | Type | Default / range | Effect |
|---|---|---|---|
Tier ladder (minXP, names, colors) | per tier | operator-defined | Progression thresholds |
Tier perks (typed fields only) | per tier | operator-defined | Withdrawal limits, cashback rates, managers |
xpRate / pointsRate | number | 1 per unit | Accrual per wagered unit |
| Maintenance windows + thresholds | per tier | operator-defined | Decay pressure |
| Shop catalog (per item kind) | per item | operator-defined | Redeemable rewards |
The accrual formula, decay semantics, and the shop/wallet mapping are platform-fixed.
Sandbox
| Seed | Purpose |
|---|---|
| 5-tier ladder: Bronze → Diamond | Ladder rendering, typed perks per tier |
| Player at 6,000 XP / 7,000 points (Gold) | Mid-tier progress + maintenance countdown |
| Gold maintenance threshold 8,000 XP / 30 days | Decay-warning UI |
| 3 shop items (Free Spins, Bonus Cash, T-Shirt) | All fulfillment kinds |
javascript
// Wagers add XP AND points; can push the player toward Platinum
await sdk.engagement.sandbox.simulateWager(500);
const status = await sdk.engagement.loyalty.getStatus();
// Merch fulfillment is controllable (see Sandbox Controls):
await sdk.engagement.sandbox.completeRedemption('red_x'); // PENDING → COMPLETED (shipped)
const history = await sdk.engagement.loyalty.getRedemptions();