# Sandbox environment

Test everything end-to-end before you ship.

| Environment | URL |
|---|---|
| **Sandbox API** | `https://sandbox-api.apogeetech.net` |
| **Sandbox game host** | `https://sandbox.apogeetech.net` |
| **Sandbox admin** | `https://admin.apogeetech.net/?env=sandbox` |

Sandbox runs the same code as production but against an isolated
Firestore database. No cross-contamination. State is **wiped on the
first day of every month at 02:00 UTC** so you can count on a clean
slate for monthly test cycles.

## Getting keys

Email `partners@apogeetech.net` with your merchant name and target
markets. We provision a sandbox merchant within one business day and
send you:

- `mrc_test_yourname` merchant ID
- `pk_test_apogee_...` public key
- `sk_test_apogee_...` secret key
- Admin panel login (email allowlist)

Sandbox keys are **separate** from live keys — you can't debug a live
issue by swapping sandbox creds for production ones. Same applies in
reverse.

## Differences from production

| Feature | Production | Sandbox |
|---|---|---|
| Uptime target | 99.95% | 99.5% (best effort) |
| State retention | Indefinite | Wiped monthly |
| Rate limits | 10 req/s burst, 5 sustained | 100 req/s burst, 50 sustained |
| Operator wallet forwarding | Required — we call YOUR URL | Optional — you can use Apogee's shadow wallet if you haven't built yours yet |
| Max stake | Per merchant config | Per merchant config OR `1_000_000` minor default |
| Player PII | Must be pseudonymised | Can be whatever you want (`p_test_...` convention) |
| Forced outcomes (crash at X) | ❌ Never | ✅ See §3 below |
| Force insufficient-balance | ❌ | ✅ Use `balance=0` on `/v1/launch` |
| Force operator rejection | ❌ | ✅ Use `merchant=mrc_test_reject_*` special merchants |

## Test merchants and accounts

These are pre-seeded in sandbox and behave deterministically.

| Merchant ID | Behaviour |
|---|---|
| `mrc_test_ok` | Happy path — every wallet call succeeds, RTP 97%, 1000 default balance |
| `mrc_test_broke` | Wallet returns `402 insufficient_funds` on every `/debit` |
| `mrc_test_slow` | Wallet forwards with artificial 4-second latency (tests client timeout UI) |
| `mrc_test_timeout` | Wallet never responds — Apogee hits 8s timeout and auto-rollbacks |
| `mrc_test_bad_body` | Wallet returns 200 with missing `balance` field (operator_bad_response) |
| `mrc_test_limit` | `/debit` returns `403 limit_exceeded` (RG test) |

## Forced outcomes (for QA teams)

Sandbox games accept a `forceOutcome` query param that lets you pin
the next round's crash point. Production ignores this param entirely.

```
https://sandbox.apogeetech.net/play.html?gameId=skyward
  &merchant=mrc_test_ok
  &sess=sess_...
  &forceOutcome=crash:1.00
```

Supported values:

| `forceOutcome=` | Effect |
|---|---|
| `crash:1.00` | Next round instant-busts at 1.00× |
| `crash:2.50` | Next round crashes at exactly 2.50× |
| `crash:10000` | Next round hits the `MAX_CRASH` ceiling |
| `credit:fail` | Next cashout credit returns 502 from operator (tests wallet-blocked fatal overlay) |
| `debit:fail` | Next debit returns 502 from operator (tests bet rejection flow) |
| `balance:<n>` | Override starting balance for the session (minor units) |

Chain them with comma: `forceOutcome=crash:2.5,balance:50000`.

**Never hardcode this param in production embed URLs.** Sandbox only.

## Launching a sandbox session

**Signed (same as production)**:

```bash
curl -X POST https://sandbox-api.apogeetech.net/v1/sessions \
  -H "X-Apogee-Key: pk_test_apogee_..." \
  -H "X-Apogee-Timestamp: $(date +%s)" \
  -H "X-Apogee-Nonce: $(openssl rand -hex 16)" \
  -H "X-Apogee-Signature: ..." \
  -H "Content-Type: application/json" \
  -d '{"gameId":"skyward","playerId":"p_test_001","currency":"EUR","balance":10000}'
```

**Unauthed (fastest path to a visible game)**:

```bash
curl 'https://sandbox-api.apogeetech.net/v1/launch?gameId=skyward&merchant=mrc_test_ok&currency=EUR&balance=10000'
```

The second form mints a demo session against `mrc_test_ok` and returns
a `launchUrl` you can paste into a browser.

## State reset

On the first day of every month at **02:00 UTC**, we truncate:

- `transactions/` (the audit log)
- `sessions/` (active sessions — players get kicked)
- `rounds/` (round history)

We do **not** truncate:

- `merchants/` — your test merchant persists
- API keys — you keep the same keys

If you need a clean slate mid-month, hit
`POST /v1/admin/sandbox/reset` (admin token required) and it wipes
your own merchant's state.

## Promoting from sandbox to production

1. Finish your integration in sandbox.
2. Run our smoke-test checklist (`docs/QUICKSTART.md §7 Go-live checklist`).
3. Email `partners@apogeetech.net` — we generate production keys and
   register your production `walletUrl` + `webhookUrl`.
4. Replace sandbox creds with production creds in your code.
5. Run 10-20 real-money test sessions with tiny amounts before
   onboarding real players.

## Common sandbox gotchas

**"My debit is rejected with `operator_unreachable`"**
You forgot to set a `walletUrl` on your sandbox merchant. Either set
one in admin panel, or use `mrc_test_ok` which uses Apogee's shadow
wallet.

**"State keeps resetting mid-test"**
You're hitting the monthly wipe window. Run your tests in the first
three weeks of the month.

**"Sandbox game loads production version"**
Hard refresh. The `/play.html` bundle is cache-busted via `?v=N` but
CDNs sometimes lag. Clear your browser cache or add a cache-busting
query param.

**"My test player has a billion in their balance and I can't lose"**
Your test merchant has `maxWin: 10_000_000_000` (100M major). That's
the default. Tighten it via `PATCH /v1/merchants/:id` or use
`mrc_test_broke` which forces rejection.
