Skip to Content
API ReferenceWebSocket

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 ready-made client, see the Python SDK template .

Ask once vs listen

REST is ask once (a photo of current state or history). WebSocket is listen (what just changed — or, for some channels, a continuous full picture).

  • Full picture channels (orderbook, trades, leaderboard) keep sending the complete state. Subscribe and stop polling REST.
  • Delta channels (markprice, volume, chart, portfolio, and ipo) only send changes. Call the matching REST endpoint once to load current state, then listen — otherwise you sit empty until something moves.

See API Reference for the shared pattern, and the REST pages for each seed endpoint.

Connection & auth

  1. Connect to wss://<host>/ws.
  2. For private channels, send an auth frame with your API key.
  3. Send subscribe with one or more channel strings.
  4. Handle incoming frames by type.

Auth frame:

{ "type": "auth", "token": "<LOAF_API_KEY>", "timestamp": 1718000000 }

Subscribe frame:

{ "type": "subscribe", "channels": ["orderbook:opera", "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 (see Get your userId). Market-data channels use lowercase tokenName (e.g. orderbook:opera), not numeric propertyId and not the ticker. Payloads may still include numeric propertyId.

Get your userId

The private portfolio:{userId} channel needs your numeric Loaf user id. That id must belong to the same account as the API key. Call GET /api/auth/profile once and copy userId:

import axios from 'axios'; const response = await axios.get('https://api.loafmarkets.com/api/auth/profile', { headers: { Authorization: `Bearer ${process.env.LOAF_API_KEY}` }, }); const userId = response.data.userId;

Put that number in LOAF_USER_ID and subscribe to portfolio:{userId}.

Channels

ChannelAuthStreamMessage typePayload
orderbook:{tokenName}publicFull pictureorderbook_updateFull bid/ask snapshot (~500ms)
trades:{tokenName}publicFull picturetrades_batchRolling recent-trades batch
chart:{tokenName}publicDeltascandle_updateOHLCV candle updates (channel name is singular chart)
markprice:{tokenName}publicDeltasmark_priceCanonical mark price (about 1s, on change)
volume:{tokenName}publicDeltasvolume_updateSession-to-date settled volume (volume24h), on settlement
ipo:{ipoId}publicDeltasipo_allocation_updatePrimary-market allocation progress
leaderboardpublicFull pictureleaderboard_updateFull competition leaderboard, on change
portfolio:{userId}privateDeltassee belowYour account deltas

Portfolio message types

The private channel is a delta stream. Seed open orders, fills, balances, and positions from REST (History, Portfolio), then apply these frames — you do not get a full portfolio snapshot on every tick:

typeMeaning
balances_updateCash / frozen balances changed
position_updatePosition quantity or entry changed
order_statusOrder reached a terminal or intermediate status
order_updateOrder fields changed (e.g. remaining quantity)
trade_newYour fill
lifetime_volume_updateYour lifetime traded volume changed
transfer_updateCash or token transfer into/out of your account
offering_order_updateIPO 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 mark price and volume24h from Trade (GET /api/trade), then replace them from mark_price / volume_update frames. Seed chart history from candle REST, then apply candle_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:opera', `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. It does not mean the order filled. Fills and cancellations arrive asynchronously on portfolio:{userId} (trade_new, order_status, order_update).

Last updated on