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.

Upside game discovery and launch surface

Threat Model

SurfaceRiskControl
Iframe messagesToken spoofing or data injectionVerify event.origin and use explicit targetOrigin.
Browser game stateModified client code or replayed requestsTreat frontend state as display/input only.
Backend credentialsAPI key exposureStore HMAC credentials only in server-side secrets.
SettlementDuplicate bets or duplicate payoutsUse server-generated sessionId and idempotent state transitions.
RandomnessPredictable outcomesGenerate outcomes server-side and store audit metadata.
Balance UIStale balance after settlementPost 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.

StateMeaningAllowed Next Step
createdPlayer clicked play; server generated a session ID.placeBet
bet_placedUpside spent WIN and returned a session.Resolve outcome
settlingBackend is calling processPayout.Retry or reconcile
settledPayout completed or loss recorded.Return result to iframe
failedBet 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.

ts
async 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.

http
Content-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:

  • sessionId
  • gameId
  • gameType
  • userId or stable internal player ID
  • trigger
  • betAmount
  • payoutAmount
  • result
  • HTTP status and Upside request latency

Do not log:

  • B3-JWT
  • UPSIDE_SECRET_KEY
  • UPSIDE_PASSPHRASE
  • Full HMAC prehash strings
  • Raw Authorization headers

Launch Checklist

  • Game has an approved gameType, display name, and production workerUrl.
  • HMAC credentials are scoped to the approved game and stored in server-side secrets.
  • Iframe verifies parent origin and sends ready after its listener is mounted.
  • Backend validates all player inputs before placeBet.
  • Backend uses BigInt or a decimal library for all WIN math.
  • Every game session has a unique server-generated sessionId.
  • processPayout is called once per placed bet, including losses with payoutAmount: "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.

Ask a question... ⌘I