Skip to main content
All client commands use the command envelope:

Subscribe

Each gateway is a separate socket with its own sid namespace. Subscribe per gateway.

/ws/user — private

/ws/market — public

sids are connection-local — each gateway numbers from 1 independently, so a sid: 10 on the user socket and a sid: 10 on the market socket point at completely different subscriptions. Trying to subscribe to the wrong-gateway channel comes back as { "code": "forbidden" } in rejected[].

Normalization rules

  • All ids are trimmed and lowercased (addresses, conditionIds, market symbols, instrument ids).
  • tokenId must be a canonical decimal string (no leading zeroes, no hex); anything else is rejected with invalid_params. There is no * wildcard — to receive every token, use the all_* channels (see Public — platform-wide below).
  • conditionId is lowercased 32-byte hex.
  • market_price_tick ids are <symbol>:<outcomeIndex>, with the outcome index written without leading zeros (0–255).
  • Channels that take no ids (user_orders, user_fills, user_activity, notifications, the all_* channels, global_activity) reject a subscription that sends any.
  • Duplicate ids removed.

Rejection codes

A subscription that fails validation comes back under rejected[]:
The message field redundantly prefixes the code — that’s an artifact of the gateway, not a separate signal. Route by code; treat message as the human-readable detail.

Per-channel reference

Private — own activity (typed)

user_orders — own order lifecycle

  • Gateway: /ws/user
  • ids: not allowed (always scoped to the socket’s wallet)
  • Required scope: portfolio:read
  • Pushes: order_placed, order_cancelled, order_rejected, order_expired, trade_failed

user_fills — own fills at match time

  • Gateway: /ws/user
  • ids: not allowed (always scoped to the socket’s wallet)
  • Required scope: portfolio:read
  • Pushes: user_fill (source: "matcher") when the matcher fills one of your orders, before on-chain settlement. Use GET /api/me/trades for the settled view with fees.

vault_positions — per-vault balance / split / merge / redeem

  • Gateway: /ws/user
  • ids: your vault address (lowercased) — the vault of the wallet this socket is bound to
  • Required scope: portfolio:read
  • Pushes: vault_position_balance_changed, vault_position_split, vault_positions_merged, vault_position_redeemed
The only accepted id is the vault of the wallet this socket is bound to (the vaultAddress in the connected greeting). Any other vault comes back under rejected[] with forbidden, and so does every vault_positions subscribe on a socket that connected before the wallet’s vault was deployed — reconnect once the vault exists.

vault_activity — on-chain vault events

  • Gateway: /ws/user
  • ids: your vault address (lowercased); any other vault is forbidden
  • Required scope: portfolio:read
  • Pushes: trade_fill (an on-chain OrderFilled naming your vault, with txHash and fee), deposit_confirmed, withdrawal_confirmed, exchange_balance_synced, and the vault’s other on-chain events
The settled counterpart of user_fills. Payloads, and how trade_fill reports maker and taker orders, are in Server events.

user_activity — withdrawals and account events

  • Gateway: /ws/user
  • ids: not allowed (always scoped to the socket’s wallet)
  • Required scope: portfolio:read
  • Pushes: withdrawal_<state> on each status change of your withdrawals, vault_created, user_paused / user_unpaused

Private — notifications and account signals

notifications — in-app notification feed

  • Gateway: /ws/user
  • ids: not allowed (always scoped to the socket’s wallet)
  • Required scope: portfolio:read
  • Pushes: notification — one per notification added to the wallet’s web-app notification feed
Display-oriented (title and body text), governed by the notification preferences set in the web app, and best-effort — a frame missed while disconnected is not re-sent. Not a substitute for the typed channels above.

me_limits_invalidated — deposit-limit changes

  • Gateway: /ws/user
  • ids: API key — exactly your wallet (the greeting’s walletAddress); JWT session — none
  • Required scope: portfolio:read
  • Pushes: me_limits_invalidated when an on-chain change to the wallet’s deposit tier, caps or deposit / trading / withdrawal restriction is indexed
The frame carries no values: re-read GET /api/me/deposit-limits when it arrives, instead of polling.

bond_payout_credited — refunded challenge bonds

  • Gateway: /ws/user
  • ids: API key — exactly your wallet; JWT session — none
  • Required scope: portfolio:read
  • Pushes: bond_payout_credited when the bond of a challenge you raised is credited back — plus the reward when the proposal is overridden, the bond alone when the market is voided or the proposal cleared — and becomes claimable from the oracle

dispute_intent — disputes filed from the web app

  • Gateway: /ws/user
  • ids: API key — exactly your wallet; JWT session — none
  • Required scope: portfolio:read
  • Pushes: dispute_intent_submitted, dispute_intent_accepted, dispute_intent_rejected, dispute_intent_expired
Tracks disputes filed through the PredictStreet web app. A challenge you send on-chain yourself has no dispute intent, so this channel stays silent for it; see Challenges. For these three channels an API-key socket must name its own wallet: no ids is rejected with invalid_params, any other wallet with forbidden. The id is case-insensitive, and the ack lists the subscription without ids.

Public — trades

token_trade_matches — low-latency matcher tape

  • Gateway: /ws/market
  • ids: one or more outcome tokenIds (decimal-string)
  • Pushes: trade_matched (source: "matcher") per off-chain match
Fires the moment the matcher prints a trade — fastest tape feed, arrives several seconds before the chain settlement.

token_trade_settlements — chain-confirmed settlements

  • Gateway: /ws/market
  • ids: one or more outcome tokenIds
  • Pushes: trade_settled (source: "chain") on indexed OrderFilled
Use this for accounting-grade trade confirmations. Carries txHash, blockNumber, and orderHash.

Public — books

token_book — public orderbook snapshots + updates

  • Gateway: /ws/market
  • ids: one or more outcome tokenIds
  • Pushes: book_snapshot (initial), book_update (the whole book: all price levels on both sides), book_delta (diff against prevSeq), book_snapshot_failed (snapshot fetch couldn’t complete)
book_update is the whole book, not top-of-book. Each frame carries every resting price level on both sides, aggregated per price (price, total quantity, order count), as the book stands at seq. Overwrite your local book with it rather than patching; book_delta is the incremental form. Levels below the dust threshold are removed, the same filter the REST book uses.Depth: the initial book_snapshot (and get_book_snapshot) is capped at 100 levels per side, and so is a periodic recovery frame, sent as a book_update about every 5 minutes. Prices sit on a 0.01 grid, so a side never has more than 99 levels and neither cap cuts the book: every one of these frames carries the whole book.
After subscribe the server pushes one book_snapshot (depth 100), then book_update events with monotonically increasing seq. book_delta is emitted only when the gateway has contiguous local book state — if seq or prevSeq is missing on a producer frame the gateway drops the live book_update rather than emit a divergent delta.
Silent gaps look like idle. When the gateway drops a frame, you see no book_update for a while — the same way you’d see nothing on a quiet market. Don’t equate silence with “no change”. If you stop receiving book_update for the channel for ~30 s on a market you’d expect to be active, issue get_book_snapshot proactively to confirm state.
See Reconnect — orderbook resync for the gap-detection flow built around book_delta.prevSeq.

token_ohlc — rolling 5-second candles

  • Gateway: /ws/market
  • ids: one or more outcome tokenIds
  • Pushes: ohlc_update (interval: "s5") — fires whenever the matcher prints inside the active 5-second window
Cheaper bandwidth than the trade tape if you only need chart bars. Bars are emitted continuously while open and once final with isClosed: true.

market_price_tick — live probability chart

  • Gateway: /ws/market
  • ids: one or more <symbol>:<outcomeIndex> pairs — market symbol (case-insensitive, lower-cased in the ack) and 0-based outcome index
  • Pushes: price_tick — the outcome’s mark (the market’s chances value) for the current second
The live edge of GET /api/markets/{symbol}/price-history. At most about one frame per second per outcome, only while the market is OPEN and only when the outcome’s best bid, best ask or last trade changed — a quiet market is silent. No backfill: seed the chart from GET /api/markets/{symbol}/price-history?range=live.

Public — platform-wide

These channels take no ids and deliver events for every market on the platform. Each counts as one subscription.

all_trade_matches — every matcher trade

  • Gateway: /ws/market
  • ids: not allowed
  • Pushes: trade_matched, the same frames as token_trade_matches for every token — two per binary-market trade (the NO mirror)

all_trade_settlements — every chain settlement

  • Gateway: /ws/market
  • ids: not allowed
  • Pushes: trade_settled, the same frames as token_trade_settlements for every token

all_books — every orderbook change

  • Gateway: /ws/market
  • ids: not allowed
  • Pushes: book_update and book_delta, the same frames (same seq and hash) as token_book for every token; no book_snapshot on subscribe
Firehose volume. all_books sends a full-depth book_update plus a book_delta for every book change on the platform. A client that can’t keep up is disconnected once its outbound buffer fills (see Resource limits). Prefer token_book for the tokens you need. A connection that holds both a per-token channel and its all_* counterpart receives each frame once per sid.

global_activity — platform-wide trade feed

  • Gateway: /ws/market
  • ids: not allowed
  • Pushes: global_activity — one frame per matched trade, in the taker’s view, with market / event titles and the taker’s username when available
Use it for an activity feed rather than all_trade_matches: one frame per trade, no mirror to dedupe. Seed it from GET /api/activity/recent, which serves the same fields. Payloads for all four channels: Server events.

Public — lifecycle

condition_lifecycle — on-chain trading pause

  • Gateway: /ws/market
  • ids: one or more conditionIds (lowercased bytes32 hex)
  • Pushes: condition_trading_paused, condition_trading_unpaused
Use for the on-chain trading pause / unpause of a condition. Each frame carries { conditionId, pauser, txHash, blockNumber, tsMs }. Status transitions, disputes and resolution are keyed by market symbol: subscribe to market_lifecycle (below) for those.

market_lifecycle — per-market lifecycle

  • Gateway: /ws/market
  • ids: one or more market symbols (lower-cased in the ack)
  • Pushes: status_changed, market_disputed, market_resolved, and — on a crypto Up/Down window — crypto_window_opened / crypto_window_closed
Keyed by the app symbol rather than the on-chain conditionId, so one subscription per market page carries every market-scoped transition. Optional fields are omitted when absent, and the frames are a hint, not the record — re-fetch GET /api/markets/{slug} for authoritative state.

Public — crypto price

crypto_price_tick — live underlying-asset price

  • Gateway: /ws/market
  • ids: one or more crypto instrumentIds (BTC-USD, ETH-USD) — case-insensitive, lower-cased in the ack
  • Pushes: crypto_price_tick
The price of the coin, not of a market — one subscription per instrument serves every crypto market on that asset. price is an unscaled integer string plus a decimals exponent; no backfill on subscribe, and silence simply means no producer is active for that instrument. Full contract: Crypto price feeds.

system — platform-wide status

  • Gateway: /ws/market
  • ids: must be ["platform_status"]
The subscription is accepted, but no frames are published on this channel. To detect a platform freeze, poll GET /api/platform/status ({ frozen, reason, since, activeNotices }).

Resource limits

Per-connection caps. Exceeding one surfaces as a structured error, except the outbound buffer, where the connection is dropped: The id limit applies to the subscription’s whole id set: add_ids fails once the ids already on the sid plus the new ones (counted before duplicates are removed) exceed 100, and the subscription is left unchanged. The command window is fixed, not sliding: it starts with a command and lasts 60 s, and the first command after it starts the next one. Every command counts, ping included. The values above are the gateway defaults; the command rate is also the production value.

Update existing subscription

For id-based channels (token_trade_matches, token_trade_settlements, token_book, token_ohlc, market_price_tick, condition_lifecycle, market_lifecycle, crypto_price_tick, vault_positions, vault_activity, system) you can change ids on an existing sid instead of opening a new subscription. The other channels (user_orders, user_fills, user_activity, notifications, dispute_intent, bond_payout_credited, me_limits_invalidated, the all_* channels and global_activity) have no editable id list, so update_subscription returns invalid_params for them.

Add ids

Remove ids

Response (server returns the resulting full id set):

Snapshot refresh: get_book_snapshot

The get_book_snapshot command lets a token_book subscriber request a fresh snapshot without unsubscribing / resubscribing. Useful when a sequence gap is detected (book_delta.prevSeq != lastSeq).
Or by token ids when you’d rather not look up the sid:
The sid must be a token_book subscription; add ids next to it to refresh only some of its tokens. The server acknowledges with { "id": 7, "type": "ok", "channel": "token_book", "sid": 12, "ids": ["12345..."] } (sid only when you passed one), then pushes a book_snapshot per token id, or book_snapshot_failed for a token whose snapshot fetch failed:
retryable is always true — back off and request again. subscriptionActive is true when the request carried a sid (that subscription stays in place and keeps streaming) and false for a request by ids alone.

Unsubscribe

List subscriptions

Useful after a reconnect to confirm what the server thinks you’re subscribed to before rebuilding state.

Ping

ts is the server clock in milliseconds — clients can use it as a clock-skew probe.