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.

text
POST https://api.b3.fun/upside/game-actions

Authentication Headers

HeaderRequiredDescription
Content-TypeYesMust be application/json.
UPSIDE-ACCESS-KEYYesAPI key issued for your game worker.
UPSIDE-ACCESS-SIGNYesBase64 HMAC-SHA256 signature.
UPSIDE-ACCESS-TIMESTAMPYesISO timestamp. Requests outside the replay window are rejected.
UPSIDE-ACCESS-PASSPHRASEYesPassphrase configured when the key was created.
B3-JWTYesPlayer 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

text
prehash = timestamp + method + requestPath + rawBody + b3Jwtsignature = base64(hmac_sha256(prehash, secretKey))

Example:

ts
const 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

triggerstringrequiredpath

Action to execute. Supported values are checkAuth, getUser, getBalance, getMaxBet, placeBet, processPayout, getActive, and getHistory.

gameTypestringpath

Approved game identifier such as spinwheel, giftbox, or your assigned game slug. Required for getMaxBet, placeBet, processPayout, and game-filtered history reads.

sessionIdstringpath

Session idempotency key. Required for processPayout; optional for placeBet, where Upside can generate one if omitted. Server-generated IDs are recommended.

gameIdstringpath

Game instance identifier. Optional for placeBet; generated if omitted.

betAmountstringpath

Atomic WIN amount to spend. Required for placeBet. WIN uses 18 decimals.

payoutAmountstringpath

Atomic WIN amount to credit. Required for processPayout. Use "0" for losses.

result'win' | 'loss' | 'push' | 'blackjack'path

Final result classification for processPayout.

gameDataunknownpath

Structured outcome metadata for history and audits. Keep it JSON-serializable.

noncestringpath

Optional client or server nonce for tracing.

thumbnailUrlstringpath

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.

ts
import { parseUnits, formatUnits } from "viem";const oneWin = parseUnits("1", 18).toString();const display = formatUnits(BigInt(oneWin), 18);

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.

ts
if (!response.ok) { const errorText = await response.text(); throw new Error(`Upside game action failed: ${response.status} ${errorText}`);}
Ask a question... ⌘I