Backend Integration
Sign game-actions requests, place WIN bets, run server-side game logic, and settle payouts from your backend.
Backend Integration
Your backend is the trusted boundary for an Upside game. It receives the player's JWT from your iframe, signs requests with your game HMAC credentials, places bets, runs game logic, stores session state, and settles payouts.
HMAC credentials authenticate your game worker. The B3-JWT header authenticates the player. Upside verifies both before a game-action is accepted.
Request Shape
All game actions use the same endpoint:
textPOST https://api.b3.fun/upside/game-actions
The request must include:
- HMAC headers proving the request came from your backend.
B3-JWT, the player JWT received by the iframe.- A JSON body with
triggerand trigger-specific fields.
Signing Helper
The signature prehash is:
texttimestamp + method + requestPath + rawRequestBody + b3Jwt
method must be uppercase. requestPath should be /upside/game-actions. rawRequestBody must be exactly the string sent over the network.
tstype UpsideEnv = { UPSIDE_API_KEY: string; UPSIDE_SECRET_KEY: string; UPSIDE_PASSPHRASE: string; UPSIDE_API_BASE_URL?: string;};async function hmacSha256Base64(secret: string, message: string) { const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message)); let binary = ""; for (const byte of new Uint8Array(signature)) binary += String.fromCharCode(byte); return btoa(binary);}export async function upsideGameAction<TResponse>( env: UpsideEnv, b3Jwt: string, body: Record<string, unknown>,): Promise<TResponse> { const path = "/upside/game-actions"; const method = "POST"; const timestamp = new Date().toISOString(); const rawBody = JSON.stringify(body); const signaturePayload = `${timestamp}${method}${path}${rawBody}${b3Jwt}`; const signature = await hmacSha256Base64(env.UPSIDE_SECRET_KEY, signaturePayload); const response = await fetch(`${env.UPSIDE_API_BASE_URL ?? "https://api.b3.fun"}${path}`, { method, headers: { "Content-Type": "application/json", "UPSIDE-ACCESS-KEY": env.UPSIDE_API_KEY, "UPSIDE-ACCESS-SIGN": signature, "UPSIDE-ACCESS-TIMESTAMP": timestamp, "UPSIDE-ACCESS-PASSPHRASE": env.UPSIDE_PASSPHRASE, "B3-JWT": b3Jwt, }, body: rawBody, }); if (!response.ok) { throw new Error(`Upside API ${response.status}: ${await response.text()}`); } return response.json() as Promise<TResponse>;}
Hono Worker Example
tsimport { Hono } from "hono";import { upsideGameAction } from "./upside-client";type Env = { UPSIDE_API_KEY: string; UPSIDE_SECRET_KEY: string; UPSIDE_PASSPHRASE: string; UPSIDE_API_BASE_URL?: string;};type PlaceBetResponse = { success: boolean; trigger: "placeBet"; sessionId: string; gameId: string; userBalance: string; userBalanceFormatted: string;};type ProcessPayoutResponse = { success: boolean; trigger: "processPayout"; result: "win" | "loss" | "push" | "blackjack"; userBalance: string; userBalanceFormatted: string;};const app = new Hono<{ Bindings: Env }>();const GAME_TYPE = "coin-flip";app.post("/api/play/coin-flip", async c => { const authHeader = c.req.header("Authorization"); const b3Jwt = authHeader?.replace(/^Bearer\s+/i, ""); if (!b3Jwt) { return c.json({ error: "Missing Authorization bearer token" }, 401); } const { prediction, betAmount } = await c.req.json<{ prediction: "heads" | "tails"; betAmount: string; }>(); if (!["heads", "tails"].includes(prediction)) { return c.json({ error: "Invalid prediction" }, 400); } const sessionId = crypto.randomUUID(); const gameId = crypto.randomUUID(); const bet = await upsideGameAction<PlaceBetResponse>(c.env, b3Jwt, { trigger: "placeBet", gameType: GAME_TYPE, sessionId, gameId, betAmount, thumbnailUrl: "https://cdn.example.com/coin-flip-thumbnail.png", }); const result = crypto.getRandomValues(new Uint8Array(1))[0] % 2 === 0 ? "heads" : "tails"; const isWin = prediction === result; const payoutAmount = isWin ? (BigInt(betAmount) * 2n).toString() : "0"; const payout = await upsideGameAction<ProcessPayoutResponse>(c.env, b3Jwt, { trigger: "processPayout", gameType: GAME_TYPE, sessionId: bet.sessionId, payoutAmount, result: isWin ? "win" : "loss", gameData: { prediction, result, betAmount, payoutAmount, }, }); return c.json({ sessionId: bet.sessionId, gameId: bet.gameId, prediction, result, outcome: isWin ? "win" : "loss", betAmount, payoutAmount, userBalance: payout.userBalance, userBalanceFormatted: payout.userBalanceFormatted, });});export default app;
Session Lifecycle
Validate the player request
Require Authorization: Bearer {token}, validate the player's bet amount, validate game inputs, and block duplicate in-flight actions from the same client.
Place the bet
Call trigger: "placeBet" before revealing or resolving the outcome. Upside spends WIN atomically and creates the session.
Run game logic
Resolve randomness, cards, reels, prize tables, or other game state on the backend. Persist enough data to audit the outcome later.
Process payout
Call trigger: "processPayout" once per session. Send payoutAmount: "0" for losses and include structured gameData for history.
Return result to iframe
Return a compact result payload to the iframe, then let the iframe ask Upside to show a modal and refresh balance/history.
Error Handling
tsasync function settleWithRetry(params: { env: Env; b3Jwt: string; gameType: string; sessionId: string; payoutAmount: string; result: "win" | "loss" | "push" | "blackjack"; gameData: Record<string, unknown>;}) { try { return await upsideGameAction(params.env, params.b3Jwt, { trigger: "processPayout", gameType: params.gameType, sessionId: params.sessionId, payoutAmount: params.payoutAmount, result: params.result, gameData: params.gameData, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (message.includes("already processed") || message.includes("already finished")) { return await upsideGameAction(params.env, params.b3Jwt, { trigger: "getHistory", gameType: params.gameType, }); } throw error; }}
Production Recommendations
- Generate
sessionIdserver-side beforeplaceBetand persist it with your game state. - Store raw bet, result, payout, player choice, and RNG/provability metadata where applicable.
- Disable the game UI while a bet is in flight.
- Use exact integer math with
BigInt; never use floating point for WIN amounts. - Reconcile unfinished sessions with
getActiveand your own session table. - Add worker-level rate limits by user and by game action in addition to Upside API key limits.