Skip to content

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

MemberReturnsDescription
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

CurrencyPurposeSpent?
XPDetermines VIP tier. Earned per real-money wager.Never (except tier decay)
Loyalty PointsSpendable 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

CodeError classHTTP statusTrigger conditionRetryable
ENG_SHOP_ITEM_NOT_FOUNDShopItemNotFoundError404redeemItem with an unknown item IDNo
ENG_INSUFFICIENT_POINTSInsufficientPointsError400Points balance below pointsCostNo (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

ValueTypeDefault / rangeEffect
Tier ladder (minXP, names, colors)per tieroperator-definedProgression thresholds
Tier perks (typed fields only)per tieroperator-definedWithdrawal limits, cashback rates, managers
xpRate / pointsRatenumber1 per unitAccrual per wagered unit
Maintenance windows + thresholdsper tieroperator-definedDecay pressure
Shop catalog (per item kind)per itemoperator-definedRedeemable rewards

The accrual formula, decay semantics, and the shop/wallet mapping are platform-fixed.

Sandbox

SeedPurpose
5-tier ladder: Bronze → DiamondLadder 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 daysDecay-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();