"Integrating a casino aggregator" sounds like it should be a single afternoon — and the README is always 90% there. This is the missing 10%: what shows up only after you take real-money traffic.
| Component | What it does | Where it bites |
|---|---|---|
| Game catalog | List of available games + metadata | Provider name vs game-code mapping changes; cache invalidation |
| Signed launch URL | Generates the in-game URL your iframe loads | Clock skew between you and aggregator; query-string ordering |
| RGS callback | Aggregator → your backend: balance/bet/win/refund | Idempotency on retries; race condition on simultaneous bets |
| Settlement webhook | Post-session reconciliation | Out-of-order delivery; partial state on errors |
Most aggregator docs say "send your API key in the X-API-Key header". That's fine for the catalog endpoint. For the RGS callback (when the aggregator calls YOU), you need HMAC-signed bodies — never trust an inbound call just because it has your key. The signing key is separate from your API key; if your docs don't mention it, ask before going live.
// Verifying an inbound RGS callback
const expected = crypto
.createHmac('sha256', RGS_SIGNING_SECRET)
.update(req.rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.header('X-Signature')))) {
return res.status(401).json({ error: 'bad sig' });
}
The aggregator hands you a URL with a TTL (usually 5–15 minutes). Your server signs it; the aggregator validates against the signature. If your server's NTP is drifting by more than ~30 seconds, signature validation will randomly fail on a fraction of launches. We've seen this kill 5–10% of game starts in production.
Fix: run chronyd or systemd-timesyncd; sample your clock drift via chronyc tracking at boot and alert if > 1s.
The aggregator will retry. Sometimes within seconds (network blip), sometimes minutes later (their queue backed up). If your /callback handler processes a bet twice, you've double-debited the player. If it processes a win twice, you've handed out free money.
The pattern that survives production:
// Pseudocode for an idempotent /callback handler
async function handleCallback(req) {
const txId = req.body.transaction_id; // ID from aggregator
const cached = await redis.get(`tx:${txId}`);
if (cached) return JSON.parse(cached); // already handled
const result = await db.transaction(async (t) => {
// Apply balance change, record tx, atomically
return await processBetOrWin(req.body, t);
});
await redis.set(`tx:${txId}`, JSON.stringify(result), 'EX', 86400);
return result;
}
If your players bet in BTC but the aggregator's pricing is in USD, someone is doing FX. Two things must be true:
Otherwise a single bet that takes 30 seconds (player thinking, then spinning) can clear at a different USD price than it was placed at, and the math doesn't balance.
Demo mode launches don't trigger RGS callbacks (no balance to move), but they DO trigger session events. If your analytics counts every game launch as "active player", demo traffic will inflate your DAU by 3–5x. Filter on the mode: 'real' flag in session events.