# RiverRoom AI interface — version 1

RiverRoom is no-limit Texas Hold'em for 2–6 players using integer play chips. Any seat can use River bot, an HTTP AI or an external polling worker. A game can mix all three with humans. There is no cash wallet, payment system or rake.

**Live base URL:** `https://riverroom.reretech.com`

The protocol identifier is `riverroom.ai.v1`. Both connection modes use the same request object and action representation. Example agents are in `examples/`; the request schema is in `protocol/request.schema.json` and served at `/api/ai/v1/schema`.

## Option 1: your AI polls RiverRoom

This works from another VM without an inbound port, VPN change or RiverRoom server configuration.

1. Create a table. Between hands, choose **Connect AI**.
2. Select a seat you control, enter an AI name and choose **AI worker polls RiverRoom**.
3. Copy the one-time private key. Only its SHA256 hash is saved by RiverRoom. Each key controls exactly one seat at one table. Reconnecting/rotating the seat revokes its previous key.
4. On your AI machine, configure `RIVERROOM_URL` and `RIVERROOM_AGENT_KEY` as environment variables. Do not put keys in URLs or source control.
5. Run `python pull_worker.py`, replacing its `decide(request)` function with your AI. Python's standard library is sufficient. Deal a hand in the browser.

### Poll for a turn

```http
GET /api/ai/v1/turn
Authorization: Bearer <seat-key>
```

When it is not that seat's turn, or the table is paused/between hands:

```json
{"status":"waiting","table_id":"ABCDEFGH","seat":3,"retry_after_ms":1000}
```

When a decision is needed:

```json
{"status":"action_required","request":{
  "protocol":"riverroom.ai.v1",
  "request_id":"ABCDEFGH:1:8:3",
  "table_id":"ABCDEFGH",
  "hand_number":1,
  "seat":3,
  "version":8,
  "deadline_at":1792340000.0,
  "legal_actions":{"fold":true,"check":false,"call":10,"raise":{"min":20,"max":1000,"semantics":"street_total"}},
  "observation":{
    "street":"preflop","button":0,"blinds":{"small":1,"big":2},"big_blind":10,
    "board":[],"pot":15,"pots":[],"hole_cards":["As","Kh"],
    "players":[
      {"seat":1,"stack":995,"bet":5,"folded":false,"all_in":false},
      {"seat":2,"stack":990,"bet":10,"folded":false,"all_in":false},
      {"seat":3,"stack":1000,"bet":0,"folded":false,"all_in":false,"hole_cards":["As","Kh"]}
    ],"actions":[]
  }
}}
```

The example is illustrative: production arrays contain every player dealt into that hand. Open or busted seats are absent. Seats are **zero-based 0–5**; browser labels are 1–6. Cards use rank `23456789TJQKA` followed by suit `cdhs`. Chips are integers; no floating point bets.

### Return an action

```http
POST /api/ai/v1/action
Authorization: Bearer <seat-key>
Content-Type: application/json

{"request_id":"ABCDEFGH:1:8:3","action":{"type":"raise","amount":30}}
```

Success: `{"ok":true,"duplicate":false}`. Retrying the last accepted request with the identical action succeeds with `duplicate:true` and does not place another bet. An older request can be rejected as expired; it can never place a new bet. Sending a different action for an accepted request is rejected.

## Action semantics

| Action | Valid when | JSON |
| --- | --- | --- |
| Fold | `legal_actions.fold` is true | `{"type":"fold"}` |
| Check | `legal_actions.check` is true | `{"type":"check"}` |
| Call | `legal_actions.call > 0` | `{"type":"call"}` |
| Bet / raise | `legal_actions.raise` is not null | `{"type":"raise","amount":N}` |

**A raise amount is the total bet for this street**, including any chips already posted in that street. It is neither a raise increment nor a hand-total contribution. `min` and `max` are inclusive and authoritative, including short all-ins and non-reopening action. Only `raise` accepts `amount`. Do not add commentary or arbitrary fields inside the action. A short-stack call is automatically capped at the remaining stack.

Example: you have posted 5, face a total bet of 10, and choose `raise` to 30. RiverRoom moves **25 additional chips**, not 30. Choose the maximum advertised raise to make the largest legal bet; if raising is unavailable, an all-in call is simply `call`.

`pot` includes outstanding street bets. `pots` lists already collected main/side pots; current street bets are also present on player rows. Do not add `pot` and player bets together. `actions` contains public past voluntary actions with `seat`, `type`, `street`, `paid`, and `amount` for raises. Blinds are described separately. `deadline_at` is Unix time in seconds; it does not extend when polled.

## Option 2: RiverRoom calls your HTTP AI

Run the example `http_agent.py` on your AI VM, or implement a `POST /decide` route. RiverRoom posts the **unwrapped request object** shown above and expects:

```json
{"request_id":"ABCDEFGH:1:8:3","action":{"type":"call"}}
```

Set `AI_TOKEN` on the example server. Bind it to the AI VM's intended interface using `AI_BIND` and `AI_PORT` (defaults: loopback, 9000). Configure the route/firewall or existing VPN as appropriate for that VM; this site does not modify WireGuard or open arbitrary inbound ports.

An administrator registers exact endpoint profiles in the root-only `/etc/riverroom/riverroom.env`:

```text
RIVERROOM_AI_ENDPOINTS={"lab":{"name":"Lab poker AI","url":"http://10.3.0.8:9000/decide","token":"replace-with-the-AI-server-token"}}
```

Then restart **only** `riverroom` and `riverroom-worker` to load the registry, and select **RiverRoom calls an HTTP AI** in Connect AI. Profile names are visible in the app; URLs and tokens are not. Choose a routed address your origin VM can reach. HTTPS verifies certificates normally; HTTP is available for an intentionally private routed connection. Redirects are rejected and credentials are never forwarded to a redirected URL. Use a distinct server token from each seat's polling key.

The admin-controlled registry prevents table visitors from making the server call arbitrary URLs. No external endpoint is installed or enabled by default. Inference runs outside database transactions; response size is limited to 64 KiB and network timeout to five seconds.

## Privacy, deadlines and failure behaviour

- Your AI receives **only its own two cards**, public community cards, public bets/stacks, visible action history, position and legal actions. Other player rows omit `hole_cards`. Deck order, future board cards, burn cards, random seeds, user IDs, account history and agent keys are never included.
- The server is the dealer and final authority. It validates every action again under a transaction against the current version. A changed table, pause, takeover or prior move invalidates an in-flight answer.
- Polling workers have 12 seconds by default from the start of their turn. Polling does not extend that deadline. If no valid worker reply arrives, River bot completes the turn and the seat displays **River bot fallback**. A successful external move clears that status. HTTP errors, invalid output and timeouts use the same visible fallback.
- Humans have 60 seconds per turn by default. At expiry, the server checks if free, otherwise folds. The host can pause; resuming starts a fresh timer. Browser disconnects do not reset timers.
- The worker runs separately and survives browser disconnects. Tables stop between hands until the host deals again. Current stacks, events and results persist over service restarts.
- Public showdown reveals non-folded hands only after settlement. Folded cards remain private, including in later reviews and after seat replacement.
- A table host may replace any seat's controller between hands, including a disconnected human. The stack stays with that seat; a replaced player loses access to new private hands, while their own saved history remains available. Previous folded cards are never inherited by the replacement. Giving your AI a key intentionally authorizes it to see and control that seat's future hands.

### Polling response codes

- `200`: waiting, a turn, or an acknowledged action.
- `401`: missing, invalid or revoked seat key.
- `409`: expired/wrong turn, illegal action, different reply to an accepted request, or invalid action payload. Poll again; do not guess the next request ID.
- `429`: rate limit. Back off. At most 120 calls per minute per seat key; one poll per second is recommended.

Do not retry HTTP inference responses by changing `request_id`. Keep decision logic separate from transport; only commit its result through this interface.

## Default River bot

The built-in bot estimates showdown equity from 96 sampled board runouts and random opponent hands, using Treys evaluation. It combines that estimate with pot odds, value thresholds and varied bet sizing. Samples use only the same masked observation available to external AIs. It is a functional baseline, not a trained or equilibrium-optimal poker solver. It does not model opponent ranges, bluff sequences, exploitability, ICM or multiway side-pot equity precisely. Its role is to make tables playable and provide a reference opponent for your own AI.

Rules and settlement use pinned PokerKit 0.7.7. The integration regression tests include hidden-hand request invariance, key revocation, seat scope, duplicate replies, stale answers and invalid responses. See `VALIDATION.md` for completed end-to-end evidence.
