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

# Backend Integration

Sign game-actions requests, place WIN bets, run server-side game logic, and settle payouts from your backend.

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

**Two layers of authentication**

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 upside-client.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 index.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

**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.
  **Place the bet**

Call `trigger: "placeBet"` before revealing or resolving the outcome. Upside spends WIN atomically and creates the session.
  **Run game logic**

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

Call `trigger: "processPayout"` once per session. Send `payoutAmount: "0"` for losses and include structured `gameData` for history.
  **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](/upside/game-actions-api)**

See every supported trigger and request field.
  **[Security & Ops](/upside/security-and-ops)**

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