Skip to main content
When you need state updates faster than 15-second polling allows, open a WebSocket. The server pushes every state transition and a heartbeat every 5 seconds. Endpoint: wss://api.pegana.xyz/v1/ws

Open a connection

wscat -c wss://api.pegana.xyz/v1/ws
import WebSocket from "ws";

const ws = new WebSocket("wss://api.pegana.xyz/v1/ws");

ws.on("open", () => console.log("connected"));
ws.on("message", (data) => {
  const msg = JSON.parse(data.toString());
  console.log(msg);
});
No auth, no key. Origin header is checked against an allowlist — https://pegana.xyz, https://www.pegana.xyz, http://localhost:3000 by default. Add more origins via the CORS_EXTRA_ORIGINS env var on self-hosted instances.

Message shapes

All messages are JSON, single-line per frame. The op field discriminates.

Server → client: state update

{
  "op": "update",
  "asset": "jitoSOL",
  "payload": {
    "state": "DRIFT",
    "previous_state": "PEGGED",
    "discount": "-0.0034",
    "intrinsic_usd": "213.42",
    "market_usd": "212.69",
    "confidence": "0.98",
    "ts": "2026-05-26T14:32:11Z"
  }
}
Pushed on every engine recompute that includes a state change for the asset.

Server → client: heartbeat

{
  "op": "heartbeat",
  "ts": "2026-05-26T14:32:16Z"
}
Every 5 seconds. Use this to detect connection death — if you go 15s without any frame (update or heartbeat), drop and reconnect.

Server → client: error

{
  "op": "error",
  "msg": "malformed subscribe message"
}
Returned on bad input. Connection stays open.

Client → server: ping

{ "op": "ping" }
Server responds with { "op": "pong" }. Optional — heartbeats already keep the connection alive on most NAT/proxy configurations.

Client → server: subscribe / unsubscribe

{ "op": "subscribe", "assets": ["USDC", "jitoSOL"] }
In v1 these are no-ops — the server broadcasts updates for every asset to every connection. Filter client-side by inspecting msg.asset. Per-asset subscription is planned for v2.

Connection limits

  • Per-IP cap: 5 concurrent connections. Beyond that, handshakes return 429.
  • Idle timeout: None. If both server heartbeat and client ping flow normally, the connection stays open indefinitely.

Reconnect strategy

Recommended client logic:
function connect() {
  const ws = new WebSocket("wss://api.pegana.xyz/v1/ws");
  let lastFrame = Date.now();

  ws.on("message", (data) => {
    lastFrame = Date.now();
    // handle msg
  });

  // Hardstop if no frame in 15s
  const watchdog = setInterval(() => {
    if (Date.now() - lastFrame > 15_000) {
      ws.terminate();
    }
  }, 5_000);

  ws.on("close", () => {
    clearInterval(watchdog);
    setTimeout(connect, jitter(1_000, 5_000));   // backoff with jitter
  });
}

connect();

What about historical data?

The WebSocket is stream-only. For history, use /v1/assets/{symbol}/history. A common pattern is to fetch the last 24h via REST on connect, then maintain real-time via WebSocket.

Next

Webhooks

Push to your endpoint instead of holding a connection.

MCP for agents

Native MCP tool for Claude, Cursor, Cline.