# M0 client and signed-wallet contract changes

This is the M0 implementation delta on the `4017cd79c76f743ed305674823590a2b2e669c5d` baseline. [The workstream](../workstreams/m0-correctness.md) records verification and closure. The September generated OpenAPI, HTML, PDF and source identity remain historical; this module does not regenerate or publish that separate portal. No deployed-service compatibility is claimed.

## Account history

`GET /v1/history?limit=20&cursor=...` uses the normal session bearer. `limit` defaults to 20 and is an integer from 1 to 100. Omit `cursor` on the first request. Responses retain the existing `rounds` items and add required `hasMore: boolean` and `nextCursor: string | null`.

- Results are account-wide, including earlier sessions, ordered by creation timestamp and round UUID descending.
- Pass `nextCursor` back unchanged for the next page. It is an opaque, account-bound position, at most 512 characters; malformed or foreign-account cursors return HTTP 400 `INVALID_REQUEST`.
- `hasMore: false` always has `nextCursor: null`. Empty histories return an empty `rounds` array.
- A cursor is not a frozen database snapshot: newer inserts do not shift later pages; existing row settlement fields may still change. It is not a total-count API.
- The internal legacy `RoundService.history` method still returns exactly `{rounds}`; the HTTP route uses `historyPage`.

## Private interactive replay

`GET /v1/interactive/rounds/{id}/replay` is registered when the interactive dependency is supplied. Supply a UUID execution/round ID and the native session bearer. Like accepted-work recovery, this authenticates ownership across replacement or expired/revoked sessions for the same account and game; it does not authorize a new wager. The route participates in the interactive authenticated-principal budget.

The JSON result is `{roundId, verification}`. `verification` contains only:

| Field | Type / meaning |
| --- | --- |
| `status` | `matched`, `mismatch`, or `unavailable` |
| `reason` | Nullable verifier reason, not a raw exception |
| `game` | `{code: string|null, version: string|null}` for the pinned runtime |
| `verifiedReceipts` | Number of verified action receipts |
| `sequence` | Nullable snapshot sequence |
| `complete` | Boolean; true only for a matched complete snapshot |
| `firstFailingSequence` | Nullable sequence where verification failed |

No raw snapshot, private game state, action payload, seed, RNG calls, account/session identifiers or internal custody receipts are returned. Replay does not allocate RNG or write money, wallet, outbox or game state.

| HTTP | Meaning |
| --- | --- |
| 200 | Matched, or unavailable with `reason: "no-actions"` |
| 400 | Invalid request/UUID |
| 401 | Missing/invalid bearer or no matching session identity |
| 404 | Missing or foreign account/game execution (`INTERACTIVE_EXECUTION_NOT_FOUND`) |
| 409 | Verification mismatch; same finite report shape |
| 429 | Request/principal admission exceeded |
| 503 | Verification unavailable for another reason (for example pinned runtime or authority unavailable); not a match |

## Multiplayer admission

Room allocation, snapshot/join/reconnect and fresh commands require a currently enabled operator/game entitlement whose currency list contains the session currency. Removing that authority stops new admission even on an authenticated socket. Allocation retains `MULTIPLAYER_GAME_UNAVAILABLE`; room access/commands retain `ROOM_ACCESS_DENIED`. Existing accepted duplicate commands and settlement remain recoverable; revocation does not void accepted money. Custodial-only and responsible-gaming rules are unchanged.

## Responsible-gaming monetary limits

The three nullable monetary limits remain JSON integer minor units. Positive values through `Number.MAX_SAFE_INTEGER` are supported, including values above signed 32-bit range. Unsafe/non-positive service inputs are rejected before mutation; stored values that cannot be represented exactly fail rather than round. This does not change limit enforcement or currency scale.

## Signed external-wallet network policy

Every operation POST and exact-operation recovery GET resolves and validates the current destination before connecting. HTTPS is required; private, loopback, link-local/metadata, mapped IPv6 and non-public destinations are rejected by default. Every returned DNS address must be allowed. The connection pins one validated address, retaining the hostname for Host/SNI and normal TLS certificate verification. It does not follow redirects, use ambient proxies, reuse connections or fall back to another address.

`RGS_WALLET_ALLOW_LOOPBACK` defaults to `false`. Only the literal `true` enables local wallet fixtures, and only with explicit `NODE_ENV=test` or `NODE_ENV=development`; it is forbidden in production or an unspecified environment. HTTP is then allowed only for `localhost` or literal loopback, and resolved addresses must still be permitted. An arbitrary hostname that resolves to loopback is not enabled by this flag. Both API and outbox-worker entrypoints pass this setting to the signed-wallet service.

DNS, connection and bounded response reading share the existing request timeout. A blocked or ambiguous attempt follows existing durable unknown/recovery semantics, never approval or a new debit. Recovery resolves the original operation ID and checks the destination again. Signature framing, wallet response identity, money semantics, BetSymphony transport and authority endpoints are unchanged.
