Skip to main content

Boros WebSocket

This guide explains how to directly connect to Boros's WebSocket service using Socket.IO client.

Basic Usage​

Here's a complete example of how to connect to and use the WebSocket:

import { io } from 'socket.io-client';

// Initialize the socket connection
const socket = io('wss://api.boros.finance/pendle-dapp-v3', {
path: '/socket/socket.io',
reconnectionAttempts: 5,
transports: ['websocket']
});

Connection Events​

Handling Connection​

socket.on('connect', () => {
console.log('Connected to WebSocket server');
});

socket.on('disconnect', () => {
console.log('Disconnected from WebSocket server');
});

socket.on('connect_error', (error) => {
console.error('Connection error:', error);
});

Subscribing to Channels​

To receive updates, you need to:

  1. Subscribe to a channel
  2. Listen for updates on that channel
// Subscribe to a channel
socket.emit('subscribe', 'statistics:MARKET_ID');

// Listen for updates
socket.on('statistics:MARKET_ID:update', (data) => {
console.log('Received market statistics update:', data);
});

Cleanup​

Always clean up your WebSocket connections when they're no longer needed:

function cleanup() {
// Unsubscribe from channels
socket.emit('unsubscribe', 'statistics:MARKET_ID');

// Remove listeners
socket.off('statistics:MARKET_ID:update');

// Disconnect
socket.disconnect();
}

Channel Types​

Market Channels​

ChannelEventDescription
orderbook:MARKET_ID:TICK_SIZEorderbook:MARKET_ID:TICK_SIZE:updateOrderbook updates. Accepted TICK_SIZE: 0.1, 0.01, 0.001, 0.0001, 0.00001
market-trade:MARKET_IDmarket-trade:MARKET_ID:updateIndividual trade executions with rate, size, blockTimestamp, txHash
statistics:MARKET_IDstatistics:MARKET_ID:updateMarket statistics: markApr, midApr, lastTradedApr, floatingApr, volume24h, notionalOI, nextSettlementTime, longYieldApr
market-data:MARKET_IDmarket-data-updateReal-time market data from on-chain events. See Market Data Events below.

Account Channels​

ChannelEventDescription
account:ACCOUNTaccount:ACCOUNT:updateLegacy notification β€” tells you something changed (type: Position, LimitOrder, or Collateral). Requires REST API refetch.
account-updates:ROOT_ADDRESSposition-updateDetailed position changes with pre/post state. See Account Update Events.
account-updates:ROOT_ADDRESSorder-updateDetailed order state changes (placed, filled, cancelled, purged).
account-updates:ROOT_ADDRESSsettlement-updateDetailed settlement data (yield paid/received, APR rates, fees).

Replace MARKET_ID with your market identifier. For ACCOUNT, use AccountLib.pack(address, accountId). For ROOT_ADDRESS, use your lowercased wallet address (e.g., 0xdac17...).

Prefer account-updates over account

The legacy account:ACCOUNT channel only tells you that something changed β€” you still need to call the REST API to find out what. The new account-updates:ROOT_ADDRESS channel includes full event details in the payload, eliminating the need for follow-up API calls.

Best Practices​

  1. Connection Management

    • Always handle connection errors
    • Implement reconnection logic if needed
    • Clean up connections when no longer needed
  2. Event Handling

    • Subscribe to channels after connection is established
    • Remove listeners before disconnecting
    • Handle potential errors in data processing
  3. Resource Management

    • Unsubscribe from channels you no longer need
    • Don't create multiple connections unnecessarily
    • Clean up resources when your application closes

Example Implementation​

Here's a complete example putting it all together:

import { io } from 'socket.io-client';

class PendleWebSocket {
private socket: any;

constructor() {
this.socket = io('wss://api.boros.finance/pendle-dapp-v3', {
path: '/socket/socket.io',
reconnectionAttempts: 5,
transports: ['websocket']
});

this.setupEventHandlers();
}

private setupEventHandlers() {
this.socket.on('connect', () => {
console.log('Connected to WebSocket server');
});

this.socket.on('disconnect', () => {
console.log('Disconnected from WebSocket server');
});

this.socket.on('connect_error', (error) => {
console.error('Connection error:', error);
});
}

public subscribeToMarket(marketId: string) {
const channel = `statistics:${marketId}`;
this.socket.emit('subscribe', channel);

this.socket.on(`${channel}:update`, (data) => {
console.log(`Received update for ${marketId}:`, data);
});
}

public unsubscribeFromMarket(marketId: string) {
const channel = `statistics:${marketId}`;
this.socket.emit('unsubscribe', channel);
this.socket.off(`${channel}:update`);
}

public disconnect() {
this.socket.disconnect();
}
}

// Usage example:
const ws = new PendleWebSocket();
ws.subscribeToMarket('YOUR_MARKET_ID');

// Clean up when done
// ws.unsubscribeFromMarket('YOUR_MARKET_ID');
// ws.disconnect();

Account Change Tracking​

You can subscribe to your account channel to receive notifications when your account state changes (e.g., order fills, position updates, balance changes).

Subscribing to Account Updates​

The account channel format is account:ACCOUNT, where ACCOUNT is your wallet address (lowercased) concatenated with your account ID.

For example, if your wallet address is 0xdAC17F958D2ee523a2206206994597C13D831ec7 and your account ID is 0 (the default account ID in Boros), the channel would be:

account:0xdac17f958d2ee523a2206206994597c13d831ec700

You can use AccountLib.pack() from @pendle/sdk-boros to construct this:

import { AccountLib } from '@pendle/sdk-boros';

const account = AccountLib.pack('0xdAC17F958D2ee523a2206206994597C13D831ec7', 0);
// account = '0xdac17f958d2ee523a2206206994597c13d831ec700'
// Subscribe to account updates
// Format: account:ACCOUNT
const channel = `account:${AccountLib.pack(walletAddress, accountId)}`;
socket.emit('subscribe', channel);

// Listen for account update events
socket.on(`${channel}:update`, (data) => {
console.log('Account updated:', data);

// Refetch the latest account data via API
refreshAccountData();
});

When you receive an account update event, refetch the relevant API endpoints to get the complete updated state:

import axios from "axios";
import { MarketAccLib, CROSS_MARKET_ID } from "@pendle/sdk-boros";

async function refreshAccountData() {
const marketAcc = MarketAccLib.pack(walletAddress, accountId, tokenId, CROSS_MARKET_ID);

// Refetch account info
const { data: accountInfo } = await axios.post(
`${API_BASE_URL}/open-api/v1/accounts/market-acc-infos`,
{ marketAccs: [marketAcc] }
);

// Refetch active orders
const { data: orders } = await axios.get(
`${API_BASE_URL}/open-api/v1/accounts/limit-orders`,
{
params: {
userAddress: walletAddress,
accountId: accountId,
isActive: true,
},
}
);

// Update your local state
updateLocalState(accountInfo, orders);
}

Market Data Events​

Subscribe to market-data:MARKET_ID to receive real-time market data derived from on-chain events. The event name is market-data-update.

Payload fields:

FieldTypeDescription
mIdnumberMarket ID
bnnumberBlock number of the on-chain event
btnumberBlock timestamp (unix)
oinumberNotional open interest
midnumberMid APR (from implied rate, AMM rate, or bid/ask average)
mknumberMark APR (from contract)
ltnumberLast traded APR
nstnumberNext settlement time (unix)
bbnumber?Best bid APR (undefined if no bids)
banumber?Best ask APR (undefined if no asks)
ainumber?AMM implied APR (undefined if no AMM)
socket.emit('subscribe', `market-data:${marketId}`);
socket.on('market-data-update', (data) => {
console.log(`Market ${data.mId}: mark=${data.mk}, mid=${data.mid}, OI=${data.oi}`);
});

Account Update Events​

Subscribe to account-updates:ROOT_ADDRESS (lowercased wallet address) to receive detailed, structured events when your positions, orders, or settlements change.

Unlike the legacy account:ACCOUNT channel which only notifies you that something changed, these events include the full details β€” no REST API refetch needed.

Position Update (position-update)​

Emitted when a position size or rate changes (trade fill, liquidation).

FieldTypeDescription
einumberEvent index (on-chain sequence number)
bnnumberBlock number
btnumberBlock timestamp (unix)
txHexTransaction hash
mIdnumberMarket ID
maMarketAccPacked market account
prfstringPrevious fixed rate (FixedX18 raw)
prsstringPrevious position size (FixedX18 raw)
ptfstringPost-trade fixed rate (FixedX18 raw)
ptsstringPost-trade position size (FixedX18 raw)

Order Update (order-update)​

Emitted when an order is placed, partially filled, fully filled, cancelled, or purged.

FieldTypeDescription
einumberEvent index
bnnumberBlock number
btnumberBlock timestamp (unix)
txHexTransaction hash
mIdnumberMarket ID
oIdstringOrder ID
sdSide0 = Long, 1 = Short
psstringOriginal placed size (FixedX18 raw)
usstringRemaining unfilled size (FixedX18 raw)
tknumberTick (price level)
otOrderTypeOrder type: 0 = Limit, 1 = Market, 2 = TakeProfitMarket, 3 = StopLossMarket
osLimitOrderStatusOrder status: 0 = Filling (unfilled/partially filled), 1 = Cancelled, 2 = FullyFilled, 3 = Expired (market matured), 4 = Purged
efsstring?Size filled by this specific event (undefined if no fill)

Settlement Update (settlement-update)​

Emitted when a funding rate settlement is processed for a position. Only emitted for positions with non-zero size.

Limited Availability

settlement-update events are currently only emitted for a few active market makers, not for all users. General user support will be added in the future. For now, use GET /accounts/settlements or the legacy account:ACCOUNT channel to track settlements.

FieldTypeDescription
einumberEvent index
bnnumberBlock number
btnumberBlock timestamp (unix)
txHexTransaction hash
maMarketAccPacked market account
mIdnumberMarket ID
psstringPosition size (negative = short)
ypstringYield paid
yrstringYield received
panumberAnnualized fixed rate paid
ranumberAnnualized floating rate received
feestringSettlement fee

Example: Subscribing to Account Updates​

const root = walletAddress.toLowerCase();
socket.emit('subscribe', `account-updates:${root}`);

socket.on('position-update', (data) => {
console.log(`Position changed on market ${data.mId}:`);
console.log(` Size: ${data.prs} β†’ ${data.pts}`);
console.log(` Rate: ${data.prf} β†’ ${data.ptf}`);
console.log(` Tx: ${data.tx}`);
});

socket.on('order-update', (data) => {
console.log(`Order ${data.oId} on market ${data.mId}: status=${data.os}`);
if (data.efs) console.log(` Filled: ${data.efs}`);
});

socket.on('settlement-update', (data) => {
console.log(`Settlement on market ${data.mId}: paid=${data.yp}, received=${data.yr}`);
});
All numeric values use FixedX18 raw format

Position sizes, rates, yields, and fees are returned as FixedX18 raw string values (18-decimal fixed-point). Use FixedX18.fromBigIntString(value).toNumber() from @pendle/boros-offchain-math to convert to human-readable numbers.

Complete Account Tracking Example​

import { io } from 'socket.io-client';
import axios from 'axios';
import { AccountLib } from '@pendle/sdk-boros';

class AccountTracker {
private socket: any;
private walletAddress: string;
private accountId: number;

constructor(walletAddress: string, accountId: number = 0) {
this.walletAddress = walletAddress.toLowerCase();
this.accountId = accountId;

this.socket = io('wss://api.boros.finance/pendle-dapp-v3', {
path: '/socket/socket.io',
reconnectionAttempts: 5,
transports: ['websocket'],
});

this.setupConnection();
}

private setupConnection() {
this.socket.on('connect', () => {
console.log('Connected - subscribing to account updates');
this.subscribeToAccount();
});

this.socket.on('disconnect', () => {
console.log('Disconnected from WebSocket');
});
}

private subscribeToAccount() {
const channel = `account:${AccountLib.pack(this.walletAddress as `0x${string}`, this.accountId)}`;
this.socket.emit('subscribe', channel);

console.log('Subscribed to account:', channel);

this.socket.on(`${channel}:update`, async (data: any) => {
console.log('Account change detected:', data);

// Fetch updated data from API
await this.refreshAccountData();
});
}

private async refreshAccountData() {
try {
// Fetch latest account state
const { data } = await axios.post('https://api.boros.finance/open-api/v1/accounts/market-acc-infos', {
marketAccs: [
/* your marketAcc */
],
});

console.log('Updated account data:', data);
// Process updated data...
} catch (error) {
console.error('Failed to refresh account data:', error);
}
}

public disconnect() {
const channel = `account:${AccountLib.pack(this.walletAddress as `0x${string}`, this.accountId)}`;
this.socket.emit('unsubscribe', channel);
this.socket.off(`${channel}:update`);
this.socket.disconnect();
}
}

// Usage
const tracker = new AccountTracker('0xdAC17F958D2ee523a2206206994597C13D831ec7', 0);

// Clean up when done
// tracker.disconnect();

Error Handling​

Always implement proper error handling:

socket.on('connect_error', (error) => {
console.error('Connection failed:', error);
// Implement your error handling logic
});

socket.on('error', (error) => {
console.error('Socket error:', error);
// Implement your error handling logic
});