Skip to main content

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

FieldTypeDescription
tokenstringToken symbol, e.g. DF or USDT. See Enums — Tokens.
availablestring (decimal)Free balance — matches available in GET /spot/balances.
frozenstring (decimal)Reserved balance — held by open orders or pending withdrawals.
tsintegerWhen this balance was computed — unix seconds.

Update Cadence

A spot_user_balance push is sent whenever a (user_address, token) balance row changes:

TriggerAffected 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 cancellationWhichever token was frozen is released
Admin credit / debitThe 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`);
}
}