> ## Documentation Index
> Fetch the complete documentation index at: https://docs.testnet.dev.adipredictstreet.com/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSocket overview

> Two gateways (/ws/market and /ws/user), X-Api-Key at handshake, Kalshi-style command envelope, channels keyed by tokenId / conditionId.

PredictStreet exposes WebSocket streaming through two separate
gateways that share one client protocol. Pick the gateway by what
data you want; never mix subscriptions across the two on one socket.

Both gateways authenticate with **`X-Api-Key`** — the same key you
use on HTTP, same scope rules.

| Gateway    | Path         | Auth                                 | What lives here                                              |
| ---------- | ------------ | ------------------------------------ | ------------------------------------------------------------ |
| **market** | `/ws/market` | `X-Api-Key`                          | public trades, orderbooks, market lifecycle, platform status |
| **user**   | `/ws/user`   | `X-Api-Key` (`portfolio:read` scope) | own orders / fills / account-control events                  |

The protocol is **Kalshi-style** (envelope with `id` / `cmd` / `params`)
and **Polymarket-style identifier lists** (`channel + ids[]`).

## Connect

Same handshake contract for both gateways:

```
wss://ws-gateway.dev.predictstreet.sde.adifoundation.ai/ws/market
wss://ws-gateway.dev.predictstreet.sde.adifoundation.ai/ws/user
```

Pick one of:

* `X-Api-Key: ps_live_<keyId>_<secret>` header on the upgrade request
* `?key=<token>` query parameter — for browser clients that can't set
  custom headers on the WebSocket upgrade

The server pushes one greeting after the upgrade:

```json theme={null}
{
  "type": "connected",
  "data": {
    "gateway":       "user",
    "walletAddress": "0x1111111111111111111111111111111111111111",
    "authMethod":    "api_key"
  }
}
```

On `/ws/market` the greeting is just `{ "gateway": "market", "authMethod": "api_key", "protocolVersion": 2 }` — no wallet context, since market data isn't per-user.

<Tip>
  Auth is validated **at handshake only**. Rotating a key does not
  invalidate an open socket; close and reopen to switch identity. A
  bad key closes with `4401 <reason>` (e.g. `4401 api_key_revoked`).
  Revoking a key closes every live socket bound to that `keyId`
  immediately via `apikey:invalidate` Redis pub/sub.
</Tip>

## Command envelope

Every client-to-server message uses the same shape:

```json theme={null}
{ "id": 1, "cmd": "subscribe", "params": { /* ... */ } }
```

* `id` — client-side correlation id; echoed in the server's response
* `cmd` — `subscribe` / `update_subscription` / `unsubscribe` /
  `list_subscriptions` / `ping`
* `params` — command-specific payload

Every accepted subscription gets a connection-local **`sid`** (numeric).
Use `sid` for subsequent updates / unsubscribes / event routing on
the client side. `sid`s are **not** stable across reconnects.

## Channel catalog

Public, on `/ws/market`:

| Channel                   | Identifier model            | Required scope | Purpose                                             |
| ------------------------- | --------------------------- | -------------- | --------------------------------------------------- |
| `token_trade_matches`     | `ids = [tokenId, ...]`      | —              | low-latency tape from the matcher (off-chain match) |
| `token_trade_settlements` | `ids = [tokenId, ...]`      | —              | chain-confirmed settlements (`OrderFilled` indexed) |
| `token_book`              | `ids = [tokenId, ...]`      | —              | public orderbook snapshots + updates                |
| `token_ohlc`              | `ids = [tokenId, ...]`      | —              | rolling 5-second OHLC candles                       |
| `condition_lifecycle`     | `ids = [conditionId, ...]`  | —              | market lifecycle, pause/unpause, oracle, payout     |
| `system`                  | `ids = ["platform_status"]` | —              | platform-wide freeze / maintenance banner           |

Private, on `/ws/user`:

| Channel           | Identifier model                      | Required scope   | Purpose                                            |
| ----------------- | ------------------------------------- | ---------------- | -------------------------------------------------- |
| `user_orders`     | no `ids` (scoped to the key's wallet) | `portfolio:read` | own `order_placed` / `order_cancelled`             |
| `user_fills`      | no `ids` (scoped to the key's wallet) | `portfolio:read` | own `user_fill` (chain-confirmed fills)            |
| `vault_positions` | `ids = [vaultAddress, ...]`           | `portfolio:read` | per-vault balance changes / split / merge / redeem |

Detailed payloads + examples in
[Subscriptions](/concepts/websocket/subscriptions) and
[Messages](/concepts/websocket/messages).

## Where do `tokenId` and `conditionId` come from?

Market data is keyed by **on-chain native ids**, not by app symbol.
Pull them from REST first:

* `GET /api/markets/{symbol}` → `conditionId` / `questionId` /
  `yesTokenId` / `noTokenId` (binary)
* For neg-risk multi-outcome markets, each question has its own
  `conditionId` and yes/no token ids — resolve them through the
  on-chain `NegRiskAdapter` before subscribing

Then open `/ws/market` and subscribe by those native ids:

```json theme={null}
{
  "id": 1,
  "cmd": "subscribe",
  "params": {
    "subscriptions": [
      { "channel": "token_trade_matches",     "ids": ["12345..."] },
      { "channel": "token_trade_settlements", "ids": ["12345..."] },
      { "channel": "token_book",              "ids": ["12345..."] },
      { "channel": "condition_lifecycle",     "ids": ["0xabc..."] }
    ]
  }
}
```

Don't subscribe by app symbol — symbols are app metadata, not the
canonical on-chain identity.

## What clients should NOT do

* **No symbol-based subscriptions.** Use `tokenId` / `conditionId`.
* **No invented room strings** like `vault:0xabc:erc1155`. Use
  `channel + ids` only.
* **No mixing of `/ws/user` and `/ws/market` on one socket.** The
  gateway rejects cross-gateway channels with `forbidden` — open one
  socket per gateway.
* **No assumptions about `sid` stability across reconnects.** After
  any close, re-subscribe from scratch.

## Next

<CardGroup cols={2}>
  <Card title="Subscriptions" icon="radio" href="/concepts/websocket/subscriptions">
    Per-channel subscribe / update / unsubscribe payloads.
  </Card>

  <Card title="Message shapes" icon="envelope" href="/concepts/websocket/messages">
    Outbound event envelope and full event catalog by channel.
  </Card>

  <Card title="Reconnect" icon="rotate" href="/concepts/websocket/reconnect">
    Heartbeat, sid rebuild, snapshot resync.
  </Card>

  <Card title="API keys" icon="key" href="/auth/api-keys">
    Scope catalog, rotation, revoke.
  </Card>
</CardGroup>
