Documentation Index
Fetch the complete documentation index at: https://docs.adipredictstreet.com/llms.txt
Use this file to discover all available pages before exploring further.
token_trade_matches,
token_trade_settlements, token_book, token_ohlc,
condition_lifecycle, system) on this gateway returns
{ "code": "forbidden" } in rejected[] — use /ws/market for
public streams.
Authentication
| How to send it |
|---|
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 |
4401 <reason> close frame before any
subscribe command is accepted. There is no fallback to any other auth
scheme; a bad key ends the connection cleanly.
Multi-wallet partners — X-User-Wallet required on upgrade
Partners whose key was provisioned as multi_wallet (one key fanning
out across many sub-account vaults — see
API keys / multi_wallet flow) must declare the
acting wallet on the upgrade request — same shape as their HTTP
calls:
X-User-Wallet, and
vault_positions resolves to that wallet’s vault. Without
X-User-Wallet, the gateway closes 4401
api_key_no_associated_wallet even though the key itself is valid
— multi_wallet keys carry no fixed wallet on the partner row, so the
gateway has nothing to bind to. single_wallet partners do not send
this header; their wallet is fixed at the partner row’s
associated_wallet.
Authority model — symmetric with HTTP
/api/me/*. The gateway
binds whatever X-User-Wallet value the partner sends, the same
way /api/me/* HTTP calls do. The platform does not enforce per-
wallet ownership against the partner row at the request boundary —
partner authority is enforced at the KYB / onboarding contract
level. Partners are responsible for sending only wallets they
legitimately act on behalf of.key= and user_wallet= carry exactly the same semantics as the
headers; mixing the two (e.g. header X-Api-Key + query
user_wallet=) is also accepted.
Every private channel requires the
portfolio:read scope on the
API key. Subscribing without it surfaces
{ "code": "api_key_scope_missing", "message": "channel user_orders needs portfolio:read" }
in rejected[]. See API keys for the full scope
catalog.For vault_positions, each requested vault must be covered by the
API key’s associated_vault row — vaults outside the grant come
back under rejected[] with forbidden.keyId immediately via Redis apikey:invalidate pub/sub.
Close codes
| Close code | Meaning |
|---|---|
4401 api_key_bad_format / api_key_unknown_key / api_key_bad_secret | Malformed token or the server doesn’t recognise the keyId / secret pair. Request a new key from your PredictStreet integration contact. |
4401 api_key_revoked / api_key_expired | Key was explicitly killed or reached its expiry. Issue a fresh one. |
4401 api_key_suspended | The partner itself is SUSPENDED in admin. Contact ops — do not auto-retry. |
4401 api_key_ip_denied | Caller IP isn’t in the key’s allowlist. |
4401 api_key_no_associated_wallet | Key authenticated, but no wallet to bind the socket to. single_wallet: admin must attach associated_wallet to the partner row. multi_wallet: caller forgot the X-User-Wallet header / user_wallet= query param on the upgrade. |
4401 api_key_user_wallet_invalid | X-User-Wallet value isn’t a 0x-prefixed 40-hex address. Trim and lower-case before sending. |
4401 api_key_auth_disabled / api_key_auth_unconfigured | Server-side misconfig (feature flag off or pepper missing). Ops issue, not the caller’s. |
1008 forbidden origin | WebSocket Origin header not in the allowlist. |
Connect greeting
The server pushes a singleconnected frame after the upgrade
handshake so you don’t need a parallel REST call to learn the wallet
tied to the key:
Available channels
| Channel | ids shape | Required scope | Server pushes |
|---|---|---|---|
user_orders | none (scoped to the key’s wallet) | portfolio:read | order_placed, order_cancelled |
user_fills | none (scoped to the key’s wallet) | portfolio:read | user_fill (chain-confirmed) |
vault_positions | vault address[] (1+ required) | portfolio:read | vault_position_balance_changed, vault_position_split, vault_position_merged, vault_position_redeemed |
type.
Subscribe example
Subscribe response
Push examples
order_placed / order_cancelled (channel: user_orders)
clientOrderId is present when the original POST /api/orders/place
supplied one; absent otherwise. The cancel frame echoes the same value
from orders.client_order_id so a partner driving its state machine off
WS gets the correlation key on both sides without a follow-up REST call.
user_fill (channel: user_fills)
Fires the moment the matcher prints a trade where your wallet is on
either side — this is a match-time notification, not a settlement-
confirmed one. The matcher emits before on-chain settlement completes,
so the payload deliberately omits fields that only exist post-
settlement: per-wallet fee, txHash, blockNumber, and the
on-chain orderHash. For those, poll
GET /api/me/trades (or
GET /api/me/fees for the per-side
fee breakdown) once you receive this event — see
user_fill vs /api/me/trades below.
side is 'buy' or 'sell' from your perspective. orderId /
clientOrderId are your side of the match (the buyer’s id when
side === 'buy', the seller’s when side === 'sell'); use them to
correlate the fill back to the placement you submitted. The market is
identified at the asset level (tokenId + conditionId +
outcomeIndex); resolve to your application marketId via
GET /api/markets if needed.
clientOrderId is forwarded only if the matcher’s trade payload
carries it (today the matcher does not always populate this field; the
key is omitted from the JSON when absent — compactRecord strips
nulls). All other fields above are present on every emission.
user_fill vs /api/me/trades
The two surfaces serve different jobs and intentionally publish
different shapes:
| Field | WS user_fill | REST /api/me/trades |
|---|---|---|
tradeId | ✓ | ✓ (as id) |
orderId (your side) | ✓ | ✓ |
clientOrderId | ✓ (when matcher provides) | ✗ |
tokenId / conditionId / outcomeIndex | ✓ | ✗ |
marketId (application id) | ✗ | ✓ |
vaultAddress | ✗ | ✓ |
fee | ✗ | ✓ |
side | 'buy' / 'sell' | 'buy' / 'sell' |
price / quantity | ✓ | ✓ |
| timestamp | tsMs (ms-epoch) | createdAt (ISO 8601) |
fee is intentionally absent from the WS payload because:
- The fill event fires from the matcher, before
exchange.fee_ledgeris written. Per-wallet fee is not yet resolved at emit time. - The fee row is keyed by
(trade_id, role)—roleis'maker'if the wallet placed the resting order,'taker'otherwise. The matcher event does not carry the role/fee join. - Fee semantics are side-specific: per the on-chain
_chargeFeerules, the BUY-taker fee is denominated in outcome shares (not USDC); the SELL-taker fee is in USDC. The singlefeecolumn on the trade row tracks the SELL-side USDC fee only — the per-wallet view that REST returns readsCOALESCE(fee_ledger.amount, t.fee)to surface the correct number per role.
user_fill as the real-time
“fill happened” signal — use it to re-render the activity feed,
update positional UI, trigger re-balance logic. Then call
GET /api/me/trades (with
limit=50 or so, paginated by before) for the post-settlement
view — that includes the resolved fee, the application-level
marketId, and the vaultAddress. Combine the two: WS for latency,
REST for accounting.
If you need the per-side fee breakdown (both maker and taker rows for
the same trade), use GET /api/me/fees
instead — it returns one row per (tradeId, role) from the
fee_ledger.
vault_position_balance_changed (channel: vault_positions)
reason is the chain-event tag (ASCII bytes4). Common values:
"OUTF" (trade-side credit), "INFL" (non-trade inflow),
"SPLT" / "MERG" / "REDM" (split / merge / redeem), "XFER"
(direct transfer). New tags can land — treat unknown as generic.
Rejection codes
code | When |
|---|---|
invalid_params | unknown channel, malformed id, missing required ids, or wallet-scoped channel sent with ids |
api_key_scope_missing | key is missing portfolio:read (required by every private channel) |
forbidden | tried a public channel on this gateway (use /ws/market); also raised when vault_positions ids aren’t covered by the API key’s associated_vault row |
subscription_cap_exceeded | connection already holds the maximum subscriptions (256) |
subscription_too_many_ids | a single subscription / add_ids call exceeded the per-channel id cap (100). Message: "subscription accepts at most 100 ids". Split your ids across multiple subscriptions on the same connection. |
Related
Commands
update_subscription / unsubscribe / list_subscriptions / ping.
Server events
Full event payload catalog per channel.
Reconnect
Heartbeat, sid rebuild, snapshot resync.
API keys
Scope catalog, rotation, revoke.