Managing promotions and bonuses at the backend level
Full article
1) Why take the promo to a separate backend
Monetary invariants. Bonus ≠ "added balance": this is a contract with conditions (vager, contribution to games, maximum bet/win).
Rate of change. Marketing teams release campaigns daily - you need a declarative rules engine and rollback.
Anti-abuse/compliance. KYC/RG/AML, velocity, segmentation, four-eye tasks for expensive offers.
Observability and reporting. SLO, promo cost, impact on GGR/NGR/LTV.
Principle: the promo core is a separate service with its own status machines, and money moves only through the wallet, idempotently.
2) Bonus typology and invariants
Deposit match (100% before X): accrued after capture deposit, vager X ×.
Cashback: calculated by time window/games, can be sticky/non-sticky.
Free Spins/Free Bets: coupons/tokens with a price per spin/bet, fixed RTP pool.
Quests/missions: task → progress → reward.
Tournaments/flight events: contribution of events, rating, prize money.
Invariants:- Sticky: cannot be output until conditions are met.
- Max bet/Max win: limits on the bet/payout from bonus funds.
- Contribution: contribution by game (e.g. slots = 100%, live = 10%).
- Expiry: Bonus validity period and vager window.
3) Bonus service architecture
Admin ─Promo ─Rules Engine API/Eligibility
│
├─Bonus Ledger (status of offers)
├─Wagering Engine (progress)
├─Anti -Abuse (limits/fraud/velocity)
└─Outbox (events) ─Kafka/Pulsar ─BI/DWH/CRM
Wallet/Ledger── Idempotent Commands ───┘
Rules Engine - declarative conditions (segments, geo/license, channels, KYC/RG).
4) Data model (simplified)
`bonus_grant`
`wager_progress`- `grant_id, required_minor, contributed_minor, remaining_minor, last_update_at`
- `schema_id, rules: [{game_type:"slot", pct:100},{game_type:"live", pct:10}]`
'bonus _ ledger _ entry '(audit)
5) Status machines and sagas
5. 1 Issue - Saga
1. eligibility. check (segment, RG/KYC, velocity)
2. grant. create (status=`issued`)
3. wallet. credit [bonus] (idempotent; at sticky - to bonus sub-balance)
4. activate (status=`active`)
5. emit `bonus. issued`
Rollback: when falling at step 3 → 'grant. cancel '+ event' bonus. revoked`.
5. 2 Vager progress
Na'bet. settled 'count contribution =' stake _ minor contribution_pct' (or according to win/loss rules).
Update'wager _ progress' atomically; when 100% is reached - 'complete'.
5. 3 Finish (consume)
complete → `wallet. convert_bonus_to_cash' (if non-sticky) or remove output restrictions.
emit `bonus. consumed`.
5. 4 Expiration/Recall
By'expires _ at'or the fraud rule → 'revoke' (idempotent), compensation according to policy is possible.
6) Purse contracts (only via API, always idempotent)
Accrue bonus
POST /v1/wallet/credit
Headers: X-Idempotency-Key: bonus_grant_123
{
"player_id":"p_001", "amount":{"minor_units":100000,"currency":"EUR"}, "balance_type":"bonus", "reference":{"grant_id":"gr_123","offer_id":"of_777"}
}
→ 200 {"status":"credited","entry_id":"e_9001"}
Convert to cache when conditions are met
POST /v1/wallet/convert
Headers: X-Idempotency-Key: bonus_convert_gr_123
{
"player_id":"p_001", "from_balance":"bonus", "to_balance":"cash", "amount_minor":100000, "reference":{"grant_id":"gr_123"}
}
→ 200 {"status":"converted","entry_id":"e_9010"}
Check max bet/max win - on the RGS/Wallet Guard side:
- request 'bets. authorize 'is rejected by code'BONUS _ MAX _ BET _ EXCEEDED'.
7) API of promo service (templates)
Create an offer (admin)
POST /v1/offers
{
"name":"Welcome 100% up to 100€", "type":"deposit_match", "params":{"match_pct":100,"cap_minor":10000,"wager_x":20,"sticky":true, "max_bet_minor":200,"max_win_minor":50000,"contribution_schema_id":"c_slot100_live10"}, "eligibility":{"brands":["A"],"regions":["EU"],"segment":"new_depositors"}, "schedule":{"start":"2025-10-20T00:00:00Z","end":"2025-11-30T23:59:59Z"}
}
→ 201 {"offer_id":"of_777"}
Issue a bonus (runtime)
POST /v1/bonus/grants
Headers: X-Idempotency-Key: grant_p001_of777
{
"player_id":"p_001","offer_id":"of_777","trigger":"deposit_captured","amount_minor":10000
}
→ 200 {"grant_id":"gr_123","status":"active"}
Wager progress (read)
GET /v1/bonus/grants/gr_123/progress
→ 200 {"required_minor":200000,"contributed_minor":45000,"remaining_minor":155000,"pct":0. 225}
Void/Revoke
POST /v1/bonus/grants/gr_123/revoke
Headers: X-Idempotency-Key: revoke_gr_123
{ "reason":"fraud_velocity" }
→ 200 {"status":"revoked"}
All write calls are with 'X-Idempotency-Key' and 'X-Trace-Id'.
8) Anti-abuse and compliance
Velocity limits: issues/conversions/deposit attempts (Redis counters + TTL + Lua).
Trigger dedup: one deposit → one grant by rule.
Segmentation and RG: exclude self-excluded/limit; per brand/region license.
Block of conflict of offers: only one welcome bonus is active at a time; priorities.
Anomaly detector: multiple accounts/devices/ASN, fast "zeroing" of the vager.
"Four eyes" on large grants and manual adjustments.
WORM audit of all rule/grant/conversion changes.
9) Observability, metrics and SLO
SLO (landmarks):- `grant. issue p95` (issue→credited) ≤ 300–500 мс.
- Update 'wager _ progress p95' ≤ 200ms since 'bet. settled`.
- Events' bonus. 'In the p95 bus ≤ 2 minutes from what happened.
- "Lost/duplicated grants/conversions" = 0.
- Rate/latency по `issue/convert/revoke`, error-rate (business/4xx/5xx), `IDEMPOTENCY_MISMATCH`.
- Vager conversion, average 'time-to-complete', proportion overdue.
- Promo cost: 'promo _ cost' (minor) and 'promo _ roi' on cohorts.
- Anti-abuse: velocity triggers rejected by max bet/win.
Tracking: OpenTelemetry on the chain'trigger → grant → wallet. credit → progress. update → convert`.
10) Integration with RGS/games
Free Spins/Free Bets coupons - via'entitlements' API: issuing tokens, scrapping in runtime, telemetry by use.
Max bet/win - rules in 'bets. authorize` и `bets. settle`; return codes' BONUS _ RULE _ VIOLATION '.
Contribution - scheme at the 'bet level. settled '(by' game _ type/provider _ id '), schema version.
11) DWH/BI and reports
Outbox events → Lake (bronze) → Silver (dedup, SCD2) → Gold showcases:- `fact_bonus_grants`, `fact_wager_progress`, `fact_bonus_cost`, `fact_promo_roi`.
- SLA freshness: Silver ≤ 15 min, Gold ≤ 30-60 min.
- Panels: conversion by offers/segments, time-to-complete, contribution by games, abuse incidents.
12) Safety and residency
mTLS + OAuth2 CC; scope’ы `promo:issue`, `promo:convert`, `promo:revoke`.
Keys/tokens - per brand/region, short-lived; secrets in Vault/HSM.
PII isolation: 'player _ id' - alias; RLS по `brand/region`.
Rate limits and issuance quotas; protection from retray storms.
13) Checklists
Platform/Operator
- All monetary transactions go through Wallet with'Idempotency-Key '.
- Rules/Eligibility are versioned; "double letter" of events on migrations.
- Contribution schemes are centralized, covered with tests.
- Velocity and anti-fraud enabled; "four eyes" on large sums.
- Outbox/CDC, DLQ and managed replay for 'bonus.'.
- SLO dashboards, OpenTelemetry, WORM audit.
- DWH storefronts for ROI and compliance (RG/AML).
Integrations (RGS/wallet/CRM)
- Checking max bet/win; returning the business error code.
- I throw 'trace _ id' and 'idempotency _ key'.
- Deadup triggers and delivery guarantees (webhooks signed).
14) Red flags (anti-patterns)
Charging the bonus "manually" directly into the balance, bypassing Wallet.
Lack of idempotency → double grants/conversions.
The wager is considered by 'bet. placed ', and not according to the results of' bet. settled`.
There are no contribution schemes or they are "protected" in the code of providers.
Conflicting offers are activated simultaneously.
There is no velocity/anti-fraud and WORM audit.
'bonus. 'events are posted bypassing outbox/CDC.
Promo metrics don't add up to Ledger/BI (no ROI showcases).
15) The bottom line
Reliable backend promos are contracts and invariants, not "add balance." It separates rules from money, considers progress according to actual outcomes, guarantees idempotency and observability, protects against abuse and ensures compliance. With such a core, marketing moves quickly, the player sees honest conditions, and finances and regulators get an accurate picture of the cost and effect of each offer.