WebSocket API#
Real-time market, orderbook, and authenticated position subscriptions.
Connection#
| Environment | URL |
|---|---|
| Production | wss://api.seesaw.markets/ws |
The server sends WebSocket ping frames every 30 seconds and closes clients that do not respond with pong frames. Clients do not need to send JSON ping messages.
Upon connecting, the server sends a welcome message:
{
"type": "connected",
"message": "Welcome to Seesaw WebSocket",
"timestamp": 1706745600000
}
const ws = new WebSocket('wss://api.seesaw.markets/ws');
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log(message);
};
Message Format#
Client Messages#
type ClientMessage =
| {
type: 'subscribe';
channel: string;
auth?: {
signature: string; // base58 Ed25519 signature
message: string; // JSON auth message string
publicKey: string; // base58 wallet public key
};
}
| {
type: 'unsubscribe';
channel: string;
};
Server Messages#
type ServerMessage =
| {
type: 'connected';
message: string;
timestamp: number;
}
| {
type: 'subscribed' | 'unsubscribed';
channel: string;
message?: string;
}
| {
type: 'update';
channel: string;
data: Record<string, unknown>;
timestamp: number;
}
| {
type: 'error';
message: string;
};
Channels#
| Channel | Auth | Description |
|---|---|---|
market:{marketId} | No | Market price and state updates. |
orderbook:{marketId} | No | Orderbook depth updates for a market. |
positions:{wallet} | Yes | Position updates for a wallet. |
Public Market Updates#
{
"type": "subscribe",
"channel": "market:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}
Subscription response:
{
"type": "subscribed",
"channel": "market:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}
Update envelope:
{
"type": "update",
"channel": "market:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"data": {
"marketId": "1937600",
"field": "state",
"value": "resolved"
},
"timestamp": 1777392000000
}
The exact data payload depends on the indexer processor that broadcasts the
market event.
Public Orderbook Updates#
{
"type": "subscribe",
"channel": "orderbook:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}
Update envelope:
{
"type": "update",
"channel": "orderbook:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"data": {
"marketId": "1937600",
"bestBid": 5800,
"bestAsk": 6000
},
"timestamp": 1777392000000
}
To get a full orderbook snapshot before subscribing to incremental updates, fetch
the market detail REST endpoint (GET /api/v1/markets/:marketId) and then
subscribe to orderbook updates.
Authenticated Position Updates#
positions:{wallet} requires the channel wallet to match auth.publicKey.
The signed auth message must contain a fresh timestamp, a unique nonce, and
action set to "WS_SUBSCRIBE positions:{wallet}".
{
"type": "subscribe",
"channel": "positions:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"auth": {
"signature": "BASE58_SIGNATURE",
"message": "{\"domain\":\"api.seesaw.markets\",\"timestamp\":1777392000000,\"action\":\"WS_SUBSCRIBE positions:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU\",\"nonce\":\"unique-random-string\"}",
"publicKey": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}
}
Auth requirements:
domainmust be the server's hostname (e.g.api.seesaw.markets).timestampmust be within five minutes of server time and no more than 30 seconds in the future.actionmust be"WS_SUBSCRIBE <channel>"where<channel>matches the subscribed channel exactly.noncemust be unique across HTTP and WebSocket auth.signaturemust verify against the exactmessagestring bytes and thepublicKey.
Update envelope:
{
"type": "update",
"channel": "positions:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"data": {
"marketId": "1937600",
"owner": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"yesShares": "105000000000",
"noShares": "50000000000"
},
"timestamp": 1777392000000
}
Unsubscribe#
{
"type": "unsubscribe",
"channel": "market:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}
Response:
{
"type": "unsubscribed",
"channel": "market:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}
Limits and Errors#
- Maximum message size: 4096 bytes.
- Maximum subscriptions per client: 20.
- Maximum total clients: 1000 (configurable via
WS_MAX_CLIENTS). - Maximum connections per IP: 10 (configurable via
WS_MAX_CONNECTIONS_PER_IP). - Backpressure: messages are skipped when the client's buffered amount exceeds 1 MB; after 50 consecutive skips the connection is closed.
Example error:
{
"type": "error",
"message": "Invalid channel: unsupported. Must start with: market:, orderbook:, positions:"
}
Common error conditions:
| Condition | Message |
|---|---|
| Invalid JSON | Invalid JSON |
| Unknown message type | Unknown message type: {type} |
| Invalid channel prefix | Invalid channel: {channel}. Must start with: market:, orderbook:, positions: |
| Too many subscriptions | Maximum subscriptions (20) reached |
| Missing position auth | Authentication required for position channels |
| Auth/channel mismatch | Channel wallet does not match authenticated wallet |
| Invalid signature | Invalid wallet signature |
| Expired auth | Authentication expired |
| Reused nonce | Nonce already used |
Reconnection Example#
class SeesawWebSocket {
constructor(url) {
this.url = url;
this.subscriptions = new Map();
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
for (const subscription of this.subscriptions.values()) {
this.ws.send(JSON.stringify(subscription.message));
}
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type !== 'update') return;
const subscription = this.subscriptions.get(message.channel);
subscription?.callback(message.data);
};
this.ws.onclose = () => {
setTimeout(() => this.connect(), 1000);
};
}
subscribe(channel, callback, auth) {
const message = auth ? { type: 'subscribe', channel, auth } : { type: 'subscribe', channel };
this.subscriptions.set(channel, { message, callback });
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}
unsubscribe(channel) {
this.subscriptions.delete(channel);
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'unsubscribe', channel }));
}
}
}
const ws = new SeesawWebSocket('wss://api.seesaw.markets/ws');
ws.subscribe('market:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', (data) => {
console.log('Market update:', data);
});
See Also#
- REST Endpoints — snapshot queries
- SDK Guide — transaction building
- Trustless SDK — account reads without indexer trust