Security & Operations
Launch-readiness guidance for player tokens, HMAC secrets, idempotency, fairness, CORS, retries, and monitoring.
Security & Operations
Upside games combine an untrusted browser iframe with authenticated player settlement. The safest pattern is simple: the iframe gathers player input, your backend owns all game truth, and Upside owns WIN ledger movements.
Threat Model
| Surface | Risk | Control |
|---|---|---|
| Iframe messages | Token spoofing or data injection | Verify event.origin and use explicit targetOrigin. |
| Browser game state | Modified client code or replayed requests | Treat frontend state as display/input only. |
| Backend credentials | API key exposure | Store HMAC credentials only in server-side secrets. |
| Settlement | Duplicate bets or duplicate payouts | Use server-generated sessionId and idempotent state transitions. |
| Randomness | Predictable outcomes | Generate outcomes server-side and store audit metadata. |
| Balance UI | Stale balance after settlement | Post refetchBalance only after successful settlement. |
Token Handling
- The player JWT arrives in the iframe by parent
postMessage. - The iframe forwards it only to your backend as
Authorization: Bearer {token}. - Your backend sends it to Upside as
B3-JWT. - The JWT is part of the HMAC signature payload, binding the signed request to that player.
UPSIDE_API_KEY, UPSIDE_SECRET_KEY, and UPSIDE_PASSPHRASE must never be included in frontend bundles, iframe config, public environment variables, analytics payloads, screenshots, or logs.
Session State
Use an internal session table or durable object keyed by sessionId.
| State | Meaning | Allowed Next Step |
|---|---|---|
created | Player clicked play; server generated a session ID. | placeBet |
bet_placed | Upside spent WIN and returned a session. | Resolve outcome |
settling | Backend is calling processPayout. | Retry or reconcile |
settled | Payout completed or loss recorded. | Return result to iframe |
failed | Bet or payout could not complete. | Show error, retry safely, or support review |
Idempotency
placeBet rejects a session id that has already been used for a bet. processPayout rejects a session id that has already been settled. Keep a clear local state transition so retry workers know whether to retry the same operation, reconcile with getHistory, or stop.
tsasync function runOnce<T>(key: string, fn: () => Promise<T>) { const existing = await db.operations.get(key); if (existing?.status === "done") return existing.result as T; await db.operations.put(key, { status: "running", startedAt: Date.now() }); try { const result = await fn(); await db.operations.put(key, { status: "done", result, finishedAt: Date.now() }); return result; } catch (error) { await db.operations.put(key, { status: "failed", error: error instanceof Error ? error.message : String(error), finishedAt: Date.now(), }); throw error; }}
Fairness
For deterministic or high-value games, store enough data to reproduce or verify the outcome:
- Server seed hash or commitment before play.
- Client seed or player choice.
- Random draw source and draw index.
- Prize table version or card shoe version.
- Bet amount, payout formula, and resulting payout.
- Session ID and game ID returned by
placeBet.
CORS And Framing
Your game URL must be frameable by Upside. Avoid X-Frame-Options: DENY and use a Content-Security-Policy frame-ancestors directive that allows Upside.
httpContent-Security-Policy: frame-ancestors https://upside.win https://www.upside.win;
Your backend should accept requests from your iframe origin, not from arbitrary origins. If the frontend and backend are on the same origin, keep CORS closed.
Logging
Log operational identifiers, not secrets.
Good fields:
sessionIdgameIdgameTypeuserIdor stable internal player IDtriggerbetAmountpayoutAmountresult- HTTP status and Upside request latency
Do not log:
B3-JWTUPSIDE_SECRET_KEYUPSIDE_PASSPHRASE- Full HMAC prehash strings
- Raw Authorization headers
Launch Checklist
- Game has an approved
gameType, display name, and productionworkerUrl. - HMAC credentials are scoped to the approved game and stored in server-side secrets.
- Iframe verifies parent origin and sends
readyafter its listener is mounted. - Backend validates all player inputs before
placeBet. - Backend uses
BigIntor a decimal library for all WIN math. - Every game session has a unique server-generated
sessionId. processPayoutis called once per placed bet, including losses withpayoutAmount: "0".- Frontend disables repeat clicks while settlement is pending.
- Parent balance and match history refresh only after successful settlement.
- Logs contain enough session data for support without exposing secrets.
Incident Response
Keep the session in bet_placed locally. Let the player resume the round, or run a backend reconciliation job that resolves and settles the session.
Do not immediately assume failure. Check your local operation state and call getHistory or retry with the same sessionId. If Upside says the payout was already processed, reconcile as settled.
Confirm the backend settlement succeeded, then have the iframe post refetchBalance. If the session is settled but balance remains stale, capture session ID and user ID for support.
Check server clock drift, request path, raw JSON body string, timestamp window, and whether the B3-JWT used for signing matches the B3-JWT header.