> ## 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.

# Reconnect & resync

> How to recover from disconnects, ping/pong, sid lifecycle, orderbook gap detection.

WebSocket delivery is **at-least-once** during normal operation. On
disconnect or reconnect you must rebuild subscriptions and reconcile
against REST.

## sid lifecycle

`sid`s are **connection-local**. They are NOT preserved across:

* Network drops
* Client-side `WebSocket.close()` + reopen
* Server-side restarts

After every reconnect, run a fresh `subscribe` for everything you
need. Old `sid`s from the previous connection are meaningless.

## Heartbeat

Send `ping` every 25 seconds (or whenever your TCP connection feels
quiet):

```json theme={null}
{ "id": 999, "cmd": "ping" }
```

Server responds:

```json theme={null}
{ "id": 999, "type": "pong", "ts": 1776949200000 }
```

If you don't get a `pong` within \~5 seconds, treat it as a dead
connection — close and reconnect.

`ts` is the server clock in ms. Use it as a clock-skew probe if you
care about latency-sensitive timing on the client.

## Reconnect strategy

```typescript theme={null}
let reconnectDelay = 1000;

function connect() {
  // Node / server: pass X-Api-Key as a header.
  // Browser:       use `${WSS}/ws/user?key=<encodeURIComponent(apiKey)>`
  //                because the WebSocket(…) ctor can't set custom headers.
  const ws = new WebSocket(`${WSS}/ws/user`, {
    headers: { 'X-Api-Key': apiKey },
  });

  ws.onopen = () => {
    reconnectDelay = 1000;
    // Always re-subscribe from scratch on a new connection.
    ws.send(JSON.stringify({
      id: nextId(),
      cmd: 'subscribe',
      params: { subscriptions: mySubscriptions },
    }));
  };

  ws.onclose = () => {
    // Drop sid → handler map; on next open you'll re-subscribe and
    // get fresh sids.
    sidHandlers.clear();
    setTimeout(connect, reconnectDelay);
    reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
  };

  ws.onmessage = handleMessage;
}
```

After a successful reconnect-and-resubscribe, **reconcile state from
REST** — don't trust the WS to have replayed missed events from before
the disconnect.

## Channel-specific reconnect / resync

| Channel                                           | On reconnect, also fetch                                                                                                                                                                                                                                    |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_orders`                                     | `GET /api/orders/open`                                                                                                                                                                                                                                      |
| `user_fills`                                      | `GET /api/me/fills?since=<lastSeen>` (or `GET /api/me/balances` + `GET /api/me/positions` for absolute state)                                                                                                                                               |
| `vault_positions`                                 | `GET /api/vaults/{addr}/positions`                                                                                                                                                                                                                          |
| `token_trade_matches` / `token_trade_settlements` | `GET /api/markets/{symbol}/trades?after=<lastSeenCreatedAt>&before=<now>` to backfill missed fills (uses each trade's `createdAt` as the cursor — that field stays immutable across status transitions). Omit `after` on first load to get the latest page. |
| `token_book`                                      | re-subscribe → `book_snapshot` pushed automatically, OR call `get_book_snapshot` mid-session                                                                                                                                                                |
| `token_ohlc`                                      | `GET /api/markets/{symbol}/ohlc?interval=s5&before=<now>`                                                                                                                                                                                                   |
| `condition_lifecycle`                             | `GET /api/markets/{symbol}` for current `status`                                                                                                                                                                                                            |
| `system`                                          | `GET /api/platform/status`                                                                                                                                                                                                                                  |

## Orderbook resync

`token_book` deltas carry `seq` and `prevSeq`. Track the last `seq`
you applied per `tokenId`. If the next delta arrives with
`prevSeq != lastSeq`, request a fresh snapshot — without dropping the
existing subscription:

```typescript theme={null}
if (delta.prevSeq !== lastSeq[delta.id]) {
  // Gap — ask the gateway to re-push a snapshot on this sid.
  ws.send(JSON.stringify({
    id: nextId(),
    cmd: 'get_book_snapshot',
    params: { sid: delta.sid },
  }));
}
```

If the snapshot fetch fails the server pushes
`{ "type": "book_snapshot_failed", "data": { "tokenId", "reason", "tsMs" } }`
on the same `sid`. Back off and retry, or fall back to
`GET /api/markets/{symbol}/orderbook?outcome=N&depth=100`.

In v1 the only way to force a fresh `book_snapshot` was an
unsubscribe + resubscribe round-trip — `get_book_snapshot` removes
that gap.

## Auth on reconnect

Auth is validated **at handshake only**. If a key rotates mid-session,
the open socket keeps working with the original credential until it
closes. Close + reopen to pick up the new one.

| Reason                                                | Close code               | What to do                                                                      |
| ----------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------- |
| `api_key_revoked` / `api_key_bad_secret`              | `4401 <reason>`          | Rotate the key on the admin panel; don't auto-retry a revoked key.              |
| `api_key_expired`                                     | `4401 api_key_expired`   | Issue a fresh key.                                                              |
| `api_key_suspended`                                   | `4401 api_key_suspended` | Partner is suspended. Contact ops.                                              |
| `api_key_unknown_key` / `api_key_bad_format`          | `4401 <reason>`          | Token on disk is stale or malformed. Refresh from your secrets manager.         |
| `api_key_ip_denied`                                   | `4401 api_key_ip_denied` | Caller IP isn't in the key's allowlist.                                         |
| `api_key_auth_disabled` / `api_key_auth_unconfigured` | `4401 <reason>`          | Server-side misconfig (feature flag off or pepper missing). Back off and alert. |
| `forbidden origin`                                    | `1008 forbidden origin`  | WebSocket `Origin` header not in the allowlist.                                 |

Revoking a key closes every live socket bound to that `keyId`
immediately via a Redis `apikey:invalidate` pub/sub — clients see the
`4401 api_key_revoked` frame within milliseconds.
