> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upside.win/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

**Never expose worker credentials**

`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.

```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

**Bet succeeded but the iframe lost connection**

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.
  **Payout call timed out**

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.
  **Player reports stale balance**

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.
  **Signature failures started suddenly**

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.