# Responsible gambling

Apogee Studios supports the operator-enforced RG model: **you**
decide who can play, for how long, and with what limits. Apogee
provides the tools to enforce those decisions inside the game
session.

## Principle

Apogee never holds, enforces, or audits responsible-gambling limits
itself. That's your regulator's job and your licence's job. What we
give you is:

1. An API endpoint to **terminate** a player's active session when
   they self-exclude or hit one of your limits.
2. An endpoint to **pass player limits** into the game session so
   the player sees them in the UI.
3. **Webhooks** for every material bet event so your RG engine can
   react in real time (rolling loss, session time, win streak).
4. **Respectful game design** baked into every game — no autoplay,
   no LDW celebrations on <10× wins, no near-miss virtual reels,
   mandatory cashout cooldowns, no "one more round" reflex loops.
   See `docs/GAMES.md` per-game RG notes.

## Endpoints

### `POST /v1/players/:playerId/terminate`

Terminate all active sessions for a player. Any in-flight bets are
refunded to the operator wallet via `/wallet/rollback`.

**Authentication**: HMAC-signed (operator only).

**Request body** (optional):

```json
{
  "reason": "self_excluded",
  "until":  "2027-04-12T00:00:00Z",
  "note":   "player-initiated 6-month exclusion"
}
```

| Field | Type | Required | Values |
|---|---|---|---|
| `reason` | enum | no | `self_excluded`, `limit_reached`, `compliance_flag`, `operator_request`, `other` |
| `until` | ISO-8601 | no | Optional reactivation time. Sessions cannot be created for this player until this time (returns `403 player_excluded`). |
| `note` | string | no | Internal note stored in audit log |

**Response 200**:

```json
{
  "playerId":   "p_1234",
  "terminated": 2,
  "rolledBack": 1,
  "reason":     "self_excluded"
}
```

`terminated` = number of active sessions killed.
`rolledBack` = number of in-flight bets refunded to your wallet.

If the player has no active sessions, returns `200` with
`terminated: 0, rolledBack: 0` — it's a no-op, not an error.

### `POST /v1/sessions` — RG limits in session metadata

When creating a session, pass player limits under `rgLimits`:

```json
{
  "gameId":   "skyward",
  "playerId": "p_1234",
  "currency": "EUR",
  "balance":  10000,
  "rgLimits": {
    "sessionTimeMs":  3600000,
    "sessionLossMax": 10000,
    "singleBetMax":   500,
    "realityCheckMs": 300000,
    "realityCheckMessage": "You've been playing for 5 minutes. Your session P/L is {pnl}."
  }
}
```

| Field | Type | Effect |
|---|---|---|
| `sessionTimeMs` | integer | Hard-kick the session after this many ms (game shows a polite exit screen) |
| `sessionLossMax` | integer (minor) | Auto-terminate the session if `(wagered − returned) ≥ this` |
| `singleBetMax` | integer (minor) | Cap the stake on a single bet at this value (tighter than merchant `maxStake`) |
| `realityCheckMs` | integer | Show a modal every N ms with session duration + P/L, player must acknowledge before continuing |
| `realityCheckMessage` | string | Template shown in the reality-check modal. `{pnl}`, `{duration}`, `{wagered}`, `{returned}` are substituted. |

All limits are **advisory on the client but authoritative server-side**.
A cracked client cannot bypass `sessionLossMax` because every debit is
checked against the session's running total in `transactions/`.

### `GET /v1/players/:playerId/status` (HMAC-signed)

Check whether a player is currently excluded.

**Response 200**:

```json
{
  "playerId":     "p_1234",
  "excluded":     true,
  "reason":       "self_excluded",
  "since":        "2026-04-12T10:30:00Z",
  "until":        "2027-04-12T00:00:00Z",
  "activeSessions": 0
}
```

Returns `excluded: false` if never excluded OR if the `until` date
has passed.

## Webhooks for RG engines

If your RG engine runs separately from your wallet (common pattern),
subscribe to these webhooks to get real-time signals:

| Event | When it fires |
|---|---|
| `session.created` | Every new `/v1/sessions` POST |
| `session.terminated` | Session ended (expiry, timeout, manual terminate) |
| `player.bet_placed` | Every committed debit |
| `player.bet_settled` | Cashout, loss, or refund |
| `player.reality_check_shown` | Modal displayed to player |
| `player.session_limit_hit` | One of the `rgLimits` fired (auto-terminates the session) |

See `docs/WEBHOOKS.md` for delivery guarantees and signature
verification.

## Game-design RG features (baked into every game)

These aren't configurable. They ship on by default.

### Skyward + Thermal
- **Opt-in re-entry after cashout**: after a cashout, the bet button
  switches to "BET FOR NEXT ROUND" and the default is NOT queued. The
  player must explicitly tap again to opt into the next round. Breaks
  the one-more-round reflex loop.
- **1-second cashout cooldown**: prevents rapid double-tap queuing.
- **2-second rejection cooldown**: after a rejected debit, retries are
  throttled so frustration-mashing can't hammer the wallet.
- **No autoplay on by default**: auto-bet is available but requires
  the player to explicitly set a number of rounds and stop conditions.
- **Honest lobby stats**: the "X bets · Y total" pill reflects real
  player count, not fake FOMO numbers.

### Contrail
- **Mandatory break support**: if operator sends `breakUntil` in the
  session, the game blocks all new bets and shows a "you're on a
  mandatory break — X minutes to go" screen. See `docs/GAMES.md`.
- **Chase detection**: server-side counter on consecutive losses +
  stake doubling. Not enforced without operator opt-in, but exposed
  via `player.chase_detected` webhook.

### Apogee Hot 5
- **No autoplay, no turbo, no gamble**: all disabled at the code
  level, not just hidden. UK GC 2021 dark-pattern ban list compliant.
- **No LDW celebrations**: win audio only fires if `payout > stake`.
  A 0.5× "win" on a 1 EUR bet (winning 0.50 EUR back from a 1 EUR
  stake) triggers no sound, no flash, no vibration.
- **2.5s global spin-cycle floor**: can't spin faster than 2.5 seconds
  per round. No "rapid-fire" feel.
- **Session net P/L always visible in header**: player always knows
  they're down, not just that they "almost won".
- **30-minute reality check** built into the slot: a polite modal
  shows total duration and P/L, requires acknowledgement.
- **Two-tap confirm for stakes ≥ 5.00 EUR**: deliberate friction on
  bigger bets to break impulse.
- **Germany build** (`SLOTS_DE_MODE=true`): clamps max stake to 1.00
  EUR per GlüStV 2021. Enabled via env var on the operator's
  deployment.

## Operator responsibilities

| Thing | Operator does | Apogee does |
|---|---|---|
| Decide who is excluded | ✅ | — |
| Call `/v1/players/:id/terminate` when they're excluded | ✅ | — |
| Store exclusion records for regulator audit | ✅ | 🟡 we log calls to `/terminate` for 90d |
| Show exclusion period to the player on your site | ✅ | — |
| Enforce geographic / age restrictions | ✅ | — |
| Enforce deposit limits | ✅ | — |
| Configure per-session loss / time / bet limits | ✅ pass via `rgLimits` | — |
| Kill in-flight bets when terminating | — | ✅ via `/wallet/rollback` |
| Terminate session immediately (<1s) | — | ✅ server-side mutex |
| Surface reality checks to the player | — | ✅ modal rendered by game |
| Block exclusion bypass via new session creation | — | ✅ `until` enforcement on `/v1/sessions` |
| Prevent one-more-round dark patterns | — | ✅ game design |

## GlüStV 2021 (Germany) quick-start

German operators should:

1. Set `SLOTS_DE_MODE=true` in their `apogee-slots` deployment env.
2. Pass `rgLimits.singleBetMax: 100` (1.00 EUR) on every session.
3. Pass `rgLimits.realityCheckMs: 3600000` (every hour).
4. Pass `rgLimits.sessionTimeMs: 21600000` (6-hour max session).
5. On player self-exclusion via OASIS, call `/v1/players/:id/terminate`
   with `reason: "self_excluded"` and `until` = the exclusion end.

Apogee does not submit to OASIS on your behalf — you query OASIS
before creating a session and reject the launch if the player is
excluded. Apogee enforces the session-level limits you pass in.

## UKGC remote technical standards

For UKGC operators, Apogee Hot 5 is the only currently-certified
candidate (slot content). Skyward and Contrail are in scope but not
yet cert'd. See `docs/CERTIFICATIONS.md`.

RTS 3, 8, 13, 14 compliance on Apogee Hot 5:

- **RTS 3** (RNG): CSPRNG verified by our hash chain — see
  `docs/PROVABLY-FAIR.md`.
- **RTS 8** (Player awareness — session reality check): ✅ built-in,
  configurable via `rgLimits.realityCheckMs`.
- **RTS 13** (Auto-play): ✅ not supported in the build. Can't be
  enabled by any flag.
- **RTS 14** (Display of player's funds): ✅ session P/L always
  visible in the slot header.

## Questions

`rg@apogeetech.net`. We respond within 1 business day and escalate
regulator-impact questions to our compliance partner within 24h.
