> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upside.win/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

**Keep secrets server-side**

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 useUpsideBridge.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 Game.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.

**pnpm**

```bash
pnpm add hono
```
  **npm**

```bash
npm install hono
```
  **bun**

```bash
bun add hono
```

Add the required environment variables to your backend runtime.

```bash .dev.vars
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 worker.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](/upside/iframe-protocol)**

See every supported parent-child message.
  **[Game Actions API](/upside/game-actions-api)**

Review headers, triggers, fields, and response shapes.