Frontend Integration

Your game frontend runs inside an Upside iframe. It should be a normal web app with one extra responsibility: exchange trusted postMessage events with the Upside parent shell.

Upside game shell around an iframe game

Responsibilities

Do

  • Listen for parent messages from https://upside.win.
  • Send the player JWT only to your own backend.
  • Let the backend decide outcomes and call Upside settlement APIs.
  • Ask the parent shell to refresh balance and match history after settlement.

Avoid

  • Putting HMAC credentials in browser code.
  • Trusting messages from unknown origins.
  • Deciding final game outcomes only on the client.
  • Rendering arbitrary modal HTML from untrusted user-generated content.

Parent Context

Upside sends the iframe a snapshot with:

FieldTypeMeaning
balancestringEstimated player WIN balance in atomic units. WIN uses 18 decimals.
tokenstringPlayer JWT. Forward this to your backend as Authorization: Bearer {token}.
localestringActive Upside locale, also appended to the iframe URL as ?locale=....

Upside also sends { type: "localeChange", locale } when the player changes language.

React Bridge Hook

tsx
import { useCallback, useEffect, useMemo, useState } from "react";const UPSIDE_ORIGINS = new Set(["https://upside.win", "https://www.upside.win"]);type UpsideContext = { balance: string; token: string | null; locale: string;};type ToastVariant = "success" | "error" | "warning" | "info";export function useUpsideBridge() { const [parentOrigin, setParentOrigin] = useState("https://upside.win"); const [context, setContext] = useState<UpsideContext>({ balance: "0", token: null, locale: "en", }); useEffect(() => { const onMessage = (event: MessageEvent) => { if (!UPSIDE_ORIGINS.has(event.origin)) return; setParentOrigin(event.origin); const data = event.data ?? {}; if (data.type === "localeChange" && typeof data.locale === "string") { setContext(current => ({ ...current, locale: data.locale })); return; } 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, })); }; window.addEventListener("message", onMessage); for (const origin of UPSIDE_ORIGINS) { window.parent.postMessage({ type: "ready" }, origin); } return () => window.removeEventListener("message", onMessage); }, []); const postToParent = useCallback( (payload: Record<string, unknown>) => { window.parent.postMessage(payload, parentOrigin); }, [parentOrigin], ); return useMemo( () => ({ ...context, showWinModal: (wins: string, htmlContent?: string) => postToParent({ type: "showWinModal", wins, htmlContent }), showLossModal: (loss: string, htmlContent?: string) => postToParent({ type: "showLossModal", loss, htmlContent }), showCustomModal: (htmlContent: string) => postToParent({ type: "showCustomModal", htmlContent }), showToast: (message: string, options?: { description?: string; duration?: number; variant?: ToastVariant }) => postToParent({ type: "showToast", message, ...options }), refetchBalance: () => postToParent({ type: "refetchBalance" }), refreshMatchHistory: () => postToParent({ type: "refreshMatchHistory" }), }), [context, postToParent], );}

Calling Your Backend

The iframe uses the player token only to call your game backend. The backend is responsible for signing Upside API requests.

tsx
import { useState } from "react";import { useUpsideBridge } from "./useUpsideBridge";const ONE_WIN = "1000000000000000000";export function PlayButton() { const upside = useUpsideBridge(); const [loading, setLoading] = useState(false); async function play(prediction: "heads" | "tails") { if (!upside.token || loading) return; setLoading(true); try { const response = await fetch("/api/play/coin-flip", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${upside.token}`, }, body: JSON.stringify({ prediction, betAmount: ONE_WIN }), }); if (!response.ok) throw new Error(await response.text()); const result = await response.json(); if (result.outcome === "win") { upside.showWinModal(result.payoutAmount); } else { upside.showLossModal(result.betAmount); } upside.refetchBalance(); upside.refreshMatchHistory(); } catch (error) { upside.showToast("Game failed", { description: error instanceof Error ? error.message : "Unknown error", variant: "error", }); } finally { setLoading(false); } } return ( <div> <p>WIN balance: {upside.balance}</p> <button disabled={!upside.token || loading} onClick={() => play("heads")}>Heads</button> <button disabled={!upside.token || loading} onClick={() => play("tails")}>Tails</button> </div> );}

Parent Commands

CommandPayloadEffect
ready{ type: "ready" }Tells Upside to resend balance, token, and locale.
showWinModal{ type, wins, htmlContent? }Opens the Upside win modal and refreshes match history.
showLossModal{ type, loss, htmlContent? }Opens the Upside loss modal and refreshes match history.
showCustomModal{ type, htmlContent }Opens a custom Upside modal. htmlContent must be a string.
showToast{ type, message, description?, duration?, variant? }Shows a parent-shell toast. Variant can be success, error, warning, or info.
refetchBalance{ type: "refetchBalance" }Asks Upside to reload the player's aggregate WIN balance.
refreshMatchHistory{ type: "refreshMatchHistory" }Asks Upside to reload recent sessions in the game sidebar.

Formatting WIN

Upside balances are atomic WIN strings with 18 decimals. Use viem or ethers for display formatting.

ts
import { formatUnits } from "viem";export function formatWin(amountWei: string) { return `${Number(formatUnits(BigInt(amountWei), 18)).toLocaleString()} WIN`;}

Accessibility Notes

  • Keep focus inside your iframe during gameplay.
  • Use the parent modal for settlement confirmation, but show the result inside your iframe as well.
  • Do not rely only on color for win/loss states.
  • Keep buttons disabled while a bet or payout is pending to avoid duplicate player actions.

Next Steps

Iframe Protocol

Review the exact message contract from both directions.

Learn More
Backend Integration

Wire the signed game-actions API from your game worker.

Learn More
Ask a question... ⌘I