User Balances
Channel Name
spot:user:balances
Authentication: required. See Authentication.
Description
Streams spot balance changes for the authenticated user. The server filters pushes by wallet address and sends one message per (token) row that changes. A single fill that moves both DF and USDT for one user emits two separate pushes — one for each token.
Subscribe
Authenticate first, then subscribe:
{ "type": "auth_token", "token": "eyJhbGciOiJIUzI1..." }
{ "type": "subscribe", "channel": "spot:user:balances" }
Response:
{ "type": "subscribed", "channel": "spot:user:balances" }
No snapshot is sent. Call GET /spot/balances after subscribing to seed your local balance state, then apply incoming spot_user_balance pushes to keep it current.
Push Format
spot_user_balance
One message per (token) row that changed. Each push is a complete replacement of that token's balance — no incremental delta.
{
"type": "spot_user_balance",
"channel": "spot:user:balances",
"data": {
"token": "DF",
"available": "10000",
"frozen": "100",
"ts": 1778400000
}
}
Example — both tokens updated after a DFUSDT fill (two sequential pushes):
Push 1 (DF received by buyer):
{
"type": "spot_user_balance",
"channel": "spot:user:balances",
"data": {
"token": "DF",
"available": "10030",
"frozen": "0",
"ts": 1778400010
}
}
Push 2 (USDT deducted from buyer):
{
"type": "spot_user_balance",
"channel": "spot:user:balances",
"data": {
"token": "USDT",
"available": "4985",
"frozen": "0",
"ts": 1778400010
}
}
Fields
| Field | Type | Description |
|---|---|---|
token | string | Token symbol, e.g. DF or USDT. See Enums — Tokens. |
available | string (decimal) | Free balance — matches available in GET /spot/balances. |
frozen | string (decimal) | Reserved balance — held by open orders or pending withdrawals. |
ts | integer | When this balance was computed — unix seconds. |
Update Cadence
A spot_user_balance push is sent whenever a (user_address, token) balance row changes:
| Trigger | Affected tokens |
|---|---|
| Fill (as taker or maker) | Both base (DF) and quote (USDT) for the DFUSDT market |
| Order placement (limit) | Quote (USDT) frozen for a buy; base (DF) frozen for a sell |
| Order cancellation | Whichever token was frozen is released |
| Admin credit / debit | The credited/debited token |
| Internal transfer (perp margin ↔ spot wallet) | The transferred token |
Each affected token yields a separate push. The order of the two pushes for a fill is not guaranteed.
Code Example
const ws = new WebSocket('wss://api-sepolia.p99.world/ws');
// Local balance map: token -> { available, frozen }
const balances = new Map();
ws.onopen = () => {
// 1. Authenticate
ws.send(JSON.stringify({ type: 'auth_token', token: '<JWT>' }));
setInterval(() => ws.send(JSON.stringify({ type: 'ping' })), 30000);
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'auth_result' && msg.success) {
// 2. Seed local state from REST
fetch('https://api-sepolia.p99.world/api/v1/spot/balances', {
headers: { Authorization: 'Bearer <JWT>' },
})
.then((r) => r.json())
.then((list) => list.forEach((b) => balances.set(b.token, b)));
// 3. Subscribe
ws.send(JSON.stringify({ type: 'subscribe', channel: 'spot:user:balances' }));
}
if (msg.type === 'spot_user_balance') {
const { token, available, frozen, ts } = msg.data;
// Replace the entire token row
balances.set(token, { available, frozen });
console.log(`[${token}] available=${available} frozen=${frozen}`);
// Re-render wallet panel
renderWallet(balances);
}
};
function renderWallet(balances) {
for (const [token, { available, frozen }] of balances) {
console.log(` ${token}: ${available} free, ${frozen} frozen`);
}
}