# OrbiPoker: AI agent integration guide

A complete workflow for an AI agent that implements the OrbiPoker integration inside an operator's platform (casino, sportsbook or aggregator). Read it together with `/openapi.json`, which is the source of truth for schemas. Do not infer endpoints from examples and do not call `/api/console/*`: that is the internal back office, not a public API. Contract: Operator API v1, wallet payload `version: 1`, SDK v1.

## Sources

All public, no login, readable without JavaScript. Paths are relative to the OrbiPoker host.

- `/integration-agent.md`: this guide
- `/openapi.json`: OpenAPI 3.1, operator API plus the `wallet` webhook
- `/docs`: human-readable docs (en, pt-BR, es), including the configuration reference and game rules
- `/sdk/v1/orbipoker.js`: iframe SDK
- `/wallet-receiver.mjs`: reference wallet receiver (Node 24, `node:sqlite`)
- `/llms.txt`: index

## 1. Scope and inputs

OrbiPoker is real-money poker. The operator owns the player account, KYC, payments and the balance. OrbiPoker runs the game and calls the operator's wallet for every chip movement. Amounts are **integers in the currency's minor unit** (cents; CLP and COP have 0 decimals).

Before writing code, locate in the operator's system: player authentication, the wallet/ledger service and its transaction model, the page or app that will host poker, its CSP, and where server secrets live. Confirm the exact production and staging origins, the player id to use (stable, never reused, 1–128 chars of `[A-Za-z0-9_.@:+-]`), the currencies and the default language.

Secrets come from the environment or secret manager. Never ask for them in chat, never print them, never commit them. If a credential is missing, implement against the sandbox and mocks and report the missing prerequisite instead of inventing one.

```
ORBIPOKER_URL=https://<orbipoker-host>
ORBIPOKER_API_KEY=<secret reference>        # orbp_sk_…, server only
ORBIPOKER_WALLET_SECRET=<secret reference>  # whsec_…, server only
ORBIPOKER_TENANT_ID=<from GET /api/v1/me>
```

OrbiPoker staff create the operator account and hand over the first API key and wallet secret once. In the back office (`/backoffice`) the operator sets the wallet URL, the allowed embedding origins (HTTPS, no path, up to 20), theme and formats, and can rotate keys and the secret.

## 2. Server: create a launch per poker open

```
POST {ORBIPOKER_URL}/api/v1/launch
Authorization: Bearer {ORBIPOKER_API_KEY}
Content-Type: application/json
Idempotency-Key: <8–100 chars [A-Za-z0-9_-], optional>

{"playerId":"42","currency":"BRL","locale":"pt-BR","country":"BR",
 "parentOrigin":"https://casino.example","sessionToken":"<your session ref>",
 "nickname":"AnaRiver","lobby":"cash"}

201 {"launchId":"lc_…","launchUrl":"https://<orbipoker-host>/launch/…","expiresAt":1792443120000}
```

- Expose a route in the operator backend (for example `POST /poker/launch`) that requires the operator's own player session, builds this request from server-side data only (never trust a `playerId` from the browser) and returns `launchUrl`.
- Create a fresh launch every time. The URL is single use and expires after 120 s. The resulting player session lasts 12 h; on `orbipoker:session-expired` call your route again.
- `parentOrigin` must exactly match an allowed origin or the call fails with `403 ORIGIN_NOT_ALLOWED`. Unknown body fields fail with `400 INVALID_BODY`. Blocked players get `403 PLAYER_BLOCKED`.
- Branch on the JSON `code`, never on `error` text. Respect `429` with `Retry-After`.

## 3. Browser: embed with the SDK

```html
<div id="poker"></div>
<script src="https://<orbipoker-host>/sdk/v1/orbipoker.js"></script>
<script>
  async function openPoker() {
    const {launchUrl} = await fetch('/poker/launch', {method: 'POST', credentials: 'same-origin'}).then(r => r.json());
    return OrbiPoker.mount({
      container: '#poker', launchUrl,
      onBalance: ({balance, currency}) => header.setBalance(balance, currency),
      onDeposit: () => cashier.open(),
      onExit: () => location.assign('/casino'),
      onSessionExpired: () => { poker.destroy(); openPoker().then(p => poker = p); },
      onError: ({code}) => console.warn('poker', code)
    });
  }
  let poker; openPoker().then(p => poker = p);
</script>
```

- The operator page CSP must allow the OrbiPoker host in `script-src` and `frame-src`.
- After a deposit in your cashier call `poker.refreshBalance()`. `poker.setLocale('es')` changes language. `poker.destroy()` removes the iframe and listeners.
- Without the SDK: validate `event.origin === ORBIPOKER_URL`, `event.source === iframe.contentWindow` and `data.source === 'orbipoker' && data.version === 1`. Events: `orbipoker:ready`, `resize`, `balance`, `deposit`, `exit`, `session-expired`, `error`, `table-opened`, `hand-complete` (all prefixed `orbipoker:`).

## 4. Wallet webhook

One HTTPS endpoint on the operator side. Start from `/wallet-receiver.mjs` or port its logic.

Request body (raw JSON) and header:

```
X-OrbiPoker-Signature: t=1792443000,v1=<hex HMAC-SHA256(ORBIPOKER_WALLET_SECRET, t + "." + rawBody)>

{"version":1,"event":"debit","transactionId":"<uuid>","referenceTransactionId":null,"tenantId":"…",
 "playerId":"42","currency":"BRL","amount":5000,"reason":"cash_buyin","tableId":"t_…",
 "tournamentId":null,"handId":null,"sessionToken":"…","createdAt":1792443000123}
```

Required behaviour, in order:

1. Verify the signature over the **raw bytes** before parsing, in constant time, with a 300 s timestamp tolerance. Failure: `401`.
2. Reject a `tenantId` that is not yours, a non-integer or negative `amount`, an unknown player or currency (`400`).
3. Open one database transaction per request and lock the player's wallet row (or use `BEGIN IMMEDIATE`).
4. Look up `transactionId`. If it exists, compare a stored hash of the raw body: different body gives `409`; same body replays the original outcome (2xx with the current balance, or the same decline). Never apply twice.
5. Apply the event:
   - `balance`: return `{"balance": n}`. Do not store; its `transactionId` is random.
   - `debit`: if a tombstone exists for this `transactionId`, refuse with `409` and move nothing. If funds are insufficient, store the decline and answer `402` (optional `{"message": "…"}` is shown to the player). Otherwise subtract and store.
   - `credit`: add and store. Credits must always succeed for a known player.
   - `rollback`: find `referenceTransactionId`. Applied debit not yet rolled back: add back its stored amount once and mark it. Declined or already rolled back: change nothing. Unknown: insert a tombstone keyed by `referenceTransactionId`. Store the rollback itself by its own `transactionId`.
6. Commit, then answer `200 {"balance": <new balance>}`.

Never answer non-2xx after money moved: a non-2xx debit means "not charged" to OrbiPoker, which then creates no chips and sends no rollback. Respond well within **8 s**. Debits are not retried; a debit timeout or network error is followed immediately by a `rollback` with `reason: "debit_timeout"`. Credits and rollbacks are retried with the identical body (exponential backoff from 2 s, capped at 30 min) for **24 h**, then marked `failed` for manual reconciliation.

Reasons: `cash_buyin`, `cash_topup`, `fastfold_buyin`, `fastfold_topup`, `tourney_buyin` (debits); `cash_cashout`, `cash_table_closed`, `cash_recovery`, `fastfold_cashout`, `tourney_refund`, `tourney_prize`, `tourney_bounty`, `tourney_recovery` (credits); `debit_timeout` (rollback). Store `reason`, `tableId` and `tournamentId` with each ledger entry.

## 5. Reconciliation and reporting

- Daily job: page through `GET /api/v1/transactions` (`limit` ≤ 500, `offset`) and compare with the operator ledger by `transactionId`. Alert on any `retrying`, `failed` or `unknown` row; `POST /api/v1/transactions/{id}/retry` resends a credit or rollback.
- `GET /api/v1/reports/ggr?from&to&groupBy=day|total`: rake, fees, Spin margin, `promoCost` (Cash Drops), `ggr`, `platformShare`, `networkSettlement`.
- `GET /api/v1/reports/players?from&to`: per-player hands, rake, cashNet, tournamentFees, tournamentEntries, for rakeback and loyalty.
- `GET /api/v1/players/{playerId}` includes `inPlay`: chips on tables and active tournament entries right now.

## 6. Responsible gaming

Wire the operator's existing tools to:

- `POST /api/v1/players/{playerId}/block` with `{"reason":"self-exclusion","until":<ms or null>}`: revokes sessions, removes the player from cash tables and pools and credits the chips back.
- `POST /api/v1/players/{playerId}/unblock`.
- `POST /api/v1/players/{playerId}/limits` with `{"maxBuyInPerDay":<minor units>|null,"maxTables":<n>|null,"sessionMinutes":<n>|null}`. The body replaces previous limits. Errors at buy-in time: `DAILY_LIMIT`, `TABLE_LIMIT`, `SESSION_LIMIT`.
- `POST /api/v1/players/{playerId}/kick`.

## 7. Acceptance tests (all must pass before production)

Run against the operator's staging wallet, using the sandbox tenant or a staging tenant.

1. A wrong signature, a timestamp older than 300 s and a modified body each get `401`.
2. Debit 3000 from 10000: `200 {"balance":7000}`. The same request again: `200 {"balance":7000}`, ledger unchanged.
3. Same `transactionId` with a different amount: `409`.
4. Debit larger than the balance: `402`; the duplicate gets `402` again.
5. Credit 500: balance 7500. Retried credit: still 7500.
6. Rollback of the debit in step 2: balance 10500. Second rollback and rollback duplicate: unchanged.
7. Rollback whose `referenceTransactionId` was never seen: `200`. The late debit with that id: non-2xx, balance unchanged.
8. Concurrent credit and debit for one player never lose an update.
9. Launch: the URL works once; a second load and a load after 120 s fail. An origin outside the allow-list cannot frame `/launch/…` (browser CSP).
10. iframe: `onReady` fires, a sit-down produces `onBalance` with the debited balance, Deposit and Exit call the operator handlers, and session expiry triggers a new launch.
11. Blocking a seated player returns their stack through a `cash_cashout` credit and the next launch gets `403 PLAYER_BLOCKED`.
12. The reconciliation job matches every OrbiPoker transaction from the test session.

The repository test `test/wallet-receiver.test.mjs` runs tests 1–7 against the reference receiver; reuse it as a template.

## 8. Report back

When done, report: files changed, where secrets are read from, the origins configured, which acceptance tests passed (with evidence), and any open prerequisite (missing key, origin not yet allow-listed, wallet not reachable over HTTPS). Do not claim production readiness while any test above is failing or untested.
