Game Actions API
Reference for the signed Upside game-actions endpoint used by iframe game backends.
Game Actions API
/upside/game-actions is the backend API used by approved game workers to read player state, spend WIN for bets, settle payouts, and fetch active/history records.
textPOST https://api.b3.fun/upside/game-actions
Authentication Headers
| Header | Required | Description |
|---|---|---|
Content-Type | Yes | Must be application/json. |
UPSIDE-ACCESS-KEY | Yes | API key issued for your game worker. |
UPSIDE-ACCESS-SIGN | Yes | Base64 HMAC-SHA256 signature. |
UPSIDE-ACCESS-TIMESTAMP | Yes | ISO timestamp. Requests outside the replay window are rejected. |
UPSIDE-ACCESS-PASSPHRASE | Yes | Passphrase configured when the key was created. |
B3-JWT | Yes | Player JWT received by your iframe from Upside. It is included in the signature payload. |
Compute the signature over the exact raw JSON string that you send as the request body. If you stringify once for signing and a different time for sending, signatures can fail.
Signature Formula
textprehash = timestamp + method + requestPath + rawBody + b3Jwtsignature = base64(hmac_sha256(prehash, secretKey))
Example:
tsconst timestamp = new Date().toISOString();const method = "POST";const path = "/upside/game-actions";const rawBody = JSON.stringify({ trigger: "getBalance" });const prehash = `${timestamp}${method}${path}${rawBody}${b3Jwt}`;
Shared Fields
Action to execute. Supported values are checkAuth, getUser, getBalance, getMaxBet, placeBet, processPayout, getActive, and getHistory.
Approved game identifier such as spinwheel, giftbox, or your assigned game slug. Required for getMaxBet, placeBet, processPayout, and game-filtered history reads.
Session idempotency key. Required for processPayout; optional for placeBet, where Upside can generate one if omitted. Server-generated IDs are recommended.
Game instance identifier. Optional for placeBet; generated if omitted.
Atomic WIN amount to spend. Required for placeBet. WIN uses 18 decimals.
Atomic WIN amount to credit. Required for processPayout. Use "0" for losses.
Final result classification for processPayout.
Structured outcome metadata for history and audits. Keep it JSON-serializable.
Optional client or server nonce for tracing.
Optional URI used for session or activity presentation.
Triggers
checkAuth
Verifies the HMAC credentials and nested player JWT.
json{ "trigger": "checkAuth"}
Returns:
json{ "success": true, "trigger": "checkAuth", "authenticated": true, "userId": "..."}
getUser
Returns the authenticated player identity and linked addresses.
json{ "trigger": "getUser"}
getBalance
Returns the aggregate player WIN balance across active WIN pools.
json{ "trigger": "getBalance"}
getMaxBet
Returns the maximum currently allowed bet for a game. The service calculates this from the game pool balance.
json{ "trigger": "getMaxBet", "gameType": "coin-flip"}
Returns:
json{ "success": true, "trigger": "getMaxBet", "gameType": "coin-flip", "maxBetAmount": "2500000000000000000000", "maxBetAmountFormatted": "2500"}
placeBet
Spends the player's WIN and creates an active game session. The session id is also the idempotency key for the bet.
json{ "trigger": "placeBet", "gameType": "coin-flip", "sessionId": "7b65f0bb-69d3-4392-9df0-34700cf5b962", "gameId": "coin-flip-round-1001", "betAmount": "1000000000000000000", "thumbnailUrl": "https://cdn.example.com/coin-flip.png"}
Returns:
json{ "success": true, "trigger": "placeBet", "sessionId": "7b65f0bb-69d3-4392-9df0-34700cf5b962", "gameId": "coin-flip-round-1001", "userBalance": "99000000000000000000", "userBalanceFormatted": "99"}
Expected failures include:
- Game type does not exist.
- Game is not active.
- Bet amount exceeds the current max bet.
- Player has insufficient WIN.
- Session was already used for a bet.
processPayout
Completes the session and credits any payout. Call it exactly once per placed bet.
json{ "trigger": "processPayout", "gameType": "coin-flip", "sessionId": "7b65f0bb-69d3-4392-9df0-34700cf5b962", "payoutAmount": "2000000000000000000", "result": "win", "gameData": { "prediction": "heads", "result": "heads", "rngCommitment": "round-1001:..." }}
Returns:
json{ "success": true, "trigger": "processPayout", "result": "win", "userBalance": "101000000000000000000", "userBalanceFormatted": "101"}
Expected failures include:
- Session not found.
- Session belongs to a different player.
- Session already finished.
- Payout already processed.
- No active WIN pool is available.
getActive
Returns active sessions for the authenticated player, optionally filtered by gameType.
json{ "trigger": "getActive", "gameType": "coin-flip"}
getHistory
Returns recent finished sessions for the authenticated player, optionally filtered by gameType.
json{ "trigger": "getHistory", "gameType": "coin-flip"}
The response includes sessions, total, and aggregate stats.
Amount Units
WIN amounts are strings in atomic units with 18 decimals.
tsimport { parseUnits, formatUnits } from "viem";const oneWin = parseUnits("1", 18).toString();const display = formatUnits(BigInt(oneWin), 18);
tsconst ONE_WIN = 1_000_000_000_000_000_000n;const betAmount = (10n * ONE_WIN).toString();
API Key Scope
API keys are linked to approved Upside game IDs and can have rate limits, expiration, and optional IP allowlists. The secret key is only shown at creation time, so store it in your secret manager immediately.
Error Response Handling
Treat non-2xx responses as failed game actions. Do not show a win/loss modal until processPayout succeeds or you have reconciled the session through your own state and getHistory.
tsif (!response.ok) { const errorText = await response.text(); throw new Error(`Upside game action failed: ${response.status} ${errorText}`);}