Build with
Pooled.
Everything you need to integrate Pooled prize pools into your casino platform. JWT authentication, iFrame embedding, and webhook handling — live in half a day.
Quickstart
§ QSPooled uses Integration Mode only. Your server authenticates players via signed JWT tokens — we never handle player accounts, KYC, or funds. Here's how it works end to end:
pool.win event is critical — this is when you credit the winner's wallet.JWT Token Auth
§ JWTYour server generates a signed JWT for each authenticated player session. Pooled validates the signature and uses the token payload to identify the player and display their balance. Tokens must be generated server-side — never in the browser.
// JWT Header (set automatically by your library) { "alg": "HS256", // Required — must be HS256 "typ": "JWT" } // JWT Payload (you supply these claims) { "sub": "player_abc123", // Your internal player ID "name": "Jordan S.", // Display name (last name initial only) "balance": 250.00, // Current wallet balance in USD "currency": "USD", // Display currency "verified": true, // KYC/age verification status "exp": 1716998400 // Unix timestamp — max 24hr from now }
Code Samples
// npm install jsonwebtoken const jwt = require('jsonwebtoken'); function getPooledToken(player) { return jwt.sign( { sub: player.id, // string — your internal player ID name: player.displayName, // e.g. "Jordan S." balance: player.walletUSD, // number — current balance in USD currency: player.currency, // "USD" | "EUR" | "GBP" | "CAD" | "AUD" | "NZD" verified: player.kycPassed, // boolean — KYC/18+ verification status exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour }, process.env.POOLED_JWT_SECRET, { algorithm: 'HS256' } ); } // In your lobby route: app.get('/lobby', (req, res) => { const player = req.user; // your authenticated session const token = getPooledToken(player); res.render('lobby', { pooledToken: token }); });
// composer require firebase/php-jwt use Firebase\JWT\JWT; function getPooledToken($player) { $payload = [ 'sub' => $player->id, 'name' => $player->display_name, 'balance' => (float) $player->wallet_usd, 'currency' => $player->currency, 'verified' => (bool) $player->kyc_passed, 'exp' => time() + 3600, ]; return JWT::encode( $payload, getenv('POOLED_JWT_SECRET'), 'HS256' ); } // In your lobby controller: $token = getPooledToken($currentPlayer); // Pass $token to your view template
# pip install PyJWT import jwt import time import os def get_pooled_token(player): payload = { 'sub': str(player.id), 'name': player.display_name, 'balance': float(player.wallet_usd), 'currency': player.currency, 'verified': bool(player.kyc_passed), 'exp': int(time.time()) + 3600, } return jwt.encode( payload, os.environ['POOLED_JWT_SECRET'], algorithm='HS256' ) # In your lobby view: token = get_pooled_token(request.user) # Pass token to template context
Token Reference
§ TR| Claim | Type | Required | Description |
|---|---|---|---|
| sub | string | Required | Your internal player ID. Must be unique and stable per player. This is the ID you will receive back in webhook events to identify the player. |
| name | string | Required | Player display name shown in the game UI and leaderboard. Use last name initial only for privacy — e.g. "Jordan S." |
| balance | number | Required | Player's current wallet balance in USD. Must be a number, not a string. Regenerate the token after wallet transactions to keep balance current. |
| currency | string | Required | Display currency for prizes and balance. Supported: USD EUR GBP CAD AUD NZD |
| verified | boolean | Required | Set to true if your platform has completed KYC and 18+ age verification for this player. Players with false cannot enter paid pools. |
| exp | unix timestamp | Required | Token expiry time. Maximum 24 hours from issuance. Recommended: 1 hour (Math.floor(Date.now()/1000) + 3600). Expired tokens are rejected. |
| string | Optional | Player email address. Only include if you want Pooled to send win notification emails to players. Not stored after session ends. |
exp timestamp or until the player reloads the page, whichever comes first.
iFrame Embed
§ IFEmbed Pooled in your casino lobby with a single HTML snippet. Players see your branding — nothing in the UI mentions Pooled. The token is injected server-side into the src URL.
<!-- Generate token server-side and inject into src --> <iframe src="https://app.pooledgame.com/?tenant=YOUR_SLUG&token=SERVER_GENERATED_TOKEN" width="100%" height="800px" frameborder="0" allow="payment" title="Prize Pools" />
royal-casino).
Content Security Policy
If your platform sets CSP headers, add app.pooledgame.com to your frame-src directive:
Content-Security-Policy: frame-src 'self' https://app.pooledgame.com;
Mobile / WebView
The game is fully responsive and works in iOS and Android WebViews. Use the same src URL. No additional configuration needed.
Webhooks
§ WHPooled sends signed HTTP POST requests to your configured endpoint when pool events occur. The pool.win event is critical — this is when you must credit the winner's wallet. Your endpoint must be HTTPS and respond with HTTP 200 within 10 seconds.
pool.win event.
Signature Verification
Every webhook is signed with HMAC-SHA256 using your Webhook Secret. Always verify the X-Pooled-Signature header before processing any event.
const crypto = require('crypto'); // IMPORTANT: Use express.raw() — not express.json() — for this route app.post('/pooled/webhook', express.raw({ type: 'application/json' }), (req, res) => { // Step 1 — Verify signature const sig = req.headers['x-pooled-signature']; const expected = crypto .createHmac('sha256', process.env.POOLED_WEBHOOK_SECRET) .update(req.body) .digest('hex'); if (sig !== expected) { return res.status(401).json({ error: 'Invalid signature' }); } // Step 2 — Parse and handle const event = JSON.parse(req.body); switch (event.event) { case 'pool.win': // Credit winner — winner_id matches the player's JWT sub claim walletService.credit(event.winner_id, event.amount); break; case 'pool.refund': // Pool expired — refund the entry fee walletService.refund(event.user_id, event.refund_amount); break; case 'pool.entry': // Optional — log entry in your system break; } // Must return 200 — Pooled logs non-200 as failed delivery res.status(200).json({ received: true }); });
$body = file_get_contents('php://input'); $sig = $_SERVER['HTTP_X_POOLED_SIGNATURE'] ?? ''; $secret = getenv('POOLED_WEBHOOK_SECRET'); $expected = hash_hmac('sha256', $body, $secret); if (!hash_equals($expected, $sig)) { http_response_code(401); exit('Invalid signature'); } $event = json_decode($body, true); if ($event['event'] === 'pool.win') { creditWallet($event['winner_id'], $event['amount']); } if ($event['event'] === 'pool.refund') { refundWallet($event['user_id'], $event['refund_amount']); } http_response_code(200); echo json_encode(['received' => true]);
import hmac, hashlib, os, json from flask import request, jsonify, abort @app.route('/pooled/webhook', methods=['POST']) def pooled_webhook(): body = request.get_data() sig = request.headers.get('X-Pooled-Signature', '') secret = os.environ['POOLED_WEBHOOK_SECRET'].encode() expected = hmac.new(secret, body, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, sig): abort(401) event = json.loads(body) if event['event'] == 'pool.win': credit_wallet(event['winner_id'], event['amount']) if event['event'] == 'pool.refund': refund_wallet(event['user_id'], event['refund_amount']) return jsonify({ 'received': True }), 200
Webhook Events
§ EVEvent Payloads
§ PLpool.win
{
"event": "pool.win",
"tenant_id": "uuid",
"timestamp": "2026-05-29T14:32:00Z",
"pool_id": "uuid",
"pool_size": 1000, // Prize in USD: 100 | 1000 | 10000 | 100000 | 1000000
"winner_id": "player_abc123", // Matches the player's JWT sub claim
"winner_name": "Jordan S.",
"amount": 1000, // Amount to credit in USD
"seed": "a3f8c2d1...", // Revealed random seed
"seed_commit": "sha256hash...", // Verify: hash(seed) == seed_commit
"entry_count": 1000
}
pool.refund
{
"event": "pool.refund",
"tenant_id": "uuid",
"timestamp": "2026-05-29T14:32:00Z",
"pool_id": "uuid",
"pool_size": 1000,
"user_id": "player_abc123", // Player's JWT sub claim
"refund_amount": 1.00, // Always $1.00 per paid entry
"entry_type": "paid" // "paid" | "bundle"
}
pool.entry
{
"event": "pool.entry",
"tenant_id": "uuid",
"timestamp": "2026-05-29T14:32:00Z",
"pool_id": "uuid",
"pool_size": 1000,
"user_id": "player_abc123",
"entry_type": "paid", // "paid" | "bundle" | "credit"
"tx_id": "uuid",
"slots_filled": 347,
"slots_total": 1000
}
Test Mode
§ TMUse the demo tenant to verify your integration before going live. Demo mode uses fast-filling pools (seconds not minutes) and fake balances — no real money involved.
// Player game view (demo) https://app.pooledgame.com/?role=game&tenant=demo&demo=true // Test your JWT token with the demo tenant https://app.pooledgame.com/?tenant=demo&token=YOUR_TEST_JWT // Casino admin (to view demo webhook delivery log) https://app.pooledgame.com/?role=casino&tenant=demo
End-to-End Test Checklist
| Test | Expected Result |
|---|---|
| Token validates at jwt.io | All 6 claims present, correct types, exp in future |
| balance is a number not a string | jwt.io shows 250 not "250" |
| iFrame loads with valid token | Game UI appears with your casino branding |
| Balance shown matches token | Exact match to balance claim |
| Player can enter a pool | Entry recorded, balance deducts $1.50 |
| pool.win webhook fires | Delivery log shows 200 OK |
| winner_id matches player sub | Exact string match |
| Your wallet credits on win | Winner balance increases by prize amount |
| pool.refund fires on expiry | Delivery log shows event, your wallet refunds |
| Invalid token is rejected | Player sees error, cannot enter game |
Error Reference
§ ER| Error | Cause | Fix |
|---|---|---|
| White screen / blank iFrame | Invalid or expired JWT token | Regenerate token server-side. Check exp claim is in the future. |
| Token validation failed | JWT secret mismatch | Verify POOLED_JWT_SECRET matches exactly what's in your admin panel. No trailing whitespace or newlines. |
| Wrong balance displayed | Token cached or balance sent as string | Regenerate token on every page load. Ensure balance is a number, not "250.00". |
| Player can't enter pool | verified is false or balance too low | Set verified: true after KYC. Balance must be ≥ $1.50. |
| Webhooks not arriving | Endpoint not HTTPS or not publicly reachable | Ensure your endpoint is HTTPS, publicly accessible, not localhost. Check firewall rules. |
| Signature verification failing | Wrong secret or body pre-parsed | Use POOLED_WEBHOOK_SECRET (not JWT secret). Use express.raw() not express.json() on the webhook route. |
| iFrame blocked by browser | Content Security Policy violation | Add app.pooledgame.com to your frame-src CSP header. |
| Webhook delivery timeout | Endpoint takes > 10 seconds to respond | Return 200 immediately, then process the event asynchronously (queue it). |
Go-Live Checklist
§ GLConfirm every item before switching from test to production traffic.
| Item | Notes |
|---|---|
| ✓ SLA signed | Required before any production traffic |
| ✓ JWT Secret stored in env var | Never hardcoded or in client-side code |
| ✓ Webhook Secret stored in env var | Separate from JWT secret |
| ✓ Production webhook URL configured | In admin panel → Integration → Webhooks |
| ✓ Webhook endpoint responds within 10s | Return 200 immediately, process async if needed |
| ✓ Wallet credits on pool.win verified | Tested end-to-end in demo mode |
| ✓ Refund logic tested | pool.refund event handled correctly |
| ✓ Token generated fresh on every page load | Not cached — balance must be current |
| ✓ CSP headers updated | app.pooledgame.com in frame-src |
| ✓ Mobile display verified | Tested on actual mobile device |