## ARENA BATTLE — OPERATOR IDENTITY AND TRANSFER WALLET

## 1. Player identity and entry flow
The operator backend derives `externalId` from its authenticated member session,
never from browser-supplied IDs, display names or balances. tenant + `externalId`
is a durable mapping. Reopening iframe or module returns the same account.

Configure `playerAliasPrefix` in Admin > Sites > Site identity and access.
For example cio assigns cio1001, cio1002... to new accounts. `publicId` is immutable
and site-local, not a globally unique identity: include the site ID in reports. The same `externalId` at another site has a separate account, balance and history. Existing display names/history remain unchanged. Prefix changes
affect only new accounts. With a prefix configured, name may be omitted; legacy
sites without a prefix require a name only for a new `externalId`. Disable guest
name-based login on integrated operator sites. Admin/operator player lists show
the public code alongside the operator member ID, within the assigned site.

## 2. Transfer wallet and spendable funds
This section describes transfer mode. For one spendable site/game balance, use the separate callback contract in SEAMLESS-WALLET.en.txt.

The site's available funds and the provider's game funds are separate spendable
balances. Never copy the same balance into two independent wallets.
Example: the site starts with TRY 50,000 and transfers TRY 10,000 to the game.
- Atomically debit or reserve TRY 10,000 in the site's authoritative wallet first.
- Credit the provider with the same durable transfer reference.
- Available site balance is TRY 40,000; available game balance is TRY 10,000.
- A TRY 2,000 entry leaves TRY 8,000 available in game and TRY 2,000 locked.
- Cancelling a waiting room returns its reservation to the game wallet.
- A settled prize enters the game wallet. Refresh the site header from the API; cashout is required to spend those funds in other site products.
- Cashout first confirms the provider debit, then credits the site wallet exactly once.

`totalAvailableMinor` is an informational sum of `siteAvailableMinor` and `gamingAvailableMinor`, not permission to spend the game portion elsewhere. Exclude locked funds.

## 3. Server wallet module
`sdk/slot-battles-wallet.mjs` is a durable Node 22.16+ / SQLite reference operator
wallet. An operator with its own wallet must port the short local transactions
to that authoritative wallet/ledger rather than maintain a second balance copy.
`createAccount({externalId,openingBalanceMinor})` seeds a local test member once,
never again on login. The reference is not connected to any real operator DB.

Server API operations (call each for its corresponding workflow; these are not a single transfer sequence):

```javascript Wallet SDK operations
const wallet = new SlotBattlesWallet({
  file: '/private/site.sqlite', client, siteId: 'betcio'
});
wallet.createAccount({externalId, openingBalanceMinor});
await wallet.fund({externalId, amountMinor, reference});
await wallet.cashout({externalId, amountMinor, reference});
await wallet.snapshot(externalId);
await wallet.recoverPending();
await wallet.syncEvents();
```

Fund debits/reserves the local site wallet BEFORE provider credit. Cashout credits
the local wallet only AFTER the provider debit receipt is verified. Amounts are
integer minor units. Same reference + same member/direction/amount is replayed,
not applied twice. Changed data with the same reference is rejected.
`WALLET_PENDING` means ambiguous/incomplete: retain the reference, do not claim a
refund/success, and do not issue a replacement reference. Retry/recover the exact
operation. Another operation for that member waits. Definitive reference clashes
and unresolved failures require operator reconciliation, not a blind compensating
credit. Obvious unavailable/locked cashouts are rejected before creating pending.
Snapshots expose `siteAvailableMinor`, `gamingAvailableMinor`, `gamingLockedMinor`,
`totalAvailableMinor`, `pendingFundingMinor`, `pendingCashoutMinor`, `hasPendingTransfer`.
Event cursors are persisted; event replay never credits the site a second time.
Keep the DB durable/backed up; run `recoverPending` after restart.

## 4. Browser integration
Use `POST /integration/launch` on your own backend with validated member session,
exact Origin and CSRF. Only the short-lived ticket/launch URL goes to the browser.
The API key stays in a private backend secret/environment variable.
Iframe callbacks; use the launch returned by your authenticated backend:

```javascript Iframe callbacks
const game = mountSlotBattles({
  container,
  launch,
  providerOrigin,
  onIdentity: ({player}) => showPublicId(player.publicId),
  onWalletChange: () => refreshFromOurAuthenticatedBackend()
});
```

Module config (server-rendered, before `integration-context.js`):
  window.SlotBattlesConfig={apiBase:'https://PROVIDER_HOST/play/betcio/',
    launchEndpoint:'/integration/launch',launchMethod:'POST',
    launchCsrfToken:pageCsrf,sessionNamespace:opaqueSessionNamespace};
The module reauthenticates every opening and clears stale previous-account tokens
before an authoritative launch. A failed login never falls back to the old account.
Module events: slot-battles:identity, slot-battles:wallet-changed. These are refresh
hints, never financial authority. Your balance endpoint resolves the member from
its own cookie session. The iframe helper verifies exact source window and origin.
On site logout/account switch call `client.revokeSessions(externalId)`, destroy the
iframe/clear module UI. This revokes that site's member game/read tokens and unused
tickets across devices; it does not cancel started battles or erase money/history.

## 5. Additional API routes

```http Transfer receipt
GET /provider/v1/wallet/transfers/{encodedReference}
```

Requires wallet scope. Returns the immutable receipt, external member ID and current available/locked game balances.

```http Revoke player sessions
POST /provider/v1/player/{encodedExternalId}/sessions/revoke
```

```json Request body
{}
```

Requires launch scope.

```http Player balance
GET /provider/v1/player/{encodedExternalId}/balance
```

Requires read scope. Returns `player.publicId`, `availableBalance`/balance and `lockedBalance`, in integer minor units. Replace path placeholders with URL-encoded identifiers. See `/sdk/openapi.json`.

```http Stable site account
POST /provider/v1/player
POST /provider/v1/launch
```

The same tenant + `externalId` reuses the permanent account. Provider and operator player lists show `publicId`, the external member ID and site context.

## 6. Runnable reference site
`examples/operator-authenticated.cjs` implements HttpOnly/SameSite cookie login,
CSRF, iframe or full DOM module, split site/game/locked header, stable first-funding
reference, session revocation and serialized launch/logout per member. GET wallet
has no money side effects. Only POST launch performs the initial transfer once;
it never replenishes losses on login. Replace its reference authentication and
wallet with your real site session and transactional ledger before integration.
Required environment: `PROVIDER_ORIGIN`, `OPERATOR_SLUG`, `OPERATOR_API_KEY`,
`OPERATOR_ACCOUNTS_FILE` (private JSON), `OPERATOR_WALLET_FILE`. Optional `OPERATOR_PORT`,
`OPERATOR_MODE`=iframe|module, `OPERATOR_INITIAL_TRANSFER_MINOR`. Allow the operator
origin in admin. Accounts JSON example (private, local test data only):
  [{"login":"member1","password":"YOUR_LOCAL_TEST_PASSWORD",
    "externalId":"member-1","openingBalanceMinor":5000000}]
The old `operator-site.cjs` profile picker is not an authentication example.

## 7. Verification
R23 exercises real loopback HTTP/SQLite and Chromium iframe/module at desktop and
mobile widths, session switching, funding exactly once, reserve/cancel/settlement,
lost responses after commit, restart, CSRF and reconciliation. These contract tests
are not a live external casino wallet integration or external game/audio/video test.
Repeat acceptance testing with the customer's real authenticated wallet adapter.

The reference revokes expired site sessions and retries failed revocations in its periodic queue. A launch expiring in flight revokes its new ticket. To run the packaged reference, extract the client ZIP into public/ at the kit root. Adapt the deployed customer module to the existing authenticated member and authoritative wallet database.

## 8. Source identity and session risk context
`/player` and `/launch` accept optional `siteUsername` from the authenticated server:
  {"externalId":"member-1545","siteUsername":"member_login","origin":"https://casino.example"}
`externalId` remains the durable site account key. Prefer an immutable member ID
rather than a changeable login as `externalId`. `siteUsername` separately stores the
original site username/account label. Only trusted server enrollment updates it;
`publicId`, public display name, balance and history are unchanged. On first signup,
name is a fallback; omitting `siteUsername` on later calls preserves its saved value.
Use 1–128 characters without control characters. Main admin and that site's panel
can view both identities. Public player/room and browser launch/state responses
exclude `siteUsername`, `externalId` and device evidence.

After login, `player-device.js` sends bounded context to
`/play/{slug}/api/telemetry`: timezone, language, platform, screen dimensions, pixel
ratio, touch points and a random origin-scoped browser identifier. The raw ID is
not persisted; an installation-keyed HMAC is stored. The same provider installation
can compare this signal across tenant participants; another installation generates
a different hash. Browser storage can be cleared, blocked or partitioned. A match
is not proof of one physical person, nor does a mismatch prove different people.
No canvas, audio or font fingerprinting or precise location is collected.

The default IP is the server-observed socket peer. A proxy explicitly configured in `TRUSTED_PROXY_IPS` may supply one valid `X-Real-IP`, verified per request. Headers from unlisted peers are ignored; `X-Forwarded-For` and `Forwarded` chains are not parsed. The proxy must overwrite this header from its own connection peer; the backend port must stay private. Do not treat an unconfigured proxy IP as the player's IP or automatically penalize users based on it. See ACCESS-SECURITY.en.txt. UA/client hints
and browser context are editable risk signals. The endpoint requires a full player
session and allowed Origin; read-only media tickets cannot write it. Room context
requires actual membership; unrelated room IDs are recorded only as generic
session context. Limit: 30 requests/player/minute. Device/environment records have
90-day retention; the pre-existing basic IP connection history remains separate
from device cleanup. Set deployment-specific retention and access policies. This
evidence informs review; it does not change game outcomes or wallet operations.
