Quickstart

This guide builds the smallest complete integration: an iframe frontend, a backend route, and two signed game-actions calls. Use it to prove the flow before you invest in richer game UI.

HMAC API keys, secret keys, and passphrases belong only in your backend or worker environment. The iframe frontend should only receive the player JWT that Upside sends through postMessage.

Prerequisites

  • An approved Upside game record with a stable gameType such as coin-flip.
  • HMAC credentials from the B3 team: UPSIDE_API_KEY, UPSIDE_SECRET_KEY, and UPSIDE_PASSPHRASE.
  • A frontend route that can run in an iframe.
  • A backend route that can receive the player JWT from the frontend and sign requests to https://api.b3.fun.

1. Add the iframe bridge

Your iframe should announce readiness, accept trusted parent messages, and store the player context.

tsx
import { useEffect, useState } from "react";const UPSIDE_ORIGINS = new Set(["https://upside.win", "https://www.upside.win"]);type UpsideContext = { balance: string; token: string | null; locale: string;};export function useUpsideBridge() { const [context, setContext] = useState<UpsideContext>({ balance: "0", token: null, locale: "en", }); useEffect(() => { const sendReady = () => { for (const origin of UPSIDE_ORIGINS) { window.parent.postMessage({ type: "ready" }, origin); } }; const onMessage = (event: MessageEvent) => { if (!UPSIDE_ORIGINS.has(event.origin)) return; const data = event.data ?? {}; if (typeof data.token === "string" || typeof data.balance === "string" || typeof data.locale === "string") { setContext(current => ({ balance: typeof data.balance === "string" ? data.balance : current.balance, token: typeof data.token === "string" ? data.token : current.token, locale: typeof data.locale === "string" ? data.locale : current.locale, })); } if (data.type === "localeChange" && typeof data.locale === "string") { setContext(current => ({ ...current, locale: data.locale })); } }; window.addEventListener("message", onMessage); sendReady(); return () => window.removeEventListener("message", onMessage); }, []); return context;}

2. Call your backend with the player token

tsx
import { useState } from "react";import { useUpsideBridge } from "./useUpsideBridge";const ONE_WIN = "1000000000000000000";export function CoinFlipGame() { const { token, balance, locale } = useUpsideBridge(); const [busy, setBusy] = useState(false); async function play(prediction: "heads" | "tails") { if (!token || busy) return; setBusy(true); const response = await fetch("/api/play/coin-flip", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ prediction, betAmount: ONE_WIN }), }); const result = await response.json(); window.parent.postMessage( result.outcome === "win" ? { type: "showWinModal", wins: result.payoutAmount } : { type: "showLossModal", loss: result.betAmount }, "https://upside.win", ); window.parent.postMessage({ type: "refetchBalance" }, "https://upside.win"); window.parent.postMessage({ type: "refreshMatchHistory" }, "https://upside.win"); setBusy(false); } return ( <main> <p>Locale: {locale}</p> <p>WIN balance in wei: {balance}</p> <button disabled={!token || busy} onClick={() => play("heads")}>Heads</button> <button disabled={!token || busy} onClick={() => play("tails")}>Tails</button> </main> );}

3. Sign backend requests

Install Hono if you do not already have a worker backend.

bash
pnpm add hono

Add the required environment variables to your backend runtime.

bash
UPSIDE_API_KEY=upside_live_...UPSIDE_SECRET_KEY=...UPSIDE_PASSPHRASE=...UPSIDE_API_BASE_URL=https://api.b3.fun

Then place the bet, run the game, and process the payout from the backend.

ts
import { Hono } from "hono";type Env = { UPSIDE_API_KEY: string; UPSIDE_SECRET_KEY: string; UPSIDE_PASSPHRASE: string; UPSIDE_API_BASE_URL?: string;};const app = new Hono<{ Bindings: Env }>();const GAME_TYPE = "coin-flip";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);}async function gameAction(env: Env, b3Jwt: string, body: Record<string, unknown>) { const path = "/upside/game-actions"; const method = "POST"; const timestamp = new Date().toISOString(); const requestBody = JSON.stringify(body); const prehash = `${timestamp}${method}${path}${requestBody}${b3Jwt}`; const signature = await hmacSha256Base64(env.UPSIDE_SECRET_KEY, prehash); 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: requestBody, }); if (!response.ok) { throw new Error(`Upside API ${response.status}: ${await response.text()}`); } return response.json();}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 player token" }, 401); const { prediction, betAmount } = await c.req.json<{ prediction: "heads" | "tails"; betAmount: string }>(); const sessionId = crypto.randomUUID(); const gameId = crypto.randomUUID(); const bet = await gameAction(c.env, b3Jwt, { trigger: "placeBet", gameType: GAME_TYPE, sessionId, gameId, betAmount, }); const result = Math.random() < 0.5 ? "heads" : "tails"; const isWin = prediction === result; const payoutAmount = isWin ? ((BigInt(betAmount) * 2n).toString()) : "0"; await gameAction(c.env, b3Jwt, { trigger: "processPayout", gameType: GAME_TYPE, sessionId: bet.sessionId, payoutAmount, result: isWin ? "win" : "loss", gameData: { prediction, result }, }); return c.json({ sessionId: bet.sessionId, betAmount, payoutAmount, outcome: isWin ? "win" : "loss", result, });});export default app;

4. Test in Upside

Encode your local game URL and open the test route.

bash
node -e 'console.log(Buffer.from("http://localhost:3000").toString("base64"))'

Open:

text
https://upside.win/test/games/aHR0cDovL2xvY2FsaG9zdDozMDAw

5. Launch checklist

  • Frontend verifies the parent origin before accepting token or balance messages.
  • Backend signs timestamp + method + path + rawBody + B3-JWT exactly.
  • Every bet uses a unique sessionId and one payout attempt path.
  • Game outcome logic runs only on the backend.
  • The iframe posts refetchBalance and refreshMatchHistory after settlement.

Next Steps

Iframe Protocol

See every supported parent-child message.

Learn More
Game Actions API

Review headers, triggers, fields, and response shapes.

Learn More
Ask a question... ⌘I