# Player bank The bank stores one points balance per exact GUID across T5 and T6. Players use `.b`, `.d ` and `.w ` with the TWS bank scripts. Deposits move spendable match points into the bank; withdrawals return bank points to the current match. Neither changes earned `score_total`. Bank-assisted games still count toward the existing leaderboards; the ledger records their match IDs. This release does not introduce separate bank-assisted rankings. ## Setup and rollout 1. Back up the controller database. Update every host agent to **1.9.1 or later** before installing the public-endpoint bank scripts. This version settles both public and legacy controller-derived journals before removing runtime storage. Package and offline-sign the agent release using the [host update workflow](native-hosts.md). 2. Deploy Electric Cherry and restart the controller. Migration 20 creates accounts, requests and the ledger; migration 21 adds instance credentials and binds existing requests to their currently active native instance, if any. 3. Set `EC_BANK_MAX_BALANCE` (default 250000, range 1–1000000). Invalid configuration refuses new deposits. Lowering the limit preserves existing balances and prepared deposits; withdrawals remain available. 4. Build/install TWS plugins `t5-plugin`/`t6-plugin` 1.1.0 or later and restart the **whole game-server process**. Install `tws_t5_bank.gsc` or `tws_t6_bank.gsc` 2.0.0, common 1.0.0+ and telemetry 1.0.0+. For existing servers changing endpoints, use the rebuild procedure below. 5. Create or rebuild the servers through EC. Launch preparation generates a cryptographically random `ec_bank_` credential for each immutable instance, stores it in the private controller database, and injects it through `configs/electric_cherry_bank.cfg`. The launch command executes this private file and sets `tws_server_id`. The host agent writes only `tws_bank_token` in that private file; it no longer generates `tws_bank_url`. Both game scripts always send bank requests to `https://ec.thewrightserver.net/api/bank`, even if an old `tws_bank_url` remains in configuration. The host agent's enrolled HTTPS controller URL is separate and continues to carry agent traffic, including authenticated bank settlement. It may differ from the public endpoint reachable by game containers. There is **no `EC_BANK_TOKENS` environment map** and no per-server secret setup. The private configuration is sent only in that host's authenticated operation payload. It is excluded from shared recipes, promoted files and blob downloads. Do not upload `electric_cherry_bank.cfg`: its filename is reserved. Protect the controller database, host operation journal and per-instance runtime directory, which contain private credentials. Drift scans exclude the exact generated configuration path for both games. A maintenance rescan resolves old bank-only incidents and retains other file changes in mixed incidents. Private-file campaign plans saved before this fix are rejected at activation; cancel them and rescan rather than retrying the old plan. Upgrade host agents to **1.9.3 or later** before resuming drift recovery so stages already saved on a host also reject private-file reads and changes. Legacy private bank-file campaign diffs and their blob downloads are blocked; this does not erase previously captured bytes from databases or backups. Retries, rebuilds and host moves of the same instance reuse its credential. Retiring the instance immediately revokes authentication. Creating another server with the same display name produces a new instance ID and secret; it cannot finish the old instance's requests. Accounts remain attached to player GUIDs, so balances survive the servers. Telemetry tokens and browser sessions cannot authorize bank requests. Host credentials can only settle decisions through the separate, operation-scoped teardown endpoint. For an upgrade from the initial static-token implementation, drain outstanding transfers while the old controller/configuration still runs, then deploy and rebuild. Remove the obsolete environment map and any manually supplied token. Do not replace identities or discard pending journals to force an upgrade. ### Upgrading from a controller-derived game endpoint Update agents to `1.9.1+` first. If an existing server's `tws_bank_url` differs from `https://ec.thewrightserver.net/api/bank`, rebuild that server through EC with bank scripts `2.0.0`. The agent stops the old game, settles its pending commit/cancel decisions with their original request IDs, then starts the replacement and removes old storage. The server identity and instance credential are preserved. Lost acknowledgments can replay the same decisions; EC's retained request IDs prevent duplicate ledger changes. An outage or invalid journal retains storage and blocks replacement until recovery succeeds. Do not deploy the new scripts with only a map restart when the endpoint changes: native journals are keyed by server and endpoint, and the new scripts cannot load the old scope. Do not rename, rewrite or delete pending journals. Keep the agent's enrolled controller URL unchanged until legacy journals drain; the agent accepts only the fixed public endpoint and the legacy `/api/bank` URL derived from that enrolled controller. Other legacy URLs require recovery with their original configuration before switching. If the old URL already matches the exact public endpoint, existing journals retain their identity and a map restart is sufficient for the script upgrade. No new native plugin build is required beyond `1.1.0`. ## Machine API All routes below are `POST /api/bank/`, with `Content-Type: application/json` and `Authorization: Bearer `. Responses are never cached. Bodies are capped at 4096 bytes. GUIDs and request IDs stay strings; never convert them to numbers. | Action | Body | Meaning | | ---------- | ------------------------- | -------------------------------------------------------------------- | | `balance` | `server`, `guid` | Read the shared balance and currently available points | | `deposit` | Transfer fields below | Reserve deposit capacity without crediting the account yet | | `withdraw` | Transfer fields below | Reserve bank funds without debiting the account yet | | `commit` | Identical transfer fields | Apply a prepared transfer once and append its ledger entry | | `cancel` | Identical transfer fields | Release a reservation; also prevents a delayed prepare from applying | Transfer example: ```json { "server": "t5_zombie_theater_1", "guid": "18446744073709551615", "match": "reporter-match-key", "request_id": "unique-process-token-17", "operation": "deposit", "amount": 2000, "capacity": 995000 } ``` `match` is the reporter's telemetry match key, **not** Electric Cherry's numeric match ID. `request_id` is unique for the server and at most 64 characters. `amount` is a positive integer up to 1000000; withdrawal also accepts `"all"`. The script resolves deposit `all` to its captured spendable points. `capacity` is the current room below the game's bank withdrawal cap of 1000000 points. An `all` withdrawal that exceeds this room is refused, rather than truncated. Preparation requires the GUID to be present in the server's latest active match, with the exact telemetry match key. Late joiners may need to wait for the next 30-second heartbeat. Unknown/destroyed servers, absent players and old matches cannot prepare transfers. Settlement of an existing decision remains possible after match completion or retention. Host teardown can finish saved decisions after retirement; the retired game credential itself no longer authenticates. Successful application responses include `ok`, `state`, `code`, `request_id`, `amount` and `balance`. States are `balance`, `prepared`, `committed`, `cancelled` or `rejected`. For balance reads, `amount` is the available balance. A business rejection uses HTTP 200 with `ok: false`, `state: "rejected"` and a code such as `insufficient_balance`, `empty`, `bank_limit`, `points_limit`, `not_in_match` or `pending`. It never changes points. Authentication failures use 401/403, malformed input 400/413/415, identity conflicts 409, and unavailable service 503. Repeated requests return the saved result/state without applying a second change. Settled IDs never reopen; a committed result retains its original resulting balance even if later transfers change the account. Reusing an ID with different transfer fields returns 409. A GUID has at most one prepared transfer across all servers. SQLite transactions serialize checks, account updates and ledger insertion, so concurrent requests cannot overspend funds or deposit past the configured maximum. ## Lost responses and recovery The game saves a cancellation decision **before** sending a prepare request. Only a validated prepared reply lets it recheck the player's life, current spendable points and withdrawal room. It then records a commit decision and updates spendable points without yielding. The sender retries settlement with the original identity, at delays of 1, 2, 4, 8, 16, then 30 seconds. Invalid, truncated or uncorrelated replies never authorize points changes. A failed/uncertain prepare leaves game points unchanged and queues cancellation. Cancellation can arrive first: its tombstone blocks the delayed prepare. Death, respawn, disconnect or match end before application also cancels. After a commit decision, retries only finish the ledger; they never change game points again. These points now belong to the current match, so an applied withdrawal is not automatically refunded if that match ends or its server crashes. Keep `tws-bank/t5/` or `tws-bank/t6/`, relative to the game process working directory, on persistent writable storage with a single process per server/endpoint. Journals contain transfer data, not secrets. On a map/process restart, saved decisions settle before new bank commands. Unreadable storage disables the bank; failed writes prevent points changes. Pending decisions remain until acknowledged, even on credential errors. Bank holds do not expire automatically, because expiring a saved commit can lose a deposit or create a free withdrawal. Restore connectivity/configuration and the original journal to resolve holds; never delete a pending journal to unblock the bank. If storage is permanently lost, reconcile against the retained request and ledger records before any manual repair. During removal, rebuild, host migration or launch cancellation, agent 1.9.1+ stops the game and drains its journals **before** deleting runtime files or starting a replacement. It uses its host credential at `POST /api/agent/bank/settle`; EC checks the assigned host, immutable instance, unfinished teardown operation, server and original request identity. This endpoint permits only commit/cancel, never new deposits or withdrawals. Journal URLs must match the public game-bank endpoint or that host's legacy controller-derived bank endpoint; the agent never follows a journal URL. Corruption, an unrecognized endpoint, failed acknowledgment or an outage retains storage and leaves the operation unresolved for retry. An interrupted rebuild uses the existing rebuild recovery workflow to retry with a fresh operation after settlement. Do not manually erase its old directory. The journal provides recovery from map/process restart. It cannot make live game memory and SQLite one atomic transaction or preserve match points after a machine/storage failure. Back up persistent storage and verify crash behavior in the actual Windows/Plutonium deployment before rollout. ## History and validation The **Players → Bank** page shows fleet-wide balances, funded accounts, completed transfers in the last 24 hours and pending transfers. Accounts can be searched by player name or GUID, sorted by balance or recent completed activity, and filtered to funded accounts, accounts at or above the configured limit, or pending transfers. Balances always remain shared across both games. Transactions can be filtered by player, game, server, direction and inclusive UTC dates. Pending transfers show oldest first, with an attention indicator after five minutes and details explaining the reservation. This indicator does not expire or cancel a transfer. Both views retain historical server identities and match IDs after retention; unavailable matches have no live link. Selecting a player opens a drawer with their balance, available points, pending transfer and paginated history. Filters and pagination are retained in the URL, including when following a match or player-profile link. The page refreshes every 30 seconds while visible. Summary counts always cover the whole fleet. Bank is read-only for all browser roles; transfers continue to originate in game. Player profiles show the shared balance, pending status and three newest ledger entries, with a **View bank history** link to the drawer. Entries include signed amounts, resulting balances, server, time and match. Accounts, request IDs and ledger entries have no cascading match/server foreign keys and are not deleted by match retention or server deletion. Retained IDs also keep old retries idempotent. Expired match links become plain historical IDs. Include these tables in normal database backups. Run `npx vitest run tests/unit/bank.test.ts tests/unit/bank-overview.test.ts tests/client/bank-page.test.ts`, then the usual lint, type checks, tests and build. TWS supplies `node tools/check_bank.mjs` and `python3 tools/test_plugin_bank.py`. Live validation must cover both games: deposit/withdraw amounts and `all`, insufficient funds and caps, joining before telemetry, `.help`, score HUD updates, death/respawn/disconnect during requests, lost prepare/commit replies, controller outage, disk failures and map/process restart with pending work. Confirm earned score and leaderboard behavior, and verify that another server's credential and the telemetry credential fail. For the endpoint upgrade, also test a different enrolled controller address and a stale `tws_bank_url`, `.b` with an empty balance, `.w all` with no funds, and rebuild/removal with both legacy and public journals. Drop a settlement reply, restart the agent and verify the original IDs settle without duplicate ledger changes before the replacement starts or runtime storage is removed.