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:

text
POST 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 trigger and trigger-specific fields.

Signing Helper

The signature prehash is:

text
timestamp + method + requestPath + rawRequestBody + b3Jwt

method must be uppercase. requestPath should be /upside/game-actions. rawRequestBody must be exactly the string sent over the network.

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

ts
import { 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

1

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.

2

Place the bet

Call trigger: "placeBet" before revealing or resolving the outcome. Upside spends WIN atomically and creates the session.

3

Run game logic

Resolve randomness, cards, reels, prize tables, or other game state on the backend. Persist enough data to audit the outcome later.

4

Process payout

Call trigger: "processPayout" once per session. Send payoutAmount: "0" for losses and include structured gameData for history.

5

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

ts
async 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 sessionId server-side before placeBet and 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 getActive and your own session table.
  • Add worker-level rate limits by user and by game action in addition to Upside API key limits.

Next Steps

Game Actions API

See every supported trigger and request field.

Learn More
Security & Ops

Review launch checks for auth, idempotency, CORS, fairness, and monitoring.

Learn More
Ask a question... ⌘I