## ARENA BATTLE — SINGLE OPERATOR WALLET / SEAMLESS WALLET v1

## Architecture
The operator's existing wallet is the only source of spendable money. Arena Battle does not transfer funds into a second wallet; the provider stores a versioned balance mirror. The site header and game show the same `availableMinor`. Other operator products must debit the same authoritative ledger transactionally.

SDK: `/sdk/slot-battles-seamless-wallet.mjs` (Node 22.16+, node:sqlite).
Runnable reference: `examples/operator-seamless.cjs`. Adapt its short SQLite transactions to your existing wallet; do not create another independently spendable balance. Opening funds are for local fixture accounts only. Real site members are not automatically granted demo funds.
Configuration: WALLET-CONFIG.en.txt. Set a site-specific callback URL, mode=seamless and server-only signing secret.

## Identity and session
The operator derives `externalId` from its authenticated member session. It is an immutable site-member key, never a browser-supplied nickname or balance. The provider maps `tenantId`+`externalId` to one stable player/alias (for example betcio/member-100 → cio1001). Memberships at different sites remain distinct.

Only the operator backend uses the API key to create a launch. The browser receives a short-lived launch ticket/URL, never API keys or wallet secrets. Iframe and module integration use this same wallet protocol.

## Signed callback
The provider POSTs application/json to the configured HTTPS endpoint. Loopback HTTP is permitted for local integration tests. Redirects are not followed. Both request and response are signed over the exact UTF-8 JSON body:

```http Signature headers
X-Slot-Wallet-Timestamp: <unix-milliseconds>
X-Slot-Wallet-Signature: <lowercase-hex-hmac-sha256>
```

Replace placeholders with the values produced by the wallet SDK. Signature formula (pseudocode):

```text Signature formula
signature = HMAC_SHA256(secret, timestamp + "." + rawBody)
```

Allow at most 60 seconds clock skew and use constant-time comparison. Retry the identical request body/`requestId` with a fresh timestamp/signature. Sign success AND authenticated error responses. Invalid signatures never mutate money. Browser requests with an Origin header are rejected. Maximum body 64 KiB.

## Request body
{
  "version": 1, "tenantId": "site-id", "externalId": "member-100",
  "requestId": "reserve:immutable-reference", "action": "reserve",
  "currency": "TRY", "amountMinor": 4000000,
  "holdReference": "reserve:immutable-reference",
  "battle": {
    "id": "room-id", "title": "Evening battle", "status": "waiting",
    "buyInMinor": 4000000, "capacity": 3, "playerCount": 3,
    "gameIds": ["sweet-bonanza-1000"], "feeBps": 500
  }
}
All money is integer TRY minor units (TRY 40,000 = 4,000,000). Reject fractional numbers, numeric strings and negative amounts. Reserve/refund positive maximum 1,000,000,000; settle maximum 1,000,000,000,000. `requestId`/`holdReference` are 8–160 characters, letters/digits/-_ . : @ (no spaces).

## Actions
balance: Read current `availableMinor`, `lockedMinor` and revision. Omit `amountMinor`, `holdReference` and battle. Return receipt=null. No wallet mutation.
reserve: `amountMinor` is the entry fee. Atomically subtract available, add locked, create hold keyed by `requestId`, increment revision and record the ledger entry. A rejected insufficient-funds request does not consume its idempotency reference.

refund: `holdReference` is the original `reserve.requestId`. Require an open reservation belonging to the same member and battle, and the exact reserved amount. Remove locked and return it to available. Used for permitted pre-start cancellation/leaving.

settle: Consume that reservation, remove locked, then add `amountMinor` prize to available. A loser receives `amountMinor`=0; still create a terminal ledger/history row. Do NOT debit the entry fee a second time.

## Response (HTTP 200)
{
  "version": 1, "tenantId": "site-id", "externalId": "member-100",
  "requestId": "reserve:immutable-reference", "currency": "TRY",
  "receipt": {
    "id": "operator-ledger-id", "requestId": "reserve:immutable-reference",
    "action": "reserve", "amountMinor": 4000000,
    "holdReference": "reserve:immutable-reference", "battleId": "room-id",
    "createdAt": 1792900000000
  },
  "balance": {"availableMinor": 1000000, "lockedMinor": 4000000, "revision": 1}
}
The receipt is immutable. Replays return that same original receipt and the CURRENT balance, not the old balance when the receipt was created. Increase revision on every successful wallet change, including other operator games. Never return different balances with the same revision.

## Retries and errors
Identical `requestId`+content is processed once. Replaying with changed content returns 409 `REFERENCE_CONFLICT`. Store the transaction and idempotency receipt in the same database transaction. A lost response can then be retried after a restart without another debit. Use durable retry queues for refunds/settlements; a timeout is not a loss, success or refund. Do not send success before COMMIT.
Error fields (the code below is a placeholder; use the actual error code):

```json Error fields
{
  "error": "description",
  "code": "..."
}
```

For authenticated requests parsed as JSON, also echo version, `tenantId`, `externalId`, `requestId` and currency from that request. This binds the signed rejection to the correct operation. An unverifiable signature or binding is uncertain and retried for reconciliation, not treated as a definitive rejection. Relevant codes: `INSUFFICIENT_FUNDS` (HTTP 409), `HOLD_CONFLICT` (HTTP 409), `REFERENCE_CONFLICT` (HTTP 409), `PLAYER_NOT_FOUND` (HTTP 404), `INVALID_REQUEST` (HTTP 400), `INVALID_SIGNATURE` (HTTP 401).

## Battle transaction details
battle.id is required on mutations. Before selection, `gameIds` records the candidate pool; unknown optional fields are omitted. Final metadata can contain title,status,`buyInMinor`,capacity,`playerCount`,`gameId`,`gameIds`,`gameName`,provider,`bonusType(type ID)`,`bonusName`,`scoreMinor`,`prizeMinor`,`feeMinor`,`feeBps`,`totalPayoutMinor`,`netPoolMinor`,`outcome(win/loss/refund/pending)`,winners,`createdAt`,`endedAt`.

Reserve history has a negative delta, refunds and prizes positive deltas, and losing settlement zero. Group reserve/settle records using `battleId`. The entry can show the candidate game pool while the terminal record shows the actual selected game, final score and prize.

## Balance example
- Available TRY 50,000; entry TRY 40,000 → available TRY 10,000, locked TRY 40,000.
- Pre-start refund → available TRY 50,000, locked TRY 0.
- Winning TRY 150,000 instead → available TRY 160,000, locked TRY 0.
- Loss → available TRY 10,000, locked TRY 0, plus a zero-prize terminal history entry.

## Reference server
Options passed to `createSeamlessOperatorExample` from `examples/operator-seamless.cjs`:

```javascript Reference server options
createSeamlessOperatorExample({
  providerOrigin, tenantSlug, tenantId, apiKey, walletSecret,
  walletFile, mode, accounts, port
});
```

mode is iframe or module. accounts is local fixture data [`{externalId,login,password,openingBalanceMinor}`]; production must use the site's own sessions and wallet. The example derives identity server-side and rejects browser-supplied `externalId`/amount.

Environment: `PROVIDER_ORIGIN`, `OPERATOR_SLUG`, `OPERATOR_TENANT_ID`, `OPERATOR_API_KEY`, `OPERATOR_WALLET_SECRET`, `OPERATOR_WALLET_FILE`, `OPERATOR_ACCOUNTS_FILE`, `OPERATOR_MODE`, `OPERATOR_PORT`.

Callback `/wallet/callback`. Authenticated own-wallet/history: `GET /integration/wallet` and `GET /integration/history`. Both site/header balances use `availableMinor`; wallet-changed events and a 2-second fallback poll refresh the display.
Internal `siteAdjustment()` is trusted operator code for showing another game's use of the same balance. It is not exposed as a public balance-edit endpoint.

## Verification

```shell Local verification
node --test tests/v15-r24-seamless-operator.test.cjs tests/v15-r24-seamless-example.test.cjs
```

Tests use actual loopback HTTP servers and SQLite for signatures, reserve/refund, wins/losses, restart, idempotency, member isolation and iframe/module delivery. Round results in accounting tests are controlled fixtures, not third-party game or broadcast-quality tests.
