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

# Game Actions API

Reference for the signed Upside game-actions endpoint used by iframe game backends.

# Game Actions API

`/upside/game-actions` is the backend API used by approved game workers to read player state, spend WIN for bets, settle payouts, and fetch active/history records.

```text
POST https://api.b3.fun/upside/game-actions
```

## Authentication Headers

| Header | Required | Description |
| --- | --- | --- |
| `Content-Type` | Yes | Must be `application/json`. |
| `UPSIDE-ACCESS-KEY` | Yes | API key issued for your game worker. |
| `UPSIDE-ACCESS-SIGN` | Yes | Base64 HMAC-SHA256 signature. |
| `UPSIDE-ACCESS-TIMESTAMP` | Yes | ISO timestamp. Requests outside the replay window are rejected. |
| `UPSIDE-ACCESS-PASSPHRASE` | Yes | Passphrase configured when the key was created. |
| `B3-JWT` | Yes | Player JWT received by your iframe from Upside. It is included in the signature payload. |

**Raw body must match**

Compute the signature over the exact raw JSON string that you send as the request body. If you stringify once for signing and a different time for sending, signatures can fail.

## Signature Formula

```text
prehash = timestamp + method + requestPath + rawBody + b3Jwt
signature = base64(hmac_sha256(prehash, secretKey))
```

Example:

```ts
const timestamp = new Date().toISOString();
const method = "POST";
const path = "/upside/game-actions";
const rawBody = JSON.stringify({ trigger: "getBalance" });
const prehash = `${timestamp}${method}${path}${rawBody}${b3Jwt}`;
```

## Shared Fields

Action to execute. Supported values are `checkAuth`, `getUser`, `getBalance`, `getMaxBet`, `placeBet`, `processPayout`, `getActive`, and `getHistory`.

Approved game identifier such as `spinwheel`, `giftbox`, or your assigned game slug. Required for `getMaxBet`, `placeBet`, `processPayout`, and game-filtered history reads.

Session idempotency key. Required for `processPayout`; optional for `placeBet`, where Upside can generate one if omitted. Server-generated IDs are recommended.

Game instance identifier. Optional for `placeBet`; generated if omitted.

Atomic WIN amount to spend. Required for `placeBet`. WIN uses 18 decimals.

Atomic WIN amount to credit. Required for `processPayout`. Use `"0"` for losses.

Final result classification for `processPayout`.

Structured outcome metadata for history and audits. Keep it JSON-serializable.

Optional client or server nonce for tracing.

Optional URI used for session or activity presentation.

## Triggers

### checkAuth

Verifies the HMAC credentials and nested player JWT.

```json
{
  "trigger": "checkAuth"
}
```

Returns:

```json
{
  "success": true,
  "trigger": "checkAuth",
  "authenticated": true,
  "userId": "..."
}
```

### getUser

Returns the authenticated player identity and linked addresses.

```json
{
  "trigger": "getUser"
}
```

### getBalance

Returns the aggregate player WIN balance across active WIN pools.

```json
{
  "trigger": "getBalance"
}
```

### getMaxBet

Returns the maximum currently allowed bet for a game. The service calculates this from the game pool balance.

```json
{
  "trigger": "getMaxBet",
  "gameType": "coin-flip"
}
```

Returns:

```json
{
  "success": true,
  "trigger": "getMaxBet",
  "gameType": "coin-flip",
  "maxBetAmount": "2500000000000000000000",
  "maxBetAmountFormatted": "2500"
}
```

### placeBet

Spends the player's WIN and creates an active game session. The session id is also the idempotency key for the bet.

```json
{
  "trigger": "placeBet",
  "gameType": "coin-flip",
  "sessionId": "7b65f0bb-69d3-4392-9df0-34700cf5b962",
  "gameId": "coin-flip-round-1001",
  "betAmount": "1000000000000000000",
  "thumbnailUrl": "https://cdn.example.com/coin-flip.png"
}
```

Returns:

```json
{
  "success": true,
  "trigger": "placeBet",
  "sessionId": "7b65f0bb-69d3-4392-9df0-34700cf5b962",
  "gameId": "coin-flip-round-1001",
  "userBalance": "99000000000000000000",
  "userBalanceFormatted": "99"
}
```

Expected failures include:

- Game type does not exist.
- Game is not active.
- Bet amount exceeds the current max bet.
- Player has insufficient WIN.
- Session was already used for a bet.

### processPayout

Completes the session and credits any payout. Call it exactly once per placed bet.

```json
{
  "trigger": "processPayout",
  "gameType": "coin-flip",
  "sessionId": "7b65f0bb-69d3-4392-9df0-34700cf5b962",
  "payoutAmount": "2000000000000000000",
  "result": "win",
  "gameData": {
    "prediction": "heads",
    "result": "heads",
    "rngCommitment": "round-1001:..."
  }
}
```

Returns:

```json
{
  "success": true,
  "trigger": "processPayout",
  "result": "win",
  "userBalance": "101000000000000000000",
  "userBalanceFormatted": "101"
}
```

Expected failures include:

- Session not found.
- Session belongs to a different player.
- Session already finished.
- Payout already processed.
- No active WIN pool is available.

### getActive

Returns active sessions for the authenticated player, optionally filtered by `gameType`.

```json
{
  "trigger": "getActive",
  "gameType": "coin-flip"
}
```

### getHistory

Returns recent finished sessions for the authenticated player, optionally filtered by `gameType`.

```json
{
  "trigger": "getHistory",
  "gameType": "coin-flip"
}
```

The response includes `sessions`, `total`, and aggregate `stats`.

## Amount Units

WIN amounts are strings in atomic units with 18 decimals.

**TypeScript**

```ts
import { parseUnits, formatUnits } from "viem";

const oneWin = parseUnits("1", 18).toString();
const display = formatUnits(BigInt(oneWin), 18);
```
  **No dependency**

```ts
const ONE_WIN = 1_000_000_000_000_000_000n;
const betAmount = (10n * ONE_WIN).toString();
```

## API Key Scope

API keys are linked to approved Upside game IDs and can have rate limits, expiration, and optional IP allowlists. The secret key is only shown at creation time, so store it in your secret manager immediately.

## Error Response Handling

Treat non-2xx responses as failed game actions. Do not show a win/loss modal until `processPayout` succeeds or you have reconciled the session through your own state and `getHistory`.

```ts
if (!response.ok) {
  const errorText = await response.text();
  throw new Error(`Upside game action failed: ${response.status} ${errorText}`);
}
```