Quickstart
Create a minimal Upside iframe game that receives a player token, places a WIN bet, and settles a payout.
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
gameTypesuch ascoin-flip. - HMAC credentials from the B3 team:
UPSIDE_API_KEY,UPSIDE_SECRET_KEY, andUPSIDE_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.
tsximport { 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
tsximport { 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.
bashpnpm add hono
bashnpm install hono
bashbun add hono
Add the required environment variables to your backend runtime.
bashUPSIDE_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.
tsimport { 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.
bashnode -e 'console.log(Buffer.from("http://localhost:3000").toString("base64"))'
Open:
texthttps://upside.win/test/games/aHR0cDovL2xvY2FsaG9zdDozMDAw
5. Launch checklist
- Frontend verifies the parent origin before accepting
tokenorbalancemessages. - Backend signs
timestamp + method + path + rawBody + B3-JWTexactly. - Every bet uses a unique
sessionIdand one payout attempt path. - Game outcome logic runs only on the backend.
- The iframe posts
refetchBalanceandrefreshMatchHistoryafter settlement.