# Provably-Fair — Apogee Crash Games

This is the player-facing promise. If any step is unverifiable, it isn't
provably fair.

## TL;DR

Apogee uses the **commit-reveal** scheme pioneered by Bustabit and now
standard across Stake, BC.Game, Roobet, Primedice, Duelbits, and every
eCOGRA-audited crash operator:

1. **BEFORE** the round starts, Apogee publishes `SHA256(serverSeed)` —
   the commitment.
2. **DURING** the round, the `serverSeed` is held only in memory and
   never logged.
3. **AFTER** the round ends, the raw `serverSeed` is revealed.
4. **YOU** can recompute the crash point with a one-line HMAC and confirm
   it matches what the game showed, and that `SHA256(revealedSeed)` matches
   the commitment.

If any of those four steps doesn't line up, the round is fraudulent.

## Why commit-reveal is the best-practice baseline

### The alternatives are all worse

| Scheme | Trust model | Weakness |
|---|---|---|
| Operator RNG, no commitment | Full trust in operator | Operator can re-roll any round |
| Published RNG output | Verify-after-the-fact only | Operator can still re-roll (just publishes whatever favors them) |
| **Commit-reveal** | Trust the hash function | None known — SHA256 is collision-resistant |
| External beacon (NIST) | Trust NIST | Slow (1 round per minute), centralized |
| Blockchain beacon | Trust a validator set | Slow, expensive, MEV leakage |

SHA256's pre-image resistance means that once Apogee publishes
`h = SHA256(seed)`, we cannot find a different `seed'` such that
`SHA256(seed') == h`. So we're locked in.

### The six properties a provably-fair scheme must have

1. **Commitment before bets**. Hash is published before the betting
   window opens. Apogee does this via the `serverSeedHash` field in the
   Fair panel of the game UI.

2. **Determinism**. Given the same inputs, the crash point is always the
   same. Apogee's `computeCrash(serverSeed, clientSeed, nonce)` is a pure
   function. No `Date.now()`, no `Math.random()`.

3. **Public algorithm**. The derivation is published. Apogee's is in
   `game.js::computeCrash` and documented in `docs/HOUSE-PROTECTION.md §3`.
   A Python reference is below.

4. **Reveal after round**. `serverSeed` is shown in the Fair panel
   immediately after the round ends.

5. **Independent recomputation**. Anyone can verify without trusting
   Apogee. Apogee exposes `GET /v1/fair/verify` which will re-run the
   math for you, but you don't have to use it — any HMAC-SHA256 library
   works.

6. **Audit chain across rounds**. Each `rounds/{id}` document records
   `prevHash = SHA256(prevRoundRecord)`, so the history can be walked
   forwards and any deletion/modification breaks the chain.

## How to verify a round

### Step 1 — grab the round data

Before the round, note `serverSeedHash` from the Fair panel. After the
round, note `serverSeed`, `clientSeed`, `nonce`, and the `crashPoint`
the game showed.

### Step 2 — recompute the hash commitment

```bash
printf '%s' "<serverSeed>" | sha256sum
# compare to the hash you saved before the round
```

If these don't match, stop — the seed was swapped.

### Step 3 — recompute the crash point

Node.js:

```js
import { createHmac } from "node:crypto";

function computeCrash(serverSeed, clientSeed, nonce, divisor = 33, maxCrash = 10000) {
  const h = createHmac("sha256", serverSeed).update(`${clientSeed}:${nonce}`).digest("hex");

  const hInsta = BigInt("0x" + h.slice(0, 16));
  if (hInsta % BigInt(divisor) === 0n) return 1.00;

  const hCrash = Number(BigInt("0x" + h.slice(16, 30)) & ((1n << 52n) - 1n));
  const e = 2 ** 52;
  const crash = Math.floor((100 * e - hCrash) / (e - hCrash)) / 100;
  return Math.min(maxCrash, Math.max(1.00, crash));
}

console.log(computeCrash("<serverSeed>", "apogee", 123));
```

Python:

```python
import hmac, hashlib

def compute_crash(server_seed, client_seed, nonce, divisor=33, max_crash=10000):
    h = hmac.new(server_seed.encode(), f"{client_seed}:{nonce}".encode(),
                 hashlib.sha256).hexdigest()
    h_insta = int(h[:16], 16)
    if h_insta % divisor == 0:
        return 1.00
    h_crash = int(h[16:30], 16) & ((1 << 52) - 1)
    e = 2 ** 52
    crash = ((100 * e - h_crash) // (e - h_crash)) / 100
    return max(1.00, min(max_crash, crash))
```

### Step 4 — (optional) use Apogee's verifier

```bash
curl "https://api.apogeetech.net/v1/fair/verify?\
serverSeed=<seed>&clientSeed=apogee&nonce=123&instantDivisor=33"
```

Returns the same crash point, plus the raw HMAC output and the two byte
windows so you can step through the math.

## Why we use two independent byte windows

Bustabit's original formula consumed a single 52-bit integer for both
the instant-bust check (`h % 33 == 0`) and the crash formula. The
surviving-rounds distribution was biased away from multiples of 33,
which is detectable over a few million rounds.

Apogee reads:

```
bytes  0..7  → 64-bit integer for the instant-bust coin flip
bytes  8..14 → 52-bit integer for the Bustabit formula
```

These two integers are outputs of the same HMAC but occupy disjoint
bit positions, so they are cryptographically independent. The bias is
gone. See `docs/HOUSE-PROTECTION.md §2.2` for the proof sketch.

## Client seed — can it be manipulated?

The client seed is public and deterministic. In Apogee's current
implementation it's the constant string `"apogee"`, which is fine
because:

* The security of the scheme depends on the **server** seed being
  unknown to the player until the round is over, not on the client seed
  being secret.
* The commitment `SHA256(serverSeed)` is published before bets open, so
  the operator can't swap the server seed no matter what the client
  seed is.
* With a constant client seed, all rounds in a single nonce sequence
  are distinguishable by nonce, which is a monotonic counter. Collisions
  are impossible.

If you want a per-session client seed (e.g. to personalize the
derivation), you can pass `?clientSeed=<your-string>` at launch. The
game will use it and record it in the round record. It doesn't change
the security guarantees.

## Commitment at game level (epoch seeds)

For extra paranoia, Apogee can be run in **epoch mode**: before opening
play for a period (say, 10,000 rounds), a chain of server seeds is
pre-computed:

```
seed[N] = crypto_random_bytes(32)
seed[i] = SHA256(seed[i+1])  for i = N-1 .. 0
```

`seed[0]` is published as the **chain head** on an immutable ledger
(e.g. a tweet, a blockchain TX, a git tag). Round 1 uses `seed[1]` and
anyone can verify `SHA256(seed[1]) == seed[0]`. Round 2 uses `seed[2]`
and anyone verifies `SHA256(seed[2]) == seed[1]`. And so on.

This upgrades the commitment from "one round at a time" to "a whole
epoch at a time", because the operator cannot rebuild the chain without
changing `seed[0]`, which is already public.

Apogee does NOT currently run in epoch mode — per-round commit-reveal
is already the industry standard and is what we deploy by default. Epoch
mode is available on request for operators with compliance requirements
that need it.

## Contrail

Contrail — the multiplayer shared-curve variant — uses a **different** provably-fair scheme than Skyward. The guarantees are stronger, and the algorithm is cleaner. Both schemes are audited and both are in production.

### What's different

| | Skyward | Contrail |
|---|---|---|
| Seed commitment | per-round `SHA256(seed)` published before the round | **full chain** of 10,000 seeds committed at service boot |
| Crash math | two-byte-window Bustabit, 1/33 instant-bust | **inverse-CDF** `cents = floor((α_bps/100)·2^52 / (h+1))` |
| RTP | 97% (drifts to ~96% at high cashout targets) | **exact 99%** at every cashout target, by construction |
| House edge | 3% | 1% (industry-low, matches Stake.com) |
| Max multiplier | 10,000× | 10,000× |
| Client seed | `"apogee"` | `"apogee-contrail-" + roundIdx` (deterministic) |
| Verifier endpoint | `/v1/fair/verify` | `/contrail/genesis` (reveals current chain head) |

### The inverse-CDF formula

```
h      = first 52 bits of HMAC_SHA256(serverSeed, "apogee-contrail-"+round)  (BigInt)
cents  = floor( (α_bps / 100) · 2^52  /  (h + 1) )
crash  = cents < 100  ?  1.00  :  min( cents / 100, MAX_MULT )
```

With `α_bps = 9900` this yields exact 99% RTP because:

```
P(crash ≥ k) = P( α/U ≥ k )  where U = (h+1)/2^52 ∈ (0, 1]
             = P( U ≤ α/k )
             = α/k                                for k ≥ 1

RTP(k)       = k · P(crash ≥ k) = k · α/k = α     (strategy-independent)

P(crash = 1.00) = P( U > α ) = 1 − α = 0.01       (exactly 1% instant)
```

The classic Bustabit ratio `(100E − h)/(E − h)` **drifts** with cashout target (97% at k=1.01 → 96.01% at k=100). Contrail's α/U is flat at every k. That's why we picked it.

### Verifying a Contrail round

Node.js:

```js
import { createHmac, createHash } from "node:crypto";

const TWO_52 = 1n << 52n;
const NUM    = 99n * TWO_52;   // α_bps/100 = 99

function contrailCrash(serverSeedHex, round) {
  const seed = Buffer.from(serverSeedHex, "hex");
  const h = BigInt("0x" + createHmac("sha256", seed)
    .update("apogee-contrail-" + round)
    .digest("hex").slice(0, 13));
  const cents = NUM / (h + 1n);
  if (cents < 100n) return 1.00;
  return Math.min(Number(cents) / 100, 10000);
}

// Walk the chain to verify the commitment held:
function verifyChain(revealedSeedHex, committedGenesisHex, depth) {
  let h = createHash("sha256").update(Buffer.from(revealedSeedHex, "hex")).digest();
  for (let i = 0; i < depth; i++) h = createHash("sha256").update(h).digest();
  return h.toString("hex") === committedGenesisHex;
}
```

Python:

```python
import hmac, hashlib
TWO_52 = 1 << 52
NUM    = 99 * TWO_52

def contrail_crash(server_seed_hex, round_idx):
    seed = bytes.fromhex(server_seed_hex)
    h = int(
        hmac.new(seed, f"apogee-contrail-{round_idx}".encode(), hashlib.sha256)
            .hexdigest()[:13],
        16,
    )
    cents = NUM // (h + 1)
    if cents < 100:
        return 1.00
    return min(cents / 100, 10000)
```

### Epoch commitment (the 10,000-seed hash chain)

At service boot Contrail generates `seeds[0..N-1]` (N = `CONTRAIL_CHAIN_SIZE`, default 10,000), then computes:

```
h[N]   = SHA256(seeds[N-1])
h[i]   = SHA256(h[i+1])       for i = N-1 down to 0
genesis = h[0]
```

The **genesis hash is published immediately** via `GET /contrail/genesis` — before any round plays. Round `r` consumes `seeds[N-1-r]` and reveals it at crash. Any verifier can then:

1. Compute `SHA256(revealedSeed)`.
2. Chain `SHA256` forward round-by-round.
3. Confirm the final result matches the genesis hash you saw at boot.

If it doesn't match, Contrail swapped a seed mid-epoch and the whole epoch is fraudulent. Since genesis was published first, Contrail cannot rebuild the chain without producing a different genesis — and anyone who saved the original genesis will notice.

This is the "epoch mode" option mentioned as 🟡 at the bottom of the Skyward section — **Contrail uses it by default**. It's stronger than per-round commit-reveal because it commits to the entire future at once.

### Fairness endpoint

```bash
curl https://apogee-contrail-jyhtisqmyq-ew.a.run.app/contrail/genesis
```

Returns the current chain's genesis hash, the algorithm name, RTP, house edge in bps, max multiplier, growth constant, and the formula string. This is the document to save if you want to audit a Contrail epoch yourself.

---

## Apogee Hot 5

Apogee's first slot inherits Contrail's **epoch hash-chain**
commitment model but derives draws with a different RNG shape
because slot math needs many uniform integers per spin (5 reel
stops + 1 multiplier draw + 1 optional respin draw), not a single
crash point.

### Commitment model — identical to Contrail

Pre-generated 10,000-seed hash chain committed at service boot.
Genesis is published via `GET /` on `apogee-slots` before any
spin runs. Spin `k` consumes `seeds[N-1-k]`; the seed is revealed
in the `/spin` response JSON. Verifier walks `SHA256(revealedSeed)`
forward to the genesis.

### Spin RNG derivation

Each spin derives its draws from `HMAC_SHA256(seed, clientSeed)`,
split into sequential 32-bit windows. The client seed is
deterministic: `"apogee-hot5-" + spinIdx`.

```
h = HMAC_SHA256(serverSeed, clientSeed)      // 32 bytes
draw_i = (h_bytes[4i..4i+4] as uint32) % N_i
```

Each `draw_i` samples one of:

1. Reel 0 stop (mod 50)
2. Reel 1 stop (mod 50)
3. Reel 2 stop (mod 50)
4. Reel 3 stop (mod 50)
5. Reel 4 stop (mod 50)
6. Multiplier reel position (mod 10)
7. Respin reel stops (if sticky respin triggered) — fresh HMAC
   with client seed `"apogee-hot5-" + spinIdx + ".respin"`

When the 32 bytes run out — the base spin needs 6 draws × 4 bytes
= 24 bytes — the remaining 8 bytes are unused. The respin uses its
own fresh HMAC so the base-spin entropy never influences it.

### Verifying a Hot 5 spin

```js
import { createHmac, createHash } from "node:crypto";

function hot5Spin(serverSeedHex, spinIdx) {
  const seed = Buffer.from(serverSeedHex, "hex");
  const clientSeed = "apogee-hot5-" + spinIdx;
  const h = createHmac("sha256", seed).update(clientSeed).digest();
  const u32 = (off) => h.readUInt32BE(off);
  return {
    reelStops: [
      u32(0)  % 50,
      u32(4)  % 50,
      u32(8)  % 50,
      u32(12) % 50,
      u32(16) % 50,
    ],
    multReelPos: u32(20) % 10,
  };
}
```

Confirm that replaying these draws through the published reel
strips + paytable produces the same outcome the game showed you
and the same payout credited to your wallet.

### Uniform-strip promise

A key fairness claim that's **not** about commitment: Apogee Hot 5
uses **uniform** reel strips. Every stop is equally likely. This is
different from most commercial slots, which use virtual-reel
mapping to make high-payout symbols feel "near-miss" more often
than random. Uniform strips mean:

- The published strip **is** the sampling distribution.
- `P(5-of-a-kind)` can be computed analytically from the strip
  counts.
- Any "near miss" you see occurred at exactly its strip probability.

See `docs/APOGEEHOT5.md §Reel strips` for the full count table.

---

## Summary — the fairness guarantees you get today

| Property | Apogee status |
|---|---|
| Commitment published before round | ✅ via `serverSeedHash` in Fair panel |
| Seed revealed after round | ✅ via `serverSeed` in Fair panel |
| Public algorithm | ✅ `game.js` + this doc |
| Deterministic | ✅ pure function of `(serverSeed, clientSeed, nonce)` |
| Independently verifiable | ✅ JS/Python reference above + `/v1/fair/verify` API |
| Seed from CSPRNG | ✅ `crypto.getRandomValues(32)` |
| Seed uniqueness | ✅ fresh 256-bit seed each round |
| Independent byte windows | ✅ fixed in 2026-04 |
| Round audit chain | ✅ `prevHash` linking in `rounds` collection |
| Player can choose client seed | ✅ via `?clientSeed=` launch param |
| External verifier endpoint | ✅ `/v1/fair/verify` |
| Epoch-level hash chain | 🟡 available on request (Skyward) · ✅ default (Contrail) |
