# Tequity wallet: launches, balances, wagers and wins

For operators whose wallet speaks Tequity's seamless protocol. The RGS calls the wallet the way Tequity's platform does:
- `/authenticate` at launch;
- `/balance`;
- `/transaction` for every wager and win;
- `/cancel` for a wager that never got an answer.

Jackpot games on these sessions are not available yet. Design: `docs/design/tequity-wallet-connector.md`. Free bets: [tequity-free-bets.md](tequity-free-bets.md).

## Set-up (by the RGS operator, in `TEQUITY_WALLETS_FILE`)

| Field | What |
| --- | --- |
| `keyId` | The operator API key the wallet acts as. It needs `launch`, plus `campaigns` for free bets. Revoked or expired, launches stop. |
| `secret` | The shared HMAC secret, at least 32 characters. The RGS signs every call with it (`X-Server-Authorization`, hex HMAC-SHA256 of the exact body). |
| `baseUrl` | The wallet's API base. The RGS calls `<baseUrl>/authenticate` and `<baseUrl>/balance` over HTTPS, to public addresses only. |
| `gameOrigin`, `rgsOrigin` | Where the game clients are served, and this RGS's public origin, passed to the client as `rgs_url`. |
| `sessionKey` | 64 hex characters. It encrypts the player tokens the wallet issues. |
| `provider` | Optional; sent as `provider`. Default `232rgs`. |
| `immediateResends` | Optional, 0–5, default 2: how many times a transaction without an answer is resent at once, before the slower recovery takes over. |

## Launch

The operator embeds `https://<rgs>/tequity/launch/real?wallet=…&operator=…&game=…&key=…` (GET or POST; query or JSON body). The RGS redirects (303) to the game. Alternatively, `https://<rgs>/tequity/wallet/{wallet}/launch/real` with the same parameters returns `{"url": "…"}`.

| Parameter | Rule |
| --- | --- |
| `wallet` | The wallet's name in the RGS set-up. |
| `operator` | Must equal the acting key's operator code. |
| `game` | The RGS game code; enabled for the operator in the player's currency. |
| `key` | The wallet's one-time session key. The RGS never stores it. |
| `currency` | Optional, lowercase. It must equal the currency the wallet returns. |
| `language` | Optional, for example `en` or `en-GB`. |

`depositUrl`, `refreshUrl`, `lobbyUrl`, `exitTarget` and `theme` are accepted and not used yet.

**What the RGS does:**
1. Calls the wallet's `POST /authenticate` with `{operator, key, wallet, provider, game, ip}`.
2. Finds or creates the player's account for (`nativeId`, currency).
3. Keeps the returned `token` encrypted with the launch, together with `brand`, `country`, `jurisdiction`, `campaignTypes` and `sessionData` for later slices.
4. Issues a short-lived native launch token. The game URL carries only that token, never the wallet's key or token.

## Balance

When the game client opens its session and whenever it asks for the balance, the RGS calls the wallet's `POST /balance` with `{nativeId, game, provider}`, the player's token (`Authorization: Bearer`) and the signature. Amounts are exact: a balance with more decimals than the currency has is refused, not rounded.

## Wagers and wins

Every round is one withdraw (the stake) and one deposit (the win). A lost round's deposit is 0, and the deposit carries `roundFinished: true`. Each is a `PUT <baseUrl>/transaction`, signed, with the player's token as `Authorization: Bearer`:

```json
{ "nativeId": "ana", "transactionId": "<uuid>", "type": "withdraw", "provider": "232rgs", "amount": 1,
  "game": "fruit-oasis", "roundId": "<uuid>", "category": "normal", "currency": "eur" }
```

- **Ids:** `transactionId` is the RGS operation id, different for the withdraw and the deposit. `roundId` is the RGS round id, the same on both.
- **Amounts:** major units, exact. The answer is `{"balance": …}`.
- **What the wallet must guarantee** (Tequity's spec):
  - A `transactionId` it has processed is never applied again: a repeat returns the current balance.
  - A cancelled `transactionId` can never be used.
  - A cancel of an id it never saw returns the balance, as if it had been cancelled.
  - Zero withdrawals and deposits work.

### Failures and retries

| Wallet answer | Withdraw | Deposit |
| --- | --- | --- |
| `{"balance": …}` | Done. | Done. |
| `UNKNOWN`, `TRANSACTION_NOT_FOUND`, a 5xx without a code, a timeout, an unreadable answer | Resent at once (`immediateResends`). If still unanswered, the RGS cancels it: `DELETE <baseUrl>/cancel` with `{nativeId, transactionId, roundId}`, repeated with backoff (2, 4, 8 … 60 s) until confirmed or the finite recovery budget is exhausted. The round then ends cancelled. | Resent at once, then resent with the same backoff until confirmed or the finite recovery budget is exhausted. Never cancelled. |
| Recognized final code (`INSUFFICIENT_FUNDS`, `LOSS_LIMIT`, `TRANSACTION_FAILED`, `PLAYER_UNAUTHORIZED`, `SESSION_EXPIRED`, `SERVER_UNAUTHORIZED`, `BLOCKED_TERRITORY`) | Refused: the round is cancelled and nothing is cancelled at the wallet. | The round goes to manual review, and the win stays owed. |

- **Recovery is bounded:** the default recovery budget is 10 attempts. Exhaustion escalates unresolved work to manual review; a deposit stays owed and must never be silently discarded. Unrecognized error codes and unreadable replies are ambiguous, not final rejections.
- **Resends are byte-identical:** each body is stored before the first send.

## Conformance (before go-live)

Before a wallet may launch players on a staging or real-money RGS, we run a conformance check against it. The retry rules above are only safe if the wallet behaves as Tequity's spec demands. Until a run passes, launches answer `409 {"error":"TEQUITY_WALLET_NOT_CONFORMANT"}`. A pass counts for 90 days.

**What we need from you:** a one-time launch key for a test player with at least the test amount (default 1.00 in the test player's currency) on balance.

**What the run checks**, all inside one test round:
1. `/authenticate` and `/balance` agree.
2. A withdraw takes the amount.
3. The same withdraw sent again (same `transactionId`, same bytes) takes nothing.
4. A cancel refunds it.
5. The same cancel again changes nothing.
6. The cancelled id is refused if sent again.
7. A cancel of an id you never saw returns the balance.
8. That id is then refused.
9. A withdraw of 0 and a deposit of 0 with `roundFinished` work.
10. A withdraw above the balance is refused with `INSUFFICIENT_FUNDS`.
11. The test player ends where it started.

**Net zero:** at the end the run cancels every withdraw id it used, so the test player ends where it started even if a check failed. If our process stops mid-run, our worker does the same. If your wallet wrongly accepts a cancelled id, the run gives the money back with a deposit.

**What it uses:** we run it during onboarding with `tools/tequity-conformance.ts`, and every result is recorded. It does not use your free bets or any real game round.

## Free rounds

Free bets the wallet adds through the [free bets API](tequity-free-bets.md) are offered to the game client. It shows the player's free rounds and plays one by sending its id ([client API](client-api.md), Free rounds).

**The wallet calls:**
- **The withdraw** has `amount: 0` and `category: "promo"`.
- **The deposit** carries the win (0 for a loss), in the campaign's payout category (`promo` for free bets), with `roundFinished: true`.
- **Both carry:**
  - `campaignType: "freeBets"`;
  - `campaignId`: the wallet's own `walletCampaignId`;
  - `campaignData: {total, used, amount, totalWin}`: the grant's free rounds, the rounds used including this one, the stake per round, and the grant's wins so far including this one, in major units.

**Set-up:** the operator needs the `free-rounds` and `free-rounds-external` modules (the second is the opt-in for free rounds through an external wallet).

## Session limits

`sessionData.betConfig` from `/authenticate` sets the session's bet limits, on top of the game's own:
- **Client offer:** the game client is offered only the bet levels within `minBet`–`maxBet`. `defaultBet` is the default when it is one of them.
- **Wagers:** a wager whose stake is below `minBet` or above `maxBet` is refused with `ERR_GE` before the wallet is called.
- **Bonus buys:** a bonus buy, where the cost is not the stake, is held to `maxBonusBet`, or `maxBet` when that is not set.
- **Rounding:** values with more decimals than the currency has are rounded inward, a minimum up and a maximum down.
- **Unreadable values:** a value that is not a non-negative number refuses play (`ERR_GE`) rather than allowing it.
- **Not applied yet:** `maxExposure`.

## Errors

| Status | Body | When |
| --- | --- | --- |
| 401 | `{"error":"TEQUITY_PLAYER_UNAUTHORIZED"}` | The wallet answered `PLAYER_UNAUTHORIZED` or `SESSION_EXPIRED` (for example, a used key). |
| 403 | `{"error":"TEQUITY_BLOCKED_TERRITORY"}` | The wallet answered `BLOCKED_TERRITORY`. |
| 502 | `{"error":"TEQUITY_WALLET_FAILED"}` | Any other wallet error, an unreadable answer, a timeout, or a balance that is not exact. |
| 400 | `{"error":"TEQUITY_OPERATOR_MISMATCH"}`, `{"error":"TEQUITY_CURRENCY_MISMATCH"}`, `{"error":"INVALID_REQUEST"}` | Wrong operator, a currency different from the wallet's, or invalid parameters. |
| 404 | `{"error":"TEQUITY_WALLET_NOT_ENABLED"}`, `{"error":"TEQUITY_GAME_OR_CURRENCY_NOT_PROVISIONED"}` | Unknown wallet, or the game not enabled in that currency. |
| 503 | `{"error":"TEQUITY_WALLET_KEY_UNAVAILABLE"}` | The acting key is revoked, expired or lacks `launch`. |

In the game client's own protocol (`/wallet/authenticate`, `/wallet/balance`, `/wallet/play`):
- A wallet session that expired is `ERR_IS`, and other balance failures are `ERR_GE`.
- A refused wager is `ERR_IPB`.
- A play still waiting on the wallet is `ERR_UE`. The round resumes on the client's next authenticate; if it ended cancelled, that is reported once as `ERR_IPB`.
