Use
sid to route inside your client; use id to identify which
specific subscribed entity matched.
user_orders (/ws/user, id = your wallet) ✅ live
Fields on the
order_* frames:
marketId— the market symbol, the same key asmarketIdonuser_filland onGET /api/orders/{id}. Onorder_placed,order_cancelledandorder_expired;order_rejecteddoesn’t carry it.originalQuantity(order_placed,order_cancelled,order_expired) — the order size as placed, in shares (decimal string, printed canonically:"10.5", not"10.50"), the same value on every frame of one order. A resting order cancelled by self-trade prevention can arrive without it. On a cancel or expiry,originalQuantity - remainingQtyis what had filled when the order ended, not a final fill total: the order’s fills are recounted after it ends, so read the total fromGET /api/orders/{id}.remainingQty(order_cancelled,order_expired) — the unfilled size at the moment the order was cancelled or expired, in shares (decimal string, same canonical printing). A resting order cancelled by self-trade prevention (reason: "self_trade_prevention") can arrive withoutremainingQty.orderType(order_placed) —LIMITorMARKET.timeInForce(order_placed) —GTC,IOCorFOK.placedAt(order_placed) — the time the exchange accepted the order, ISO 8601;tsMson that frame is the same instant in ms. All orders of oneplace-batchshare it.eventAt(order_cancelled,order_expired,order_rejected) — the time the exchange recorded the cancel, expiry or rejection, ISO 8601. On these framestsMsis the time the frame was sent, so order them byeventAt.
clientOrderId is. Key your order state by orderId / clientOrderId
from the placement response.
These frames are state-change notifications: each reports one
transition of one order with the fields known at that point. They are
not a snapshot of the order — there is no filledQty (a cancel or
expiry gives only the size filled by then, see originalQuantity
above), and status is the event label (placed / cancelled /
rejected / expired), not the REST order status. Read the current
state with
GET /api/orders/{id}; the field
mapping between the two is in
user_orders frames vs REST order detail.
trade_failed names the market in marketId (the market symbol) when
it can be resolved.
user_fills (/ws/user, id = your wallet) ✅ live
vault_positions (/ws/user, ids = [your vault address]) ✅ live
Amounts and balances are base-unit integers as decimal strings.
balanceAfter on split / merge / redeem is the vault’s collateral
balance after the operation. These frames carry no tsMs; order them
by blockNumber / logIndex.
vault_position_balance_changed.reason is the vault contract’s
bytes4 tag, sent as hex (e.g. "0x53504c54" = SPLT). Values:
SPLT 0x53504c54 (split), MERG 0x4d455247 (merge), INFL
0x494e464c (exchange delivered tokens into the vault — a fill),
OUTF 0x4f555446 (exchange took tokens out of the vault — a fill),
REDM 0x5245444d (redeemed, balance goes to 0), CONV 0x434f4e56
(neg-risk conversion), EMER 0x454d4552 (emergency withdrawal). Treat
unknown tags as a generic balance change; don’t drop them.
Required scope: portfolio:read. Subscription must include at least
one vault id, and every id must be the vault bound to this socket:
your key’s vault, or the vault of the X-User-Wallet wallet on a
multi-wallet key. Any other vault comes back under rejected[] with
forbidden, and so does every vault id when the socket was opened
before that wallet had a vault — reconnect once vault_created
arrives, and if the new greeting still shows vaultAddress: null,
reconnect again a little later.
vault_activity (/ws/user, ids = [your vault address]) ✅ live
trade_fill. The exchange emits one OrderFilled per filled order:
one for each maker order in a match and one for the taker order.
orderHash is the order the event is about, makerVault the vault that
signed it, and takerVault its counterparty — the taker’s vault on a
maker order’s event, the exchange contract on the taker order’s own
event. side is your vault’s place in that event, not your trade role:
maker means the event is for your own order (orderHash and fee
are yours), taker means it is the event of a maker order your order
took. A maker therefore gets one frame per fill; a taker gets one for
its own order plus one per maker order it matched. Read your fee from
the side: "maker" frames. makerAssetId / takerAssetId are "0"
for USDC and the outcome tokenId otherwise, seen from the order in
orderHash (what it gave / received). makerWallet / takerWallet
are the vaults’ owner wallets (EIP-55), null when not resolved.
Amounts on the on-chain frames are base-unit integers as decimal
strings; exchange_balance_synced amounts are human-decimal USDC,
except balanceAfterRaw (the vault’s on-chain balance in base units).
These frames carry no tsMs; order them by blockNumber /
logIndex. Required scope: portfolio:read. The subscription must
name your own vault — any other vault comes back under rejected[]
with forbidden.
user_activity (/ws/user, no ids, id = your wallet) ✅ live
There is no
withdrawal_* frame for the final confirmation: it arrives
as withdrawal_confirmed on vault_activity, and
GET /api/me/withdrawals/{id} reports CONFIRMED. The channel also
repeats order and trade events (order_placed, order_cancelled,
order_rejected, order_expired, trade_matched, trade_failed) in
an unnormalized form not described here; use user_orders and
user_fills for those. Required scope: portfolio:read.
notifications (/ws/user, no ids, id = notification id) ✅ live
The live side of the PredictStreet web app’s notification centre.
data.type is the kind of notification — ORDER_FILLED,
POSITION_SETTLED, DEPOSIT_CONFIRMED, WITHDRAWAL_CONFIRMED,
MARKET_RESOLVED, MARKET_STATUS_CHANGED, MARKET_CLOSING_SOON,
MARKET_VOIDED, NEW_MARKETS, PROMOTION — not to be confused with
the envelope type, which is always notification. New kinds can be
added; treat an unknown one generically. title and body are display
text. category is the notification-preference item the kind belongs
to (e.g. order-filled, deposit-completed) and is sent as null for
kinds that are not a preference item, such as MARKET_VOIDED.
payload is a kind-specific object; its occurredAt is when the
underlying event happened, while createdAt (ISO 8601) and tsMs are
when the notification was added. marketId and marketSymbol are
omitted when unknown, and either one can be missing on a notification
about a market.
Which notifications are created, and whether they are pushed here,
follows the notification preferences set in the web app — a kind whose
in-app delivery is switched off there sends no frame. The notification
feed itself is a web-app surface, not part of this API. Frames are best-effort: a frame missed
while you were disconnected is not sent again. Don’t drive trading or
accounting from this channel — use user_orders, user_fills,
vault_activity and user_activity. Required scope: portfolio:read.
dispute_intent (/ws/user, id = dispute intent id) ✅ live
Every frame carries
{ id, walletAddress, marketSymbol, txHash?, status, challengeId?, rejectionReason?, expiresAt, createdAt, updatedAt, tsMs }.
Fields without a value are omitted; txHash is absent on an intent
that never got a transaction. challengeId is the challenge’s id in
GET /api/challenges.
expiresAt, createdAt and updatedAt are ISO 8601; tsMs is
updatedAt in milliseconds.
A dispute intent is the record the PredictStreet web app keeps when a
user files a dispute from the app. There is no API to create one, and a
challenge you send on-chain yourself (see
Challenges) has no intent — so on an
API-key socket this channel stays silent unless the socket’s wallet
also files disputes through the web app. Follow your own challenges
with GET /api/challenges?challengerWallet=<address> and the
market_disputed frame on market_lifecycle.
On an API-key socket, subscribe with your wallet as the only id —
{ "channel": "dispute_intent", "ids": ["<the greeting's walletAddress>"] };
a JWT session subscribes without ids. Required scope: portfolio:read.
bond_payout_credited (/ws/user, id = challenge id) ✅ live
Fires when the decision on a challenge you raised credits your bond
back to
pendingBondPayouts(yourAddress) on the oracle you challenged, once the
platform has indexed that decision: the proposal was overridden (bond
plus reward), the market was voided (bond), or the proposal was
cleared through the
emergency proposal clear
(bond). Nothing is sent when the original outcome is confirmed — the
bond is forfeited. See Challenges.
Amounts are USDC base units (6 decimals) as decimal strings:
amountUsdcRaw = bondUsdcRaw + rewardUsdcRaw, and rewardUsdcRaw
is omitted when there is no reward. It is the amount this decision
credited, not your total pending balance. sourceChallengeId (also the
envelope id) is the challenge’s id in GET /api/challenges;
creditedAt is ISO 8601 and tsMs the same instant. The credit is not
paid out automatically — claim it with withdrawBondPayout(to). The
frame is best-effort: read pendingBondPayouts(yourAddress) on the
oracle for the authoritative amount.
On an API-key socket, subscribe with your wallet as the only id
("ids": ["<the greeting's walletAddress>"]); a JWT session subscribes
without ids. Required scope: portfolio:read.
me_limits_invalidated (/ws/user, id = transaction hash) ✅ live
Fires when an on-chain
DepositLimitRegistry transaction that changes
your wallet’s tier, caps, or deposit / trading / withdrawal restriction
is indexed. reason names the kind of change; the frame carries none
of the new values, so re-read
GET /api/me/deposit-limits
when it arrives. That makes it the push alternative to polling while a
new wallet waits for its first tier
(Initialisation timing).
The REST view is refreshed separately from this push, so if the first
re-read still shows the old values, read again a few seconds later.
There is no tsMs; use the envelope serverTs.
On an API-key socket, subscribe with your wallet as the only id
("ids": ["<the greeting's walletAddress>"]); a JWT session subscribes
without ids. Required scope: portfolio:read.
token_trade_matches (/ws/market, id = tokenId) ✅ live
Fires the moment the matcher prints a trade — fastest tape feed,
arrives several seconds before the chain settlement.
tradeId is the
platform trade id, a numeric string (the same id that
GET /api/me/trades and /api/markets/{symbol}/trades return).
trade_settled does not carry
tradeId; tie a settlement back to your own orders through
orderHash (also sent on your order_placed).
On a binary market every trade is published on both outcome
tokens: the frame for outcome 0 and a mirrored frame for outcome 1
at price 1 − p, with the same tradeId. takerOutcomeIndex is the
outcome the taker actually traded and is identical on both frames. If
you subscribe to both tokens of a market, keep only the frame whose
outcomeIndex equals takerOutcomeIndex (or dedupe on tradeId)
before counting trades or volume.
GET /api/markets/{symbol}/trades for history.
token_trade_settlements (/ws/market, id = tokenId) ✅ live
Fires when the platform indexes an on-chain
OrderFilled event. The
exchange emits one per filled order — one for each maker order in a
match and one for the taker order — so a single match produces several
trade_settled frames, each describing the order in orderHash
(side, price and quantity are that order’s). Use it for
accounting-grade confirmations of your own orders, keyed by
orderHash; don’t add the frames up as independent trades.
token_ohlc (/ws/market, id = tokenId) ✅ live
5-second candles built from the
trade_matched tape, pushed whenever a
trade prints inside the active window. isClosed: false while the bar is still
accumulating; isClosed: true is the final tick of the bar.
For history: GET /api/markets/{symbol}/ohlc?interval=s5.
market_price_tick (/ws/market, id = <symbol>:<outcomeIndex>) ✅ live
The live edge of the probability chart: each frame is the point
GET /api/markets/{symbol}/price-history returns for that second, and
price is the value the market serves as chances — the book
midpoint when the book is two-sided with a spread of at most 0.10, else
the last trade. When neither exists the last known mark is carried
forward, and before any activity it is 1 / outcomeCount.
Subscribe with ids of the form <symbol>:<outcomeIndex> — the market
symbol (case-insensitive, lower-cased in the ack and in the envelope
id) and the 0-based outcome index without leading zeros (0–255),
one id per outcome you chart. data.symbol keeps the symbol’s original
case.
Frames are event-driven: at most about one per second per outcome, and
only when that outcome’s best bid, best ask or last trade changed, so a
frame can repeat the previous price and a quiet market sends nothing.
Only markets that are OPEN and not past their close time are ticked.
A frame that is more than about 10 seconds old by the time it reaches
the gateway is dropped rather than delivered late. No backfill on
subscribe — seed the chart from
GET /api/markets/{symbol}/price-history?range=live (one-second
points) and append frames by ts.
token_book (/ws/market, id = tokenId) ✅ live
book_update is the raw full-depth replacement; book_delta is the
computed diff. Most integrators want book_delta; book_update is
there for clients that prefer to replay state wholesale. Live
book_update is dropped if an upstream frame is missing seq /
prevSeq — the gateway never emits a divergent delta.
What a book frame holds. book_update is not just the best bid
and ask: it is the complete book after the change,
every resting price level on both sides, aggregated per price. Levels
whose total size is below a small dust threshold are left out of every
book frame, the same filter the REST orderbook applies. book_snapshot
is capped at 100 levels per side, but prices sit on a 0.01 grid, so a
side never has more than 99 levels and a snapshot also carries the
whole book. book_delta carries the same change as a diff. Apply
book_update frames, or book_snapshot + book_delta frames, not
both.
On book_snapshot_failed, retryable is always true: back off, then
send get_book_snapshot again or read the REST orderbook.
subscriptionActive only tells you whether the snapshot was requested
under a sid: true on subscribe, on update_subscription and on
get_book_snapshot with a sid (that subscription stays in place and
keeps streaming), false on a get_book_snapshot sent with ids
alone.
seq is monotonically increasing. After subscribe the server pushes
exactly one book_snapshot (depth 100), then one book_update per
book change, each followed by the book_delta for the same change
with the same seq (the delta is skipped when the gateway lacks the
previous state). Apply one stream, not both — book_update frames, or
book_snapshot + book_delta frames — and track the last seq you
applied; a frame whose data.prevSeq differs from it means you missed
a change. On such a gap or unexpected silence, issue
get_book_snapshot
to refresh without unsubscribing. See
Reconnect — orderbook resync.
all_trade_matches / all_trade_settlements / all_books (/ws/market, no ids, id = tokenId) ✅ live
Platform-wide firehoses: every frame the per-token channel sends, for
every token, without listing ids. Subscribe with no
ids at all —
sending any is rejected with invalid_params. The per-token channels
take explicit token ids only (there is no * wildcard); these
channels are the way to receive everything. Each frame is identical to
the per-token frame — same type, data, envelope id (the frame’s
tokenId) and, on all_books, the same seq and hash — only sid
and channel differ.
- Duplicates across subscriptions. A connection holding both a
per-token subscription and the matching
all_*channel receives each frame twice, once persid. Use one or the other on a connection, or dedupe on the envelopeidtogether withtradeId(trades),orderHash+txHash(settlements) orseq(books). - Two frames per binary trade.
all_trade_matchescarries the outcome-0frame and its NO mirror for every binary-market trade, as described undertoken_trade_matches: keep the frame whoseoutcomeIndexequalstakerOutcomeIndex(or dedupe ontradeId) before counting trades or volume.trade_matchednames its market inmarketSymbol; fortrade_settledand the book frames, resolve an unknown token withGET /api/markets/by-token/{id}. For one frame per trade with market titles attached, useglobal_activitybelow. - No initial book.
all_bookssends nobook_snapshoton subscribe. Eachbook_updateis a token’s full book, so start a token from its firstbook_update, or request snapshots withget_book_snapshotbyids(aget_book_snapshotnaming this channel’ssidis rejected — it needs atoken_booksubscription). Apply abook_deltaonly on top of the state at itsprevSeq. - Volume.
all_booksdelivers every change of every book on the platform as a full-depthbook_updateplus abook_delta. A client that doesn’t keep up fills its outbound buffer and is disconnected (see resource limits); subscribe totoken_bookfor the tokens you need unless you really need all of them.
global_activity (/ws/market, no ids, id = tradeId) ✅ live
The platform-wide trade feed: exactly one frame per matched trade (not
two per binary trade, unlike
all_trade_matches), sent at match time,
from the taker’s side. outcomeIndex, side and price are the
taker’s own leg — on a binary market where the taker traded NO,
outcomeIndex is 1 and price is 1 − p of the YES price.
actorWallet is the taker’s wallet and actorUsername their public
username. outcomeLabel is the outcome’s canonical label (YES /
NO, or the outcome name) and outcomeDisplayLabel the label shown to
traders. tokenId is the taker’s outcome token. price and quantity
are decimal strings; tsMs is the trade time.
The market, event and username fields are display metadata looked up
for each trade. A field without a value is omitted, and when the lookup
is unavailable the frame is still sent with the trade fields only —
render a fallback (the market symbol, a shortened wallet) rather than
waiting for them. A username is not shown for a wallet without one or
whose public profile is not available. The lookup can reorder frames by
a few milliseconds; order by tsMs.
No ids: subscribe with { "channel": "global_activity" }. No
backfill on subscribe — load the latest rows from
GET /api/activity/recent (same field names; the REST rows send null
where the frame omits a key), then apply frames on top, deduplicating on
tradeId.
condition_lifecycle (/ws/market, id = conditionId) ✅ live
Only the on-chain per-condition trading pause / unpause is published
here, so the channel is silent most of the time. Nothing is pushed on
subscribe. Status changes, disputes and resolution are on
market_lifecycle below.
market_lifecycle (/ws/market, id = market symbol) ✅ live
Keyed by the app
symbol (lower-cased), so one subscription per market
page carries status, dispute, resolution and — on a crypto window — the
frozen open and the bound close. condition_lifecycle is not a
per-condition copy of this channel: it carries only the on-chain
trading pause.
null — treat every
optional key above as “may be absent”. Frames are a low-latency hint;
re-fetch GET /api/markets/{slug} for the authoritative state.
crypto_price_tick (/ws/market, id = instrumentId) ✅ live
The price of the coin (
BTC-USD), not of a market. Keyed by
instrument, so every crypto market on that asset shares one feed.
price is an unscaled integer as a decimal string — render as
price / 10 ** decimals, never Number(price) (18 decimals overflows
Number.MAX_SAFE_INTEGER).
src distinguishes the two producers: "datastreams" (live spot,
~1/s, drives short Up/Down windows) and "candlestick" (one-minute
candle close, ~1/min, drives long-dated markets). Plot points at their
own at, de-duplicate on it, and expect silence when no producer is
active for the instrument. No backfill on subscribe — seed the curve
from
GET /api/markets/{symbol}/crypto-price-history.
Full guide: Crypto price feeds.
system (/ws/market, id = "platform_status") — no frames yet
The gateway accepts
system subscriptions, but no platform_status
frames are published today, so the channel stays silent. To detect a
platform freeze, poll GET /api/platform/status, which returns
{ frozen, reason, since, activeNotices }.