WebSocket
Real-time feeds for order books, public trades, candles, mark price, session volume, IPO allocation progress, competition leaderboard, and your private portfolio deltas.
URL: wss://api.loafmarkets.com/ws (derive from your REST host by replacing /api with /ws and https with wss).
For a batteries-included client, see the Python SDK template .
Connection & auth
- Connect to
wss://<host>/ws. - For private channels, send an
authframe with your API key. - Send
subscribewith one or more channel strings. - Handle incoming frames by
type.
Auth frame:
{
"type": "auth",
"token": "<LOAF_API_KEY>",
"timestamp": 1718000000
}Subscribe frame:
{
"type": "subscribe",
"channels": ["orderbook:42", "portfolio:3"],
"timestamp": 1718000000
}You may also unsubscribe with the same shape. Server replies include auth_result, subscription_confirmed, and error frames when something fails.
Public channels work without auth. The private portfolio:{userId} channel requires a valid API key and a userId that matches the authenticated account (find your numeric id in the Loaf web app).
Channels
| Channel | Auth | Message type | Payload |
|---|---|---|---|
orderbook:{propertyId} | public | orderbook_update | Full bid/ask snapshot (~500ms) |
trades:{propertyId} | public | trades_batch | Rolling recent-trades batch |
chart:{propertyId} | public | candle_update | OHLCV candle updates (channel name is singular chart) |
markprice:{propertyId} | public | mark_price | Canonical mark price (about 1s, on change) |
volume:{propertyId} | public | volume_update | Session-to-date settled volume (volume24h), on settlement |
ipo:{ipoId} | public | ipo_allocation_update | Primary-market allocation progress |
leaderboard | public | leaderboard_update | Full competition leaderboard, on change |
portfolio:{userId} | private | see below | Your account deltas |
Portfolio message types
The private channel is a delta stream. You receive separate frames rather than a full portfolio snapshot on every tick:
type | Meaning |
|---|---|
balances_update | Cash / frozen balances changed |
position_update | Position quantity or entry changed |
order_status | Order reached a terminal or intermediate status |
order_update | Order fields changed (e.g. remaining quantity) |
trade_new | Your fill |
lifetime_volume_update | Your lifetime traded volume changed |
transfer_update | Cash or token transfer into/out of your account |
offering_order_update | IPO subscription status (PENDING / ALLOCATED / REJECTED) |
To value positions live, combine position_update with the markprice channel — the server does not push recomputed portfolio totals on every price tick. Seed volume24h from REST property detail / trade list, then replace it from volume_update frames.
Example: order book
{
"type": "orderbook_update",
"propertyId": 42,
"bids": [{ "price": 124.0, "quantity": 5.0, "orderId": 8801 }],
"asks": [{ "price": 126.0, "quantity": 3.0, "orderId": 8802 }]
}Example: public trades batch
{
"type": "trades_batch",
"propertyId": 42,
"trades": [
{
"tradeId": 9001,
"propertyId": 42,
"aggressorSide": "BUY",
"price": 125.5,
"quantity": 2.0,
"timestamp": 1718000000
}
]
}Example: mark price
{
"type": "mark_price",
"propertyId": 42,
"price": 125.25
}Example: session volume
{
"type": "volume_update",
"propertyId": 42,
"volume24h": 1204.5
}Example: your fill (private)
{
"type": "trade_new",
"trade": {
"tradeId": 9002,
"propertyId": 42,
"side": "BUY",
"quantity": 1.0,
"price": 125.5
}
}Client example
const ws = new WebSocket('wss://api.loafmarkets.com/ws');
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'auth',
token: process.env.LOAF_API_KEY,
timestamp: Math.floor(Date.now() / 1000),
}));
ws.send(JSON.stringify({
type: 'subscribe',
channels: ['orderbook:42', `portfolio:${process.env.LOAF_USER_ID}`],
timestamp: Math.floor(Date.now() / 1000),
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(String(event.data));
if (msg.type === 'orderbook_update') {
console.log('book', msg.bids?.[0], msg.asks?.[0]);
}
if (msg.type === 'trade_new') {
console.log('filled', msg.trade);
}
};A REST 200 on create-order means the exchange accepted the order — not that it filled. Fills and cancellations arrive asynchronously on portfolio:{userId} (trade_new, order_status, order_update).