# Apogee Studios — Operator Integration Guide

**Version:** 2.1 · **Last updated:** 2026-04-13 · **Contact:** partners@apogeetech.net

This is the integration guide for **operators** (casinos, sportsbooks, sweepstakes platforms) who want to offer Apogee Studios games to their players.

You don't host anything. You don't deploy anything. Apogee Studios runs the games; you **embed them** in your site and connect your wallet to our API. You're billed monthly on GGR.

If you want a game on screen in 5 minutes, read [QUICKSTART.md](./QUICKSTART.md).

---

## 1. How it works

```
 ┌─────────────┐   1. POST /v1/sessions       ┌──────────────┐
 │             │ ────────────────────────▶    │              │
 │   Your      │                               │    Apogee    │
 │   platform  │ ◀── 2. launchUrl token ────── │    Studios   │
 │             │                               │              │
 │             │    3. player loads iframe     │   ┌────────┐ │
 │             │ ◀──────────────────────────── │   │Skyward │ │
 │             │                               │   │Thermal │ │
 │             │ ◀── 4. signed wallet calls ── │   │…       │ │
 │             │     (balance / debit /        │   └────────┘ │
 │             │      credit / rollback)       │              │
 │             │                               │              │
 │             │ ◀── 5. webhook events ─────── │              │
 └─────────────┘                               └──────────────┘
```

**Your responsibilities:**
1. Call `POST /v1/sessions` from your backend when a player opens a game.
2. Serve the returned `launchUrl` in an iframe.
3. Implement four HTTP endpoints on your side: `/wallet/balance`, `/wallet/debit`, `/wallet/credit`, `/wallet/rollback`. Apogee calls them, signed with HMAC-SHA256. Your responses are the source of truth for the player's balance.
4. (Optional) Receive webhook events for analytics and leaderboards.

**Apogee's responsibilities:**
1. Host the games at `https://apogeetech.net` (publicly accessible to your players).
2. Authenticate your requests against your API keys.
3. Call your wallet endpoints on every bet and every payout.
4. Generate a monthly invoice based on GGR × your fee rate.

You never deploy Apogee code. There is nothing to host on your side except the iframe and the four wallet endpoints.

---

## 2. Onboarding — what happens before you write a line of code

1. **You contact partners@apogeetech.net** with your operator name, website, target markets, and expected volume.
2. **We create a merchant record** for you in the Apogee admin panel. You get a **merchant ID** like `mrc_8a3f2bc1`.
3. **We issue you two API key pairs:**
   - **Sandbox** — `pk_test_apogee_…` / `sk_test_apogee_…` for development
   - **Live** — `pk_live_apogee_…` / `sk_live_apogee_…` for production
4. **We agree on a fee rate** (% of GGR) and it's stored on your merchant record.
5. **We allowlist your IPs** if you want IP-based restrictions on top of HMAC signing.
6. **You receive a welcome email** with the keys, your merchant ID, and a link to this doc.

After that, you can start building immediately against the sandbox.

---

## 3. Live URLs

| Service | URL | Who uses it |
|---|---|---|
| 🎮 Game frontend | **https://apogeetech.net** | Your players (via iframe) |
| 🎮 Game frontend (direct Cloud Run) | https://apogee-web-634190752875.europe-west1.run.app | Fallback, same content |
| 🔧 Sandbox API | `https://sandbox-api.apogeetech.net` | Your backend (test keys) |
| 🔧 Live API | `https://api.apogeetech.net` | Your backend (live keys) |
| 🔒 Apogee admin panel | https://admin.apogeetech.net | **Apogee staff only** — you do NOT access this |

You never need to visit the admin URL. That's where Apogee staff manage merchants, keys, and billing.

---

## 4. Authentication — HMAC-SHA256 signing

Every request you send to Apogee must be signed. Every request Apogee sends to you will be signed. We use HMAC-SHA256 over a canonical message.

### Canonical message (newline-delimited)
```
{HTTP_METHOD}\n
{REQUEST_PATH}\n
{TIMESTAMP}\n
{NONCE}\n
{BODY_SHA256_HEX}
```

### Required headers
```
X-Apogee-Key:       pk_test_apogee_…            # your public key
X-Apogee-Timestamp: 1712899200                  # unix seconds
X-Apogee-Nonce:     9f3a…c12                    # 16 random bytes, hex
X-Apogee-Signature: hex(hmac_sha256(secret, canonical_message))
Content-Type:       application/json
```

### Validation rules (you must enforce these on incoming wallet calls)
1. Reject requests where `|now - timestamp| > 60s` — prevents replay attacks from stale captures.
2. Reject requests where `nonce` has been seen in the last 5 minutes — prevents replay within the timestamp window. Store nonces in Redis with 300s TTL.
3. Use **constant-time comparison** on the signature.
4. Reject unknown or revoked keys.

### Node.js signer
```js
import crypto from "node:crypto";

export function signApogeeRequest({ method, path, body, secret }) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = crypto.randomBytes(16).toString("hex");
  const bodyStr = body ? JSON.stringify(body) : "";
  const bodyHash = crypto.createHash("sha256").update(bodyStr).digest("hex");
  const canonical = [method, path, timestamp, nonce, bodyHash].join("\n");
  const signature = crypto.createHmac("sha256", secret).update(canonical).digest("hex");
  return { timestamp, nonce, signature, bodyStr };
}
```

### Python signer
```python
import hashlib, hmac, json, secrets, time

def sign_apogee(method, path, body, secret):
    ts = str(int(time.time()))
    nonce = secrets.token_hex(16)
    body_str = json.dumps(body, separators=(",", ":")) if body is not None else ""
    body_hash = hashlib.sha256(body_str.encode()).hexdigest()
    canonical = "\n".join([method, path, ts, nonce, body_hash])
    sig = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    return ts, nonce, sig, body_str
```

### PHP signer
```php
function apogee_sign($method, $path, $body, $secret) {
    $ts = (string) time();
    $nonce = bin2hex(random_bytes(16));
    $bodyStr = $body ? json_encode($body, JSON_UNESCAPED_SLASHES) : "";
    $bodyHash = hash("sha256", $bodyStr);
    $canonical = implode("\n", [$method, $path, $ts, $nonce, $bodyHash]);
    $sig = hash_hmac("sha256", $canonical, $secret);
    return compact("ts", "nonce", "sig", "bodyStr");
}
```

---

## 5. Launching a game

### Step 1 — Create a session (server-to-server)

`POST https://sandbox-api.apogeetech.net/v1/sessions`

```json
{
  "gameId": "skyward",
  "playerId": "p_1234",
  "playerName": "anon",
  "currency": "EUR",
  "balance": 10000,
  "language": "en",
  "returnUrl": "https://yourcasino.example/lobby",
  "mode": "real",
  "country": "DE",
  "ipAddress": "203.0.113.5"
}
```

**Fields:**
| Field       | Type    | Required | Notes                                              |
|-------------|---------|----------|----------------------------------------------------|
| `gameId`    | string  | yes      | One of the IDs in [GAMES.md](./GAMES.md).          |
| `playerId`  | string  | yes      | Opaque. Max 64 chars.                              |
| `playerName`| string  | no       | Display name in chat/leaderboards.                 |
| `currency`  | string  | **yes**  | **ISO-4217 of the player's wallet**, e.g. `EUR`, `KES`, `ETB`, `SSP`, `ZMW`, `MAD`, `BTC`. The game will launch with this currency everywhere — bets, payouts, displayed balance, auto-cashout values. It's your responsibility to pass the currency that matches the player's wallet; Apogee does not convert. |
| `balance`   | integer | yes      | Minor units of the above currency (cents, sats).   |
| `language`  | string  | no       | BCP-47. Default `en`.                              |
| `returnUrl` | string  | no       | Where to send player on exit.                      |
| `mode`      | enum    | no       | `real` · `demo` · `fun`. Default `real`.           |
| `country`   | string  | no       | ISO-3166.                                          |
| `ipAddress` | string  | no       | For fraud/geo.                                     |

**Currency matching is critical.** The `currency` you pass in the session request tells the game which currency to display and which unit to use when calling your wallet. If your player's wallet is in `KES`, pass `"currency": "KES"` and `balance` in cents (1 KES = 100 cents). The game will render `KSh 1,234.56`, bet in KES, and call your `/wallet/debit` with KES amounts. The game never converts between currencies — the currency on the session is the currency of the whole player session.

**Supported currencies** (as of this version):
- **Fiat major:** EUR, USD, GBP, JPY, CHF, CAD, AUD, NZD, SEK, NOK, DKK, PLN, CZK, HUF, RON, TRY
- **Africa:** KES (Kenyan Shilling), ETB (Ethiopian Birr), SSP (South Sudanese Pound), SOS (Somali Shilling), SLS (Somaliland Shilling), ZMW (Zambian Kwacha), DJF (Djiboutian Franc), MAD (Moroccan Dirham), ZAR, NGN, GHS, EGP
- **LATAM:** BRL, ARS, MXN, CLP, COP, PEN
- **Asia / MENA:** CNY, HKD, TWD, KRW, SGD, MYR, THB, IDR, PHP, VND, INR, PKR, BDT, LKR, NPR, AED, SAR, ILS
- **Crypto:** BTC, ETH, USDT, USDC, BNB, SOL, XRP, ADA, DOGE, TRX, LTC, BCH, DAI, MATIC, AVAX

Request a currency that isn't listed? Email partners@apogeetech.net — enabling a new currency is a config flip on our side, not a code change.

**Response `201 Created`:**
```json
{
  "sessionToken": "sess_01HXXXXXX",
  "launchUrl": "https://apogeetech.net/play.html?token=sess_01HXXXXXX",
  "expiresAt": "2026-04-11T12:30:00Z"
}
```

Tokens are valid for **30 minutes**, single-game. Create a fresh one per launch.

### Step 2 — Embed the iframe

```html
<iframe
  src="{{launchUrl}}"
  width="100%"
  height="720"
  allow="autoplay; fullscreen"
  referrerpolicy="strict-origin-when-cross-origin">
</iframe>
```

Recommended min size: 1024×640 on desktop. Mobile responsive down to 360×640.

### Step 3 — Your wallet gets called

While the player plays, Apogee calls your four wallet endpoints on every bet and payout. See §6 below.

### Step 4 — (Optional) Receive webhooks

Apogee posts round-level events to a webhook URL you configure in your merchant record. See [WEBHOOKS.md](./WEBHOOKS.md). Webhooks are optional — your wallet calls alone are enough to stay balanced.

### Gameplay note: opt-in re-entry after cashout

Skyward uses an **explicit opt-in** model for next-round participation. After a player cashes out (or loses), their bet button switches to `BET FOR NEXT ROUND` — the player is **not** automatically re-queued. They must tap the button to join round N+1, and can tap again to cancel before the next betting window closes.

Additional safeguards:
- **1-second cashout cooldown.** For 1 second immediately after cashout, the button is disabled and shows `✓ CASHED · just a moment…`. A rapid double-tap cannot accidentally queue the next round. Only a deliberate tap after the cooldown expires counts.
- **Loss has no cooldown** (no accidental-tap risk) but the same opt-in button appears.

From an integrator perspective this means:
- `/wallet/debit` is only called when the player has explicitly opted into a round. No surprise debits.
- A cashout and the next bet are two independent round-level events. If you're aggregating GGR per player, treat them as such.
- Auto-bet (martingale / paroli / stop-on-profit / stop-on-loss) is available to the player but **off by default**. When enabled, it chains automatically and will issue `/wallet/debit` calls on each new round until the stop condition fires.

This design is a deliberate responsible-gambling feature — it breaks the "one more round" reflex loop by making re-entry a conscious action. Documented in [GAMES.md](./GAMES.md#skyward--skyward).

### Embedding on your site

Every Apogee game has frame-ancestors `*` so you can iframe it from any `https://` origin.

**Strongly recommended — always take the iframe `src` from the `launchUrl` returned by `POST /v1/sessions` or `GET /v1/launch`.** Hardcoding a path is a footgun: each game ships its own HTML bundle (Skyward has `play.html`, Contrail has `contrail.html`, Apogee Hot 5 has `apogeehot5.html`) and pointing the wrong `gameId` at the wrong path loads the wrong game with broken state. The API takes care of this for you.

**Per-game launch paths** (authoritative table in [GAMES.md §Launch paths](./GAMES.md#launch-paths)):

| Game ID | Launch path |
|---|---|
| `skyward` | `/play.html?gameId=skyward` |
| `thermal` | `/thermal.html?gameId=thermal` |
| `contrail` | `/contrail.html` |
| `apogeehot5` | `/apogeehot5.html?gameId=apogeehot5` |
| `safariapogee` | `/safariapogee.html?gameId=safariapogee` |
| `foxyapogee` | `/foxyapogee.html?gameId=foxyapogee` |
| `hormuz` | `/hormuz.html?gameId=hormuz` |
| `apex` | `/apex.html?gameId=apex` |

Skyward embed:

```html
<iframe
  src="https://apogeetech.net/play.html?gameId=skyward&merchant=YOUR_MERCHANT_ID"
  width="100%"
  height="720"
  allow="autoplay; fullscreen"
  referrerpolicy="strict-origin-when-cross-origin"
  style="border:0; display:block;">
</iframe>
```

Contrail embed (note the different path):

```html
<iframe
  src="https://apogeetech.net/contrail.html?merchant=YOUR_MERCHANT_ID"
  width="100%"
  height="720"
  allow="autoplay; fullscreen"
  referrerpolicy="strict-origin-when-cross-origin"
  style="border:0; display:block;">
</iframe>
```

Apogee Hot 5 embed:

```html
<iframe
  src="https://apogeetech.net/apogeehot5.html?merchant=YOUR_MERCHANT_ID"
  width="100%"
  height="720"
  allow="autoplay; fullscreen"
  referrerpolicy="strict-origin-when-cross-origin"
  style="border:0; display:block;">
</iframe>
```

Required: replace `YOUR_MERCHANT_ID` with your merchant ID from onboarding (`mrc_...`). Every round your iframe plays will be tagged with that ID so it shows up in your Apogee admin dashboard under billing. If you omit `merchant`, rounds fall back to a demo merchant and won't be billable to you.

**If you're driving the iframe from a launch URL** (recommended), `POST /v1/sessions` returns `launchUrl` already pointing at the correct bundle + all session params. Your lobby code should look like:

```js
const resp = await fetch("https://api.apogeetech.net/v1/sessions", {
  method: "POST",
  headers: { /* ... signed headers ... */ },
  body: JSON.stringify({ gameId, playerId, currency, balance, returnUrl }),
});
const { launchUrl } = await resp.json();
document.getElementById("apogee-iframe").src = launchUrl;
// gameId=contrail → launchUrl = https://apogeetech.net/contrail.html?...
// gameId=skyward  → launchUrl = https://apogeetech.net/play.html?gameId=skyward&...
```

**Bare domain redirect** — for backwards compatibility with legacy lobby code that launches via `https://apogeetech.net/?gameId=<id>&...`, the landing page has an inline redirect that routes to the correct bundle based on `gameId`. This table must stay in sync with `apps/api/server.js::GAMES[].launchPath`. If you add a new game, update:
- `apps/api/server.js` (authoritative)
- `admin.js::GAME_LAUNCH_PATHS` (admin panel)
- `index.html` inline `LAUNCH` map (root-domain redirect)
- `docs/GAMES.md` Launch paths table

Full checklist in [GAMES.md §Adding a new game](./GAMES.md#adding-a-new-game--launch-path-checklist).

Recommended minimum iframe size: **1024×640** on desktop, fully responsive down to **360×640** on phones. No additional JavaScript wiring is required for the iframe to work — the game is entirely self-contained.

---

## 6. Wallet endpoint contract

You host four endpoints at a base URL you give Apogee during onboarding. Example: `https://yourcasino.example/apogee/wallet`.

### Request envelope (all four endpoints)
```json
{
  "sessionToken": "sess_01HXXXXXX",
  "playerId": "p_1234",
  "gameId": "skyward",
  "currency": "EUR",
  "roundId": "rnd_8fxk9",
  "refTxId": "tx_3bcd1",
  "txId": "tx_3bcd1.win",
  "requestId": "req_01HXYZ",
  "amount": 250
}
```

### Wire types — strict rules

All amounts are **integers in minor units**. Never floats.

All IDs are **JSON strings**. Never numbers. This applies to **every one** of:
`sessionToken`, `playerId`, `gameId`, `currency`, `roundId`, `txId`,
`refTxId`, `requestId`. If your unmarshal target is a strongly-typed
`string` field (Go, Rust, Kotlin, Swift, Java, C#, TypeScript with strict
mode), reject any non-string with `invalid_body` and call the support
channel — the client-side game may have regressed.

Apogee's own wire-forwarding layer (`apps/api/server.js`) coerces
`roundId` to `String(roundId)` regardless of what the game client sends,
so you will always receive a string even if the game evolves to use
integer round counters internally. **Do not rely on this coercion for
fields other than `roundId`** — operators have been observed receiving
integer `playerId` from legacy lobby integrations; always coerce defensively
on your side too.

See [INTEGRATION-PITFALLS.md §3b](./INTEGRATION-PITFALLS.md#3b-roundid--txid-wire-type-drift--fixed-2026-04-11) for the full incident postmortem.

### 6.1. Minor-unit contract — the #1 integration bug

Every numeric money field — `balance`, `amount`, and any field you return
in a response — MUST be an integer expressed in the **minor** unit of
the session's currency. This applies to:

* the `balance` you return from `/wallet/balance`
* the `balance` you return from `/wallet/debit`, `/credit`, `/rollback`
* the `amount` Apogee sends you in every request body

**Minor units by example:**

| Currency | Decimals | Major   | Minor (send this) |
|----------|---------:|--------:|------------------:|
| EUR      | 2        | €10.00  | `1000`            |
| USD      | 2        | $250.50 | `25050`           |
| ETB      | 2        | 42,994.70 Br | `4299470`    |
| KES      | 2        | KSh 500 | `50000`           |
| NGN      | 2        | ₦1,200  | `120000`          |
| JPY      | 0        | ¥10,000 | `10000`           |
| KRW      | 0        | ₩50,000 | `50000`           |
| UGX      | 0        | USh 3,000 | `3000`          |
| BTC      | 8        | ₿0.001  | `100000`          |
| USDT     | 2        | 100.00 USDT | `10000`       |

If your ledger stores balances as major-unit decimals (e.g. `42994.70`),
you **must** multiply by `10 ** decimals` and round to integer before
responding. Shipping floats will fail signature verification because
our body hash is computed over the raw bytes, and will fail validation
server-side with `bad_amount: amount must be an integer in currency
minor units`.

**Wrong:**
```json
{ "balance": 42994.70, "currency": "ETB" }
```
**Right:**
```json
{ "balance": 4299470, "currency": "ETB" }
```

**On the game client**: Apogee's game UI converts minor → major for
display (`4299470 cents → 42,994.70 Br`) and major → minor for wallet
calls (player types `10` → Apogee sends `amount: 1000`). As long as
your wallet speaks minor units end-to-end, the UI rendering is handled
automatically by `game.js::minorToMajor()` / `majorToMinor()`.

**Rounding rule**: use banker's rounding (round-half-to-even) if you
can, otherwise round-half-up. Never truncate — truncation biases
against the player on every transaction and compounds over millions
of rounds into a measurable house-edge artefact.

**Negative balance check**: your response balance must always be
`>= 0`. Apogee rejects negative balances with `502 operator_bad_response`
and terminates the round, forcing a rollback.

### `POST /wallet/balance`
Apogee asks for the player's current balance. No state change.

**Response 200:**
```json
{ "balance": 10000, "currency": "EUR" }
```

### `POST /wallet/debit`
Player placed a bet. **Must be idempotent on `txId`** — if you see the same `txId` twice, return the original response without debiting again.

**Response 200:**
```json
{ "balance": 9750, "currency": "EUR", "txId": "tx_3bcd1" }
```

**Errors (HTTP 4xx):**
| code                   | HTTP | when                                   |
|------------------------|------|----------------------------------------|
| `insufficient_funds`   | 402  | Player balance < amount.               |
| `player_locked`        | 403  | Account locked.                        |
| `currency_mismatch`    | 409  | Currency ≠ session.                    |
| `invalid_signature`    | 401  | Signature didn't verify.               |
| `session_expired`      | 401  | Session token expired.                 |
| `rate_limited`         | 429  | Your rate limit hit.                   |

### `POST /wallet/credit`
Player cashed out / settlement, OR a bet was cancelled mid-flight and needs
to be refunded. Idempotent on `txId`.

**Incoming fields you'll see:**
- `txId` — the credit's own transaction ID (always unique). Convention:
  cashouts use `<debit-txId>.win`, refunds use a fresh `ctx_...` ID.
- `refTxId` — points at the **original debit** that this credit refunds
  or settles. Operators MUST pair the two in their ledger via this
  field so the debit row is marked as settled/refunded.
- `reason` — one of `cashout`, `bet-cancelled`, `bet-cancelled-pending`,
  `bet-cancelled-in-flight`, `bet-window-closed`. Only `cashout` is a
  real payout; the others are refunds of a never-actually-played bet.
- `roundId` — the round the bet was placed in (always a string).

**Response 200:**
```json
{ "balance": 10750, "currency": "EUR", "txId": "tx_3bcd1.win" }
```

You can optionally echo `refTxId` in the response for operator-side
symmetry; Apogee doesn't require it on the wire but logs it in the
`transactions/{txId}` audit row if present.

### `POST /wallet/rollback`
Apogee voided a round or detected inconsistency. `txId` refers to the
original debit/credit via the `refTxId` field — if you've never seen
the referenced transaction, return **200 OK** with the current balance
(the rollback is a no-op). **Do not return 404.**

**Response 200:**
```json
{ "balance": 10000, "currency": "EUR", "txId": "tx_3bcd1" }
```

---

## 7. Testing

1. Use your sandbox key (`pk_test_apogee_…`) against `https://sandbox-api.apogeetech.net`.
2. Use player IDs prefixed `p_test_` for rate-unlimited synthetic accounts.
3. Sandbox state is wiped monthly. Live state is never touched.
4. Once your sandbox integration passes a 100-round smoke test with zero signature failures, notify partners@apogeetech.net and we'll flip your live keys on.

---

## 8. Go-live checklist

- [ ] Sandbox passes 100-round smoke test with zero signature failures
- [ ] Wallet endpoints return within **200ms p95** (seamless play requires low latency)
- [ ] Idempotency verified: replaying the same `txId` returns the original response
- [ ] Rollback handler tested for both "tx exists" and "tx never seen" paths
- [ ] Your webhook receiver (if used) tolerates out-of-order and duplicate events
- [ ] Secrets (`sk_live_*`) stored in a secrets manager — never in source control
- [ ] CSP allows Apogee:
  `frame-src 'self' https://apogeetech.net; connect-src 'self' https://api.apogeetech.net`
- [ ] Responsible-gambling limits (deposit / loss / session) enforced **in your wallet**, not by the game
- [ ] You've tested with real player IDs in the sandbox before requesting live keys

---

## 9. Demo mode

Every game supports a **demo mode** for showcasing games to prospects, trade shows, or internal testing without touching real wallets.

### Quick demo launch

The API exposes a convenience endpoint that mints a demo session and redirects straight to the game:

```
GET https://api.apogeetech.net/demo?gameId=foxyapogee
GET https://api.apogeetech.net/demo?gameId=skyward&currency=USD
```

**Parameters:**
| Param | Default | Notes |
|---|---|---|
| `gameId` | *(required)* | Any game ID from [GAMES.md](./GAMES.md) |
| `currency` | `EUR` | ISO-4217 code |

The endpoint mints a demo session with 10,000 balance (minor units), assigns a random `demo_*` player ID, and returns a `302` redirect through `/v1/launch`. The player lands directly in the game — no operator backend needed.

### DEMO badge

All games display a floating **DEMO** badge in the top-right corner when running in demo mode. The badge is always visible and pulsing so players (and regulators) can instantly distinguish demo sessions from real-money play.

Detection logic varies by game type:
- **Slots, crash (Skyward/Thermal), instant-win (Apex/Hormuz):** checks `WALLET_MODE !== "remote"` — any session without a real wallet connection shows the badge.
- **Contrail (multiplayer):** checks the session token — `demo` or absent tokens trigger the badge.

The badge is purely cosmetic and client-side. Server-side, demo sessions use the same provably-fair math and RNG as real sessions — the only difference is the wallet is local (no operator wallet calls).

### Embedding demos on your site

The landing page at `https://apogeetech.net` includes "Try Demo — Free Play" buttons for every game. You can also embed demo links in your own lobby:

```html
<a href="https://api.apogeetech.net/demo?gameId=foxyapogee" target="_blank">
  Try Foxy Apogee Demo
</a>
```

Or iframe it:
```html
<iframe src="https://api.apogeetech.net/demo?gameId=foxyapogee"
  width="100%" height="720" allow="autoplay; fullscreen">
</iframe>
```

The 302 redirect resolves to the correct game bundle automatically.

---

## 10. Billing

- Apogee charges a **percentage of GGR** (Gross Gaming Revenue = total wagered − total returned), agreed during onboarding.
- GGR is computed per calendar month, per merchant.
- Invoices are generated on the 1st of each month for the previous month.
- Round data is tagged with your merchant ID automatically — you don't need to report anything to us.
- Disputes: contact billing@apogeetech.net within 14 days of invoice.

---

## 11. Design rationale

**Why HMAC and not OAuth?** Symmetric keys avoid the token refresh dance. Every call is independently verifiable — no auth server to maintain. Same pattern Stripe, AWS, and most iGaming platforms use.

**Why timestamp + nonce?** Timestamp blocks replays older than clock-skew tolerance. Nonce blocks replays within that window. Together they let signed requests travel over plain HTTPS without extra session state.

**Why minor units?** Floats lose pennies at scale. Always integer cents/sats. End-to-end integer arithmetic.

**Why idempotency?** Networks retry. Without idempotency, retried debits double-charge players. With it, retries are harmless.

**Why webhooks are optional?** Your wallet calls are enough for a complete audit trail. Webhooks are for analytics, leaderboards, and responsible-gambling alerts — you don't need them to stay balanced.

**Why a seamless wallet?** Fewer sync bugs, no dual-ledger reconciliation, real-time balance updates across tabs. Industry default since ~2018.

**Why provably-fair commit-reveal?** SHA256 pre-image resistance guarantees the operator cannot re-roll an individual round after it completes. The committed `serverSeedHash` is published in the game's Fair panel BEFORE bets open; the seed is revealed AFTER the round. Any player can recompute the crash point and verify it matches. See [PROVABLY-FAIR.md](./PROVABLY-FAIR.md).

---

## 11.1. Verifying a crash round (operator + player)

Every round's crash point is derived deterministically from
`HMAC-SHA256(serverSeed, clientSeed + ":" + nonce)`. The `serverSeed` is
committed via `SHA256(serverSeed)` before the round and revealed after.

**Verify via our open endpoint:**
```bash
curl "https://api.apogeetech.net/v1/fair/verify?\
serverSeed=<revealed-seed>&clientSeed=apogee&nonce=<round-nonce>&instantDivisor=33"
```

Response contains the crash point, the raw HMAC, and both byte windows
so you can step through the math. Full Node / Python reference
implementations are in [PROVABLY-FAIR.md](./PROVABLY-FAIR.md). Operators
should hit this endpoint from their own audit tooling on a sample of
rounds daily.

---

## 12. Reference docs

**Integration**
- [QUICKSTART.md](./QUICKSTART.md) — 5-minute integration path
- [API.md](./API.md) — endpoint reference
- [WEBHOOKS.md](./WEBHOOKS.md) — webhook event catalog
- [GAMES.md](./GAMES.md) — game catalog
- [MERCHANTS.md](./MERCHANTS.md) — merchant lifecycle (onboarding, key rotation, suspension)

**Safety & fairness**
- [INTEGRATION-PITFALLS.md](./INTEGRATION-PITFALLS.md) — 31 common wallet-integration mistakes and how Apogee handles them (idempotency, rollback, race conditions, fingerprint binding, rate limits, `roundId` wire types, cross-game launch guards)
- [HOUSE-PROTECTION.md](./HOUSE-PROTECTION.md) — deep-dive on protecting the operator bankroll: tail-risk caps, aggregate exposure, martingale throttling, RTP canary, audit chain
- [PROVABLY-FAIR.md](./PROVABLY-FAIR.md) — player-facing fairness guarantees, commit-reveal scheme, `/v1/fair/verify` endpoint, Node/Python reference implementations
- [BET-PREFLIGHT.md](./BET-PREFLIGHT.md) — the 14-check preflight every game runs before a single byte reaches the wallet API; required for new-game ports
- [ERRORS.md](./ERRORS.md) — complete error code catalog with HTTP status matrix and client handling patterns

**Operations & contracts**
- [SLA.md](./SLA.md) — uptime commitment (99.95%), incident response windows, service credits
- [CERTIFICATIONS.md](./CERTIFICATIONS.md) — RNG certifications, jurisdictional coverage, operator-vs-Apogee responsibility split
- [VERSIONING.md](./VERSIONING.md) — API versioning and 180-day deprecation policy
- [SANDBOX.md](./SANDBOX.md) — test environment, test merchants, forced-outcome QA params

**Responsible gambling**
- [RESPONSIBLE-GAMBLING.md](./RESPONSIBLE-GAMBLING.md) — `/v1/players/:id/terminate` endpoint, `rgLimits` session metadata, per-game RG features, UKGC / GlüStV compliance

**Bonuses & promotions** (Planned Q3)
- [BONUSES.md](./BONUSES.md) — free spins, free bets, stake match, cashback, bonus wallet separation

**Machine-readable spec**
- [openapi.yaml](./openapi.yaml) — OpenAPI 3.1 spec for all endpoints. Feed into `openapi-generator-cli` to produce client libraries in any language.

Questions: **partners@apogeetech.net**
