X-Api-Key — the same key you
use on HTTP, same scope rules.
The protocol is Kalshi-style (envelope with
id / cmd / params)
and Polymarket-style identifier lists (channel + ids[]).
Connect
Same handshake contract for both gateways: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
/ws/user each socket is bound to one wallet, and its private
channels serve only that wallet and its vault. A single_wallet key is
bound to the partner’s associatedWallet. A multi_wallet key has no
wallet of its own, so it must also name one on the upgrade with
X-User-Wallet: 0x… (or ?wallet=0x… / ?user_wallet=0x…). Without
it the socket closes with 4401 api_key_user_wallet_required; a
malformed address closes it with 4401 api_key_user_wallet_invalid.
(If a multi_wallet partner also has an associated wallet configured,
the socket binds to that wallet and ignores X-User-Wallet.) Open one
/ws/user socket per wallet. See
WSS /ws/user.
The server pushes one greeting after the upgrade:
vaultAddress is null when the wallet has no vault yet. An open
socket keeps the value it got at the handshake, so reconnect after the
vault is deployed before you subscribe to vault_positions /
vault_activity.
On /ws/market the greeting is { "gateway": "market", "authMethod": "api_key", "protocolVersion": 2, "partnerId": "…" } — no wallet context, since market data isn’t per-user.
Command envelope
Every client-to-server message uses the same shape:id— client-side correlation id; echoed in the server’s responsecmd—subscribe/update_subscription/get_book_snapshot/unsubscribe/list_subscriptions/ping; anything else gets anunknown_cmderrorparams— command-specific payload
sid (numeric).
Use sid for subsequent updates / unsubscribes / event routing on
the client side. sids are not stable across reconnects.
Channel catalog
Public, on/ws/market:
Private, on
/ws/user:
Detailed payloads + examples in
Subscriptions and
Messages. The two crypto feeds have
their own page: Crypto price feeds.
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, and each outcome’soutcomes[].tokenId- A neg-risk (multi-outcome) event is listed as one market per outcome
(
negRiskEligible: true). Each has its ownsymbol,conditionId,yesTokenIdandnoTokenIdin that same response, so no on-chain lookup is needed
/ws/market and subscribe by those native ids:
market_lifecycle and market_price_tick are keyed by market
symbol (market_price_tick as <symbol>:<outcomeIndex>),
crypto_price_tick by instrument id, and the all_* channels and
global_activity take no ids at all.
What clients should NOT do
- No symbol-based subscriptions on token or condition channels. Use
tokenId/conditionId; onlymarket_lifecycleandmarket_price_ticktake a market symbol. - No invented room strings like
vault:0xabc:erc1155. Usechannel + idsonly. - No mixing of
/ws/userand/ws/marketon one socket. The gateway rejects cross-gateway channels withforbidden— open one socket per gateway. - No assumptions about
sidstability across reconnects. After any close, re-subscribe from scratch.
Next
Subscriptions
Per-channel subscribe / update / unsubscribe payloads.
Message shapes
Outbound event envelope and full event catalog by channel.
Reconnect
Heartbeat, sid rebuild, snapshot resync.
API keys
Scope catalog, rotation, revoke.