# Refund Rush — Aggregator Integration Specification

**Version:** 1.0.0 · **Last updated:** 2026-05-08 · **Protocol:** REST/JSON over HTTPS

This document describes the API contract that aggregators / operators integrate against to launch Refund Rush sessions. The game is delivered as an HTML5 client served by the same FastAPI server that exposes the RGS endpoints below.

---

## 1. Architecture overview

```
   ┌──────────────┐         ┌────────────────────┐         ┌───────────────┐
   │ Player /     │  HTTP   │  Refund Rush RGS   │  HTTP   │ Aggregator    │
   │ HTML5 Client │ ──────▶ │  (FastAPI)         │ ──────▶ │ Wallet API    │
   └──────────────┘         └────────────────────┘         └───────────────┘
                                  │
                                  ▼
                            PostgreSQL + Redis
                            (rounds, jackpots, sessions)
```

- Player launches the game via an aggregator-provided URL with a session token.
- The HTML5 client calls our `/api/v1/game/init` endpoint with that token.
- The RGS validates the token, opens a session, and issues its own short-lived HMAC-signed `session_token`.
- All subsequent player-mutating calls (`/spin`, `/topup`, `/feature/*`, `/self-exclude`) require the RGS session token in the `Authorization: Bearer <token>` header.
- The wallet integration is pluggable: demo mode uses an internal counter; real-money mode calls the aggregator's seamless wallet API for debit/credit/rollback.

---

## 2. Launch URL

Aggregators construct a launch URL of the form:

```
https://refundrush.example/?
    operator=<id>
    &player_id=<id>
    &session_token=<aggregator_token>
    &currency=USD
    &mode=real
    &locale=en
    &return_url=<urlencoded>
```

The HTML client reads these query params and posts them to `/init`.

---

## 3. Endpoints

All endpoints are under `/api/v1/game/`. Request and response bodies are JSON.

| Method | Path                  | Auth   | Idempotent | Purpose                                    |
| ------ | --------------------- | ------ | ---------- | ------------------------------------------ |
| POST   | `/init`               | none   | no         | Open or recover a player session           |
| POST   | `/spin`               | bearer | optional²  | Execute one base-game spin                 |
| POST   | `/feature/spin`       | bearer | yes¹       | Step through a triggered bonus feature     |
| POST   | `/feature/complete`   | bearer | yes        | Finalize a feature and credit final win    |
| POST   | `/topup`              | bearer | no         | Demo-mode top-up (no-op for real money)    |
| POST   | `/self-exclude`       | bearer | no         | Set a self-exclusion lockout               |
| GET    | `/progressive`        | none   | n/a        | Read current progressive jackpot values    |

¹ `/feature/spin` replays the final step on duplicate calls after `complete: true` is returned.  
² `/spin` is idempotent when the request includes a stable `client_spin_id`.

### 3.1 Authentication

The RGS issues an `session_token` on `/init`. Clients must send:

```
Authorization: Bearer <session_token>
```

on every mutating call. The token is HMAC-SHA256 over the session UUID with a server-side secret. Cross-session token use returns `403`. Missing token returns `401`.

### 3.2 Idempotency

- `/spin` accepts optional `client_spin_id` for partner/client retry safety. A duplicate call with the same `session_id`, `bet_amount`, and `client_spin_id` returns the cached committed spin response and does not create a second round, RNG draw, debit, credit, progressive contribution, or feature trigger. Reusing the same `client_spin_id` with a different `bet_amount` returns `409`.
- Existing browser/demo clients may omit `client_spin_id`; those requests create a new spin each time.
- `/feature/complete` is keyed by `round_id`. A duplicate call with the same `round_id` returns the cached result, not a new credit.
- `/feature/spin` after the feature has been fully stepped through replays the final step (rather than 400) so a dropped network response can recover.
- The wallet API still receives deterministic idempotency keys derived from `round_id` for debit/credit/rollback, so provider-level retries remain safe even when a browser client omits `client_spin_id`.

### 3.3 Currency & money fields

- All currency values are **integer minor units** (e.g. cents) in fields named with `_cents` suffix or as plain `int` (`balance`, `bet_amount`, `payout`, `progressive_win_amount`).
- Float fields (`total_win`, `line_win`, `scatter_pay`, `running_total`, `total_feature_win`) are **bet-multipliers** (unit-less). Frontend display should prefer the explicit `_cents` companion fields where present (`total_win_cents`, `total_feature_win_cents`) since those match the actual wallet credit.

---

## 4. Endpoint reference

### 4.1 `POST /init`

Open or recover a session. No auth required.

**Request**
```json
{
  "game_id": "refund_rush",
  "mode": "real",
  "session_token": "<aggregator-issued token>",
  "player_id": "operator_player_42",
  "currency": "EUR"
}
```

**Response — 200**
```json
{
  "session_id": "8f2a...uuid",
  "session_token": "H4Uipx...token",
  "balance": 5000,
  "game_config": {
    "game_id": "refund_rush",
    "num_reels": 5, "num_rows": 3, "num_paylines": 0, "ways_to_win": 243,
    "min_bet": 100, "max_bet": 2500000,
    "mode": "real",
    "compliance_mode": "strict",
    "currency": "EUR",
    "language": "en"
  },
  "receipt_meter_state": { "points": 0.0, "paid_standard": false, "paid_itemized": false },
  "progressive_values": { "mini": 100000, "minor": 500000, "major": 5000000, "grand": 50000000 },
  "last_result": null,
  "state": "IDLE",
  "self_excluded_until": null
}
```

If `last_result` is non-null and `state` is non-`IDLE`, the player has an in-progress feature from a prior session and the client should resume / force-complete it.

### 4.2 `POST /spin`

**Request**
```json
{ "session_id": "8f2a...uuid", "bet_amount": 300, "client_spin_id": "partner-spin-20260701-000001" }
```

**Response — 200** (representative; some fields omitted when null)
```json
{
  "round_id": "a040...uuid",
  "reel_stops": [18, 5, 49, 1, 36],
  "grid": [[12,12,10,4,9],[6,4,5,3,1],[2,1,4,2,1]],
  "payline_wins": [{"symbol": 5, "length": 3, "ways": 4, "payout": 60}],
  "scatter_count": 0,
  "scatter_pay": 0.0,
  "random_event": null,
  "line_win": 2.0,
  "meter": { "points": 4.0, "threshold_hit": null, "prize": 0 },
  "feature_triggered": null,
  "feature_state": null,
  "progressive_values": { "mini": 100015, "minor": 500030, "major": 5000030, "grand": 50000015 },
  "progressive_win_tier": null,
  "progressive_win_amount": 0,
  "total_win": 2.0,
  "total_win_cents": 600,
  "balance": 5300
}
```

**Errors**
- `400 Insufficient demo balance` — wallet rejected the debit
- `400 Session is in state X, complete current feature first` — caller must walk feature before spinning again
- `400 invalid request` — bet_amount out of range or wrong type
- `409 client_spin_id reused with different bet_amount` — retry key collision
- `401 Missing bearer token` — Authorization header absent
- `403 Invalid session token` — token does not match session_id
- `403 Self-exclusion active until ...` — player is locked out

### 4.3 `POST /feature/spin`

Walks one step of a triggered feature: Direct Deposit respin or Bonus Season
free-spin step.

**Request**
```json
{ "session_id": "...", "round_id": "..." }
```

**Response — 200**
```json
{
  "step": 4,
  "total_steps": 11,
  "complete": false,
  "feature_type": "free_spins",
  "running_total": 12.5,
  "step_data": {
    "type": "fs_spin",
    "spin_index": 4,
    "grid": [[...],[...],[...]],
    "multiplier": 2,
    "spin_win": 3.0,
    "running_total": 12.5,
    "step_payout": 3.0,
    "remaining": 6,
    "retrigger_spins": 0,
    "payline_wins": [{"symbol": 8, "length": 4, "ways": 2, "payout": 210}]
  }
}
```

When `complete: true`, the client calls `/feature/complete` to finalize.

### 4.4 `POST /feature/complete`

Finalize the feature and report the cents that were already credited on the trigger spin.

**Response — 200**
```json
{ "total_feature_win": 14.8, "total_feature_win_cents": 4440, "balance": 9740 }
```

**Idempotent** — repeat call with same `round_id` returns the same cached result.

### 4.5 `POST /topup`

Demo mode only. Adds `amount` minor units to the demo balance. Returns 400 in real-money mode.

### 4.6 `POST /self-exclude`

```json
{ "session_id": "...", "duration_seconds": 86400 }
```

Sets a self-exclusion lockout. Subsequent `/spin` calls return 403 until the timestamp passes. The lockout is also reported in `/init` responses.

### 4.7 `GET /progressive`

Returns current jackpot pool values. No auth required (public ticker).

```json
{ "mini": 100024, "minor": 500031, "major": 5000033, "grand": 50000018 }
```

---

## 5. Error format

All error responses use the same shape:

```json
{ "detail": "human-readable message" }
```

Validation errors include an `errors` array with field-level pydantic detail.

| HTTP | Meaning                                           |
| ---- | ------------------------------------------------- |
| 400  | Domain validation (insufficient balance, bad state, round_id mismatch, etc.) |
| 401  | Missing bearer token                              |
| 403  | Invalid token, or self-exclusion active           |
| 404  | Session not found                                 |
| 422  | Pydantic schema validation error                  |
| 500  | Unhandled internal error (logged, never exposes stack) |

---

## 6. Wallet integration (real-money mode)

When `mode=real`, the RGS calls the aggregator's wallet API for every bet/win:

- `POST <wallet_base>/wallet/balance` — read balance
- `POST <wallet_base>/wallet/debit`   — reserve bet (with `round_id` for idempotency)
- `POST <wallet_base>/wallet/credit`  — pay win
- `POST <wallet_base>/wallet/rollback` — reverse a failed round

Configure `WALLET_BASE_URL` and `WALLET_TIMEOUT` via env. The aggregator's wallet API is expected to be idempotent on `round_id`.
See `wallet_contract_mock.json` for the aggregator-neutral request/response
contract used before a partner-specific adapter exists. The RGS sends
`currency` on every wallet request.

---

## 7. Compliance modes

`COMPLIANCE_MODE` env var controls the strictness of the responsible-gaming layer.

| Mode     | Used for                | Behaviors                          |
| -------- | ----------------------- | ---------------------------------- |
| `off`    | social / .com freeplay  | No autoplay caps, turbo allowed    |
| `strict` | UK / MGA / AGCO / DGOJ  | Autoplay ≤100, turbo disabled, ≥2.5s spin/hold, session-time HUD, 60-min reality check, LDW celebration suppression, demo-currency disclaimer |

---

## 8. RNG, math, and audit

- RNG is `secrets.randbelow` (CSPRNG, OS-backed). All draws are recorded to a per-round audit log keyed by `round_id`.
- `par_sheet.md` is historical/draft math evidence only while the current
  runtime is `PRE_CERT_BLOCKED`. Do not use it as final PAR, RTP,
  hit-frequency, volatility, or certification evidence.
- RTP targets are configured through approved operator/game-instance metadata,
  but current target-to-actual values are intentionally withheld until the math
  launch path is resolved and fresh current-hash evidence reconciles.
  Minor and Grand pool progressives are uncapped until hit, so the total game
  maximum is not a fixed multiplier; non-progressive wins are capped at
  10,000x total bet.

### 8.1 Build integrity & change management

Refund Rush separates the certification-scoped math layer from the patchable platform layer at the source-tree level. This lets us patch CVEs continuously without changing the math hash that a future lab certificate will be keyed to.

| Layer | What it covers | Hash exposed at | Changes when | Triggers |
|---|---|---|---|---|
| **Math** | `app/core/` — paytable, reel strips, RNG wrapper, feature engines, RTP scale table | `GET /api/v1/_meta/math-hash` | Math change only | Full re-certification (4–12 weeks) |
| **Platform** | All other source — FastAPI, SQLAlchemy, OS deps, frontend, infra | `GET /api/v1/_meta/platform-hash` | Every security patch / dep upgrade | Internal change-management log |

**Verification workflow for the lab / regulator / aggregator:**

1. After certification, the lab issues a certificate keyed to a specific `math_hash` (e.g. `c7d11952...`).
2. At any time, pull `GET /api/v1/_meta/math-hash` against production. The response is a JSON document with the live hash plus the per-file SHA-256 of every Python file under `app/core/`.
3. After certification, if the live hash matches the certificate, the running build matches the certificate-scoped math bundle.
4. If it does not match after certification, the operator should disable the game pending lab re-issuance.

**`GET /health`** returns service status plus non-sensitive runtime metadata:
version, environment, active `game_id`, supported skin IDs, `RTP_TARGET`,
compliance mode, and deploy `BUILD_COMMIT` when the pipeline sets it.

**`/api/v1/_meta/build-info`** returns both hashes, file counts, and the same
runtime profile for a single regulator or aggregator pull.

**Authentication:** these endpoints are intentionally unauthenticated — they return public attestations, not secrets. They are safe to poll continuously and are designed for inclusion in operator/aggregator monitoring dashboards.

**Patch cadence:**

| Severity | SLA | Triggers re-cert? |
|---|---|---|
| CVSS ≥ 9.0 (emergency) | 24–48h | No, unless math is touched |
| CVSS 7.0–8.9 | 7d | No |
| CVSS 4.0–6.9 | 30d | No |
| Routine maintenance | Monthly window | No |
| Major dep version bump (Python minor, Postgres major) | Planned + lab attestation | No, but lab is notified with regression evidence |
| Any change under `app/core/` | Per re-cert cycle | **Yes** |

**Change management policy** is published at `aggregator/change_mgmt_policy.md` and submitted with the initial cert package. Aggregators may request a copy before signing.

**Math regression suite:** `game_server/tests/cert/` contains freeze tests for paytable, reel strips, scale table, progressive config, feature constants, and math-hash stability. The suite fails closed against drift in the certification-scoped math layer and is part of the pre-cert submission evidence.

---

## 9. Operational

### Required environment variables (production)

| Variable | Purpose | Required value in prod |
|---|---|---|
| `ENVIRONMENT` | Deploy environment marker | `production` |
| `DEBUG_MODE` | Exposes `/api/v1/game/debug/*` for QA | **MUST be `0` or unset in production** |
| `RR_SESSION_SECRET` | HMAC key for session tokens | Strong 32-byte hex from secret store; rotate quarterly |
| `COMPLIANCE_MODE` | UK/EU strict-mode toggle | `strict` for regulated markets, `off` for social/.com |
| `RR_CORS_ORIGINS` | Comma-separated allowed origins | Restrict to your aggregator's launch URL(s) |
| `DATABASE_URL` | PostgreSQL connection | Per environment |
| `REDIS_URL` | Redis connection (progressive cache) | Per environment |
| `WALLET_BASE_URL` | Aggregator's seamless wallet | Per integration |

The server **refuses to start** if `ENVIRONMENT=production` and `DEBUG_MODE=1` are both set — fail-safe against accidental deploys.



- **Concurrency**: every mutating endpoint locks the session row via `SELECT ... FOR UPDATE`. Concurrent spins on the same session serialize.
- **Atomicity**: progressive jackpot awards take the row lock before reading the pool, so two concurrent winners can't both collect the un-reset value.
- **Latency** (typical, single-node sample): `/spin` p50 10ms, p99 14ms; `/feature/spin` ~7ms.

---

## 10. Sample integration sequence

```
Aggregator → RGS:  POST /init  (mode=real, session_token=<agg>, player_id, currency)
RGS       → Aggr:  200 {session_id, session_token, balance, ...}

Player    → RGS:   POST /spin  (Authorization: Bearer <RGS token>)
RGS       → Wallet: debit (bet_amount, round_id)
Wallet    → RGS:    {balance: 4700}
RGS       → Wallet: credit (win_cents, round_id)  [if win > 0]
Wallet    → RGS:    {balance: 4910}
RGS       → Player: 200 {grid, total_win_cents, balance, feature_triggered: "free_spins", ...}

Player    → RGS:   POST /feature/spin (round_id)  [repeat until complete: true]
Player    → RGS:   POST /feature/complete (round_id)
RGS       → Player: 200 {total_feature_win_cents, balance}
```

---

## Appendix A — sample curl invocations

```bash
# 1. Init
curl -s -X POST https://api/v1/game/init -H 'Content-Type: application/json' \
  -d '{"mode":"demo","player_id":"alice"}'

# 2. Spin (with token from init)
curl -s -X POST https://api/v1/game/spin \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"session_id":"...","bet_amount":300}'

# 3. Walk a feature
curl -s -X POST https://api/v1/game/feature/spin \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"session_id":"...","round_id":"..."}'

# 4. Self-exclude for 24 hours
curl -s -X POST https://api/v1/game/self-exclude \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"session_id":"...","duration_seconds":86400}'
```
