Solana Tracker LogoSolana Tracker
Swap
Developers
⌘K
Affiliate
All Resources
Solana Price WebSocket: Aggregated and Pool Rooms
Data APITypeScriptMay 31, 202610 min readUpdated October 7, 2026Solana Tracker

Solana Price WebSocket: Aggregated and Pool Rooms

Stream live prices over a Solana price WebSocket: aggregated, per-pool and all-pool rooms, the message shape, reconnects and a REST snapshot on reconnect.

  • websocket,
  • datastream,
  • prices,
  • streaming,
  • typescript
Solana Data API›Raptor Swap API›

A solana price websocket pushes a new price to your app whenever a token's pools move, so you stop polling and stop guessing how stale the number on screen is. With Solana Tracker Datastream you join a room such as price:aggregated:{mint} and receive a median, min, max and pool count across every pool, or a single pool's price with price:{poolId}. Pair it with a REST snapshot on every connect and reconnect, and the price stays correct across network drops.

Datastream is available on the Premium, Business and Enterprise plans of the Solana Data API. The examples below use the @solana-tracker/data-api SDK 0.5.0, which handles the socket, heartbeats, reconnects and room rejoins for you.

Pick a Solana price WebSocket room

Four price rooms exist. They differ in scope, not in speed.

SDK callRoomPayloadUse it for
subscribe.price.aggregated(mint)price:aggregated:{mint}Median, average, min, max, pool count, primary pool and top poolsTickers, portfolio marks, alerts on "the" price
subscribe.price.pool(poolId)price:{poolId}One pool's USD and quote priceA specific venue, pool-level charts, arbitrage monitoring
subscribe.price.allPoolsForToken(mint)price:{mint}One message per pool update, each tagged with its poolBuilding your own cross-pool view
subscribe.price.token(mint)price-by-token:{mint}Primary pool's priceDeprecated in the SDK; use aggregated instead

Start with the aggregated room. A token often trades in several pools, and any single pool can print an outlier after one large swap. The median across pools is harder to move, and the payload still tells you the primary pool's price and the min-max range, so you can see disagreement instead of hiding it.

Use the pool room when the venue matters: you route through one pool, you chart one market, or you monitor the gap between two pools. The all-pools room is the raw material; it gives you every pool tick and leaves aggregation to you.

Message shape

Every room message arrives in the same envelope. The SDK unwraps it and calls your listener with data.

{
  "type": "message",
  "room": "price:aggregated:So11111111111111111111111111111111111111112",
  "data": { "...": "room payload" }
}

The aggregated payload, trimmed from the SDK type and docs example. timestamp is in milliseconds, and price and pool refer to the token's primary pool.

{
  "token": "So11111111111111111111111111111111111111112",
  "timestamp": 1791380000000,
  "price": 153.70,
  "pool": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv",
  "aggregated": {
    "median": 153.68,
    "average": 153.67,
    "min": 153.36,
    "max": 153.71,
    "poolCount": 10
  },
  "topPools": [
    { "poolId": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", "price": 153.70, "liquidity": 17950616, "market": "raydium-clmm" }
  ]
}

The pool and all-pools rooms send a smaller PriceUpdate. price is USD, price_quote is the price in the pool's quote token, and time is in milliseconds.

{
  "price": 0.008006,
  "price_quote": 0.00010064,
  "pool": "EWiYmq3nWQpoTkcU4UfGYEoYvDHduDsXhpPvqmoqpump",
  "token": "9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump",
  "time": 1723728065246
}

Price messages carry no traded volume. If you build candles from them, the volume column has to come from somewhere else, such as the trade stream or the chart endpoint.

WebSocket or REST polling

Datastream price roomPolling getPrice
LatencyPushed when the price changesYour polling interval, plus request time
PlanPremium or higherAny Data API plan
Cost modelUnlimited messages on Premium and above; no REST allowance usedOne request per poll against your monthly allowance
Missed updatesPossible while disconnected; recover with a REST snapshotNone, but values between polls are never seen
Best forTickers, alerts, live charts, botsReports, slow dashboards, startup state

Most apps need both. REST gives you a value at startup and after every reconnect; the socket keeps it current in between. The Solana token price API guide covers the REST side, including per-pool prices and batches of up to 100 mints.

How reconnects work in the SDK

new Datastream({ wsUrl, ... }) opens two sockets to the same URL: one for most rooms and one for rooms whose name contains transaction. Subscribing auto-connects, so calling connect() first is optional.

Reconnects are built in. With autoReconnect: true (the default), the SDK retries with capped exponential backoff and jitter controlled by reconnectDelay, reconnectDelayMax and randomizationFactor, then rejoins every room you subscribed to. It also answers the server's JSON ping with pong, so you do not need your own heartbeat.

The events tell you what happened:

  • connected fires once both sockets are open, on the first connect and again after every reconnect. This is where you fetch the REST snapshot.
  • disconnected passes which socket dropped: main, transaction or all.
  • reconnecting passes the attempt number, starting at 0.
  • error must have a listener. An EventEmitter with no error listener throws and can crash the process.

Two behaviors are easy to miss. First, an invalid key can complete the WebSocket handshake and then close immediately with no error payload, so the client keeps reconnecting. If you never see a price after the first connected, check the key and the plan. Second, disconnect() closes the sockets but does not switch off auto-reconnect in SDK 0.5.0. For a clean stop, exit the process after disconnecting, or construct the client with autoReconnect: false when you want a bounded session.

Run the example

Create an empty directory and install the SDK:

npm init -y && npm pkg set type=module
npm install @solana-tracker/data-api@^0.5.0
npm install --save-dev tsx typescript @types/node
node --env-file=.env --import tsx index.ts

Add a .env file. ST_DATASTREAM_KEY takes either the Datastream key or the full wss://datastream.solanatracker.io/ URL from the Data API dashboard. The URL contains your key, so never log it or ship it to a browser.

ST_DATASTREAM_KEY=
ST_API_KEY=
TOKEN_MINT=So11111111111111111111111111111111111111112

The script subscribes to the aggregated room, fetches a REST snapshot on every connected event, keeps whichever value has the newer server timestamp, and refreshes over REST when the room goes quiet.

import { Client, Datastream } from "@solana-tracker/data-api";

function required(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) {
    console.error(`Missing ${name}. Add it to .env`);
    process.exit(1);
  }
  return value;
}

const dsKey = required("ST_DATASTREAM_KEY");
const apiKey = required("ST_API_KEY");
const mint = process.env.TOKEN_MINT?.trim() || "So11111111111111111111111111111111111111112";
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(mint)) {
  console.error(`TOKEN_MINT is not a valid mint address: ${mint}`);
  process.exit(1);
}

const client = new Client({ apiKey });
const ds = new Datastream({
  wsUrl: dsKey.startsWith("wss://") ? dsKey : `wss://datastream.solanatracker.io/${dsKey}`,
  autoReconnect: true,
  reconnectDelay: 1_000,
  reconnectDelayMax: 30_000,
  randomizationFactor: 0.5,
});

let lastAt = 0; // newest server timestamp shown, in ms
let lastMessageAt = Date.now();
let connects = 0;

function show(source: "stream" | "rest", at: number, price: number, detail: string): void {
  if (at < lastAt) return; // older than what is on screen: drop it
  lastAt = at;
  console.log(`${new Date(at).toISOString().slice(11, 19)}  ${source.padEnd(6)}  $${price.toFixed(4)}  ${detail}`);
}

async function snapshot(reason: string): Promise<void> {
  let timer: NodeJS.Timeout | undefined;
  try {
    const timeout = new Promise<never>((_, reject) => {
      timer = setTimeout(() => reject(new Error("getPrice timed out")), 10_000);
    });
    const p = await Promise.race([client.getPrice(mint), timeout]);
    if (Number.isFinite(p.price)) show("rest", p.lastUpdated || Date.now(), p.price, `${reason} snapshot`);
  } catch (error) {
    console.warn(`REST snapshot failed (${reason}): ${error instanceof Error ? error.message : String(error)}`);
  } finally {
    clearTimeout(timer);
  }
}

// Never log the error object or the URL: the URL contains your key.
ds.on("error", () => console.error("Datastream error (check ST_DATASTREAM_KEY and plan access)"));
ds.on("disconnected", (socket: string) => console.warn(`Disconnected (${socket})`));
ds.on("reconnecting", (attempt: number) => console.warn(`Reconnecting, attempt ${attempt + 1}`));
ds.on("connected", () => {
  connects++;
  void snapshot(connects === 1 ? "initial" : "reconnect");
});

const sub = ds.subscribe.price.aggregated(mint);
const listener = sub.on((tick) => {
  lastMessageAt = Date.now();
  const a = tick.aggregated;
  if (!a || !Number.isFinite(a.median)) return;
  show("stream", tick.timestamp, a.median, `${a.poolCount} pools, $${a.min.toFixed(4)} to $${a.max.toFixed(4)}`);
});
console.log(`Joined ${sub.room}. Ctrl+C to stop.`);

// A quiet token and a stalled socket look the same; refresh instead of guessing.
const watchdog = setInterval(() => {
  if (Date.now() - lastMessageAt > 30_000) {
    lastMessageAt = Date.now();
    void snapshot("stale");
  }
}, 5_000);

process.once("SIGINT", () => {
  clearInterval(watchdog);
  listener.unsubscribe();
  ds.unsubscribe(sub.room);
  ds.disconnect();
  process.exit(0); // also stops auto-reconnect, which disconnect() alone does not
});

The companion project adds an optional pool room, output throttling, a not-connected hint after 20 seconds and a shutdown summary.

Production pitfalls

Order by server time, not arrival time. Around a reconnect, the REST snapshot and the first stream message can land in either order. Compare timestamp (aggregated) or time (pool rooms) with the last value you applied and drop anything older.

The snapshot is the recovery mechanism. The SDK rejoins rooms, but it does not replay what was published while you were offline. For a price that is fine, since only the latest value matters; fetch it with getPrice on every connected event. If you keep a history, backfill it from the OHLCV endpoint instead.

Silence is ambiguous. A token that nobody trades produces no price messages. A stalled connection also produces none. Track the time since the last message and refresh over REST after a threshold rather than raising an alarm.

Throttle rendering, not the socket. A busy token can update several times a second. Keep the newest value in memory and render on a timer. Do not unsubscribe and resubscribe to slow the feed down.

Median, primary and pool prices answer different questions. Display the median for "the price", the primary pool price when you show where most liquidity sits, and a pool's own price when you trade on that pool. Label which one you show.

One connection, many rooms. Datastream has no limit on rooms per connection. Use one client per process and subscribe to every token you need, keying state by token or pool, instead of opening a socket per token.

Keep the key server-side. The key is part of the WebSocket URL. Run Datastream on your backend and fan prices out to browsers through your own channel.

Prices are not quotes. A live price does not include your trade size, route or slippage. Before a swap, request a quote from the Raptor Swap API.

FAQ

Which plan includes the price WebSocket?

Datastream is available on Premium, Business and Enterprise plans. Messages are unlimited on those plans and do not count against the REST request allowance.

Do I need to reply to pings or rejoin rooms after a reconnect?

Not with the SDK. It answers the server's JSON ping with pong, reconnects with backoff and rejoins every subscribed room. You only need to fetch a fresh REST snapshot when connected fires again.

Why do I keep reconnecting without receiving any prices?

The most common cause is an invalid key or a plan without Datastream. The server can accept the handshake and then close the connection with no error message, which triggers the reconnect loop. Check ST_DATASTREAM_KEY and your plan in the dashboard.

Should I use the price-by-token room?

No. subscribe.price.token() is deprecated in the SDK and logs a warning. The aggregated room includes the primary pool's price and pool id, plus the cross-pool median and range.

Can I build candles from price messages?

You can update the open, high, low and close of the current candle from ticks, but price messages carry no volume. Load history from the chart endpoint and take volume from trades; see the trades WebSocket guide.

References

  • Stream Solana token prices and update live charts (guide)
  • Datastream protocol: join, leave, heartbeat, auth failures
  • Price aggregated room
  • Price by pool room
  • Price all pools room
  • Price by token room (deprecated in the SDK)
  • Get token price (REST)
  • @solana-tracker/data-api on npm

Companion project

The companion project streams the aggregated room for TOKEN_MINT, optionally a single pool with POOL_ADDRESS, takes a REST snapshot on every connect and reconnect, and refreshes over REST after STALE_AFTER_SECONDS without a message.

cp .env.example .env && npm install && npm start
View source on GitHub›Run in StackBlitz›

Runnable Node.js project — clone from GitHub, add keys to .env, then npm start. Use a private local environment for credentials. Check the companion package version before running.

Related Guides

Solana Trades WebSocket: Live Swaps Without Gaps
Data API

Solana Trades WebSocket: Live Swaps Without Gaps

Read more
Solana Liquidity Events API: LP History and Live Stream
Data API

Solana Liquidity Events API: LP History and Live Stream

Read more
Jupiter DCA API: Track Recurring Orders and Live Fills
Data API

Jupiter DCA API: Track Recurring Orders and Live Fills

Read more

Products

  • Data API
  • Pump.fun API
  • Solana RPC
  • Dedicated Nodes
  • Yellowstone gRPC
  • Raptor Swap API
  • Enterprise

Trading

  • Swap
  • Latest Tokens
  • Trending
  • Top Gainers
  • Memescope
  • Whale Watch
  • KOL Tracker

Tools

  • Wallet Tracker
  • Rugcheck
  • PnL Leaderboard
  • KOLScan
  • Axiom Leaderboard
  • Photon Leaderboard
  • Bloom Leaderboard
  • FOMO Leaderboard
  • GMGN Leaderboard
  • Pump.fun App Leaderboard
  • Terminal Leaderboard
  • Platform Compare
  • My Positions
  • Teams

Resources

  • Developer Guides
  • Blog
  • Documentation
  • API Reference
  • Status
  • Affiliate Program — 25% recurring, uncapped
Solana TrackerSolana Tracker© 2026
Terms of ServicePrivacy PolicyContact