Solana Tracker LogoSolana Tracker
Swap
Developers
⌘K
Affiliate
All Resources
Solana Trades WebSocket: Live Swaps Without Gaps
Data APITypeScriptJune 8, 202612 min readUpdated October 7, 2026Solana Tracker

Solana Trades WebSocket: Live Swaps Without Gaps

Stream buys and sells over a Solana trades WebSocket by token, pool or wallet, backfill with REST, dedupe trades and recover what you missed on reconnect.

  • websocket,
  • datastream,
  • trades,
  • streaming,
  • typescript
Solana Data API›Enterprise›

A solana trades websocket delivers every buy and sell for a token, pool or wallet the moment it is indexed, instead of making you poll trade history. With Solana Tracker Datastream you join transaction:{mint} (or a pool, wallet, whale or KOL room) and receive trade objects with the signature, side, USD and SOL volume, token amount, price, wallet and DEX program. A reliable feed also backfills recent trades over REST, dedupes the overlap and refetches what it missed after a reconnect; this guide builds exactly that.

Datastream is included in the Premium, Business and Enterprise plans of the Solana Data API. The code uses the @solana-tracker/data-api SDK 0.5.0, which manages the sockets, heartbeats, reconnects and room rejoins.

Solana trades WebSocket rooms

Pick the narrowest room that answers your question. Every room below is a method on ds.subscribe.tx.

SDK callRoomWhat arrives
token(mint, opts?)transaction:{mint}Every swap of the token across all pools
pool(mint, pool, opts?)transaction:{mint}:{pool}Swaps of the token in one pool
poolWallet(mint, pool, wallet, opts?)transaction:{mint}:{pool}:{wallet}One wallet's swaps in one pool
wallet(address, opts?)wallet:{address}One wallet's swaps across all tokens (wallet-shaped payload)
whale(minVolume?, opts?)`transaction:whale:{1000\2500\5000\10000}`Trades at or above a USD tier, any token
kol(minVolume?, opts?)transaction:kol or transaction:kol:{tier}Trades by wallets on the shared KOL roster

Passing { enriched: true } appends :enriched to the room and adds wallet identity to each trade (see the wallet identity guide). Identity reflects current labels, and an incomplete lookup is marked identityStatus: "partial" without a later correction.

Whale and KOL volume tiers are cumulative: a $12,000 trade is published to the 1,000, 2,500, 5,000 and 10,000 rooms. Join one tier, not all four, or you will count the same trade several times. Whale rooms exclude trades under $1,000; the plain transaction:kol room has no volume floor.

The trade message shape

Room messages arrive as { type: "message", room, data }, and on token transaction rooms data is an array of trades. The SDK calls your listener once per trade. This is the TokenTransaction shape, trimmed from the SDK type and the docs example:

{
  "tx": "5V4apVkgHf49J5acYPiuh5rB89EGsF8ocSysgnD9vFfZsTtDamhN3MFDKJSq7Dn7Z6Y5XhjghuVYuvvYWnSFpnvW",
  "amount": 90,
  "priceUsd": 6.002467911111111,
  "volume": 540.22,
  "solVolume": 2.956,
  "type": "sell",
  "wallet": "F7R61te4Ac8xZ4pfkJUdF6iaaHy9tnM8nVRUPv8H8EMi",
  "time": 1760201288835,
  "program": "raydium",
  "token": {
    "from": { "name": "OFFICIAL TRUMP", "symbol": "TRUMP", "decimals": 6, "amount": 90, "address": "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN" },
    "to": { "name": "USD Coin", "symbol": "USDC", "decimals": 6, "amount": 540.22, "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }
  }
}

volume is USD, solVolume is SOL, amount is the token amount in UI units and time is milliseconds. token.from is what the wallet gave and token.to is what it received.

REST trade history uses a slightly different shape for the same trade. Normalize both into one type before you store or render anything:

MeaningLive room (TokenTransaction)REST history (Trade)
SOL volumesolVolumevolumeSol
Token legstoken.from, token.tonot included
Poolsnot includedpools (array of pool ids)
Signature, side, USD volume, amount, price, wallet, time, programsame namessame names

WebSocket or REST trade history

Datastream transaction roomsgetTokenTradeHistory and variants
DeliveryPushed as trades are indexedPages of up to 500 trades (default 250)
HistoryOnly what happens while connectedPaginate backwards with nextCursor
OrderingArrival order, mostly chronologicalsortDirection DESC (default) or ASC
FiltersToken, pool, pool and wallet, wallet, whale tier, KOLToken, pool, wallet in token, wallet in pool; hideArb, enrich, events
PlanPremium or higherAny Data API plan
Best forLive tapes, alerts, copy-trade triggersBackfill, reconciliation, analytics

You need both. REST fills the screen on startup and recovers trades after a disconnect; the socket keeps it current. The REST methods are getTokenTradeHistory(mint, params), getPoolTradeHistory(mint, pool, params), getUserTokenTradeHistory and getUserPoolTradeHistory. Keep events, sortDirection and the scope unchanged while you reuse a cursor. Swap cursors are timestamps, while mixed or liquidity feeds return opaque strings, so store nextCursor as given.

Dedupe and ordering

Duplicates are normal in a feed that combines a backfill, a live socket and reconnects. Plan for these cases:

  • Backfill overlap. A trade that lands while the REST request is in flight can arrive from both sources.
  • One signature, several trades. A routed swap can contain more than one trade under the same tx. Deduping by signature alone silently drops real trades.
  • Reconnect replays. After a reconnect, the trades you refetch over REST overlap with what you already showed.
  • SDK-level dedupe is partial. In SDK 0.5.0, array payloads on token, pool and wallet rooms (the normal shape) pass through untouched, as do :enriched rooms. Whale and KOL rooms are deduped per room by signature, so a second trade in the same transaction can be dropped there. Do not depend on either behavior; dedupe in your own code.

A practical key is signature, wallet, side and amount: ${tx}:${wallet}:${type}:${amount}. Keep the set bounded (a few thousand recent keys) so memory does not grow forever.

Arrival order is close to chronological but not guaranteed, especially across a reconnect. Sort the backfill by time, flush buffered live trades in time order, and flag a trade as late when its time is older than the newest one you have printed rather than reordering the whole tape.

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. Get both keys from the Data API dashboard. ST_DATASTREAM_KEY takes the key or the full wss://datastream.solanatracker.io/ URL; it contains your key, so never log it. TOKEN_MINT defaults to the token used in the docs.

ST_DATASTREAM_KEY=
ST_API_KEY=
TOKEN_MINT=6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN

The script subscribes first and buffers, backfills the last 25 trades over REST, flushes the buffer deduped, and walks REST history back after every reconnect until it overlaps what it already printed.

import { Client, Datastream } from "@solana-tracker/data-api";
import type { EnrichedTokenTransaction, EnrichedTrade } 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() || "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN";
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);
}

type Row = { tx: string; wallet: string; side: "buy" | "sell"; amount: number; usd: number; sol: number; time: number };
const fromLive = (t: EnrichedTokenTransaction): Row =>
  ({ tx: t.tx, wallet: t.wallet, side: t.type, amount: t.amount, usd: t.volume, sol: t.solVolume, time: t.time });
const fromRest = (t: EnrichedTrade): Row =>
  ({ tx: t.tx, wallet: t.wallet, side: t.type, amount: t.amount, usd: t.volume, sol: t.volumeSol, time: t.time });

const seen = new Set<string>();
const keyOf = (r: Row) => `${r.tx}:${r.wallet}:${r.side}:${r.amount}`;
let newest = 0;

function emit(r: Row, source: string): void {
  const key = keyOf(r);
  if (seen.has(key)) return;
  seen.add(key);
  if (seen.size > 5_000) seen.delete(seen.values().next().value as string);
  const late = r.time < newest ? " (late)" : "";
  newest = Math.max(newest, r.time);
  console.log(`${new Date(r.time).toISOString().slice(11, 19)} ${r.side.toUpperCase().padEnd(4)} $${r.usd.toFixed(2).padStart(10)}` +
    ` ${r.sol.toFixed(3)} SOL ${r.wallet.slice(0, 4)}…${r.wallet.slice(-4)} ${r.tx.slice(0, 8)} ${source}${late}`);
}

const client = new Client({ apiKey });
async function history(maxPages: number): Promise<Row[]> {
  const rows: Row[] = [];
  let cursor: number | string | undefined;
  for (let page = 0; page < maxPages; page++) {
    let timer: NodeJS.Timeout | undefined;
    const timeout = new Promise<never>((_, reject) => {
      timer = setTimeout(() => reject(new Error("trade history timed out")), 15_000);
    });
    const params = { limit: 25, sortDirection: "DESC" as const, ...(cursor !== undefined ? { cursor } : {}) };
    const res = await Promise.race([client.getTokenTradeHistory(mint, params), timeout]).finally(() => clearTimeout(timer));
    const pageRows = res.trades.map(fromRest);
    rows.push(...pageRows);
    if (pageRows.some((r) => seen.has(keyOf(r))) || !res.hasNextPage || res.nextCursor == null) break;
    cursor = res.nextCursor;
  }
  return rows.sort((a, b) => a.time - b.time);
}

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,
});
ds.on("error", () => console.error("Datastream error (check ST_DATASTREAM_KEY and plan access)"));
ds.on("reconnecting", (attempt: number) => console.warn(`Reconnecting, attempt ${attempt + 1}`));

let connectedOnce = false;
ds.on("connected", () => {
  if (connectedOnce) {
    // The SDK rejoined the room; REST recovers what was published while we were offline.
    history(5)
      .then((rows) => rows.forEach((r) => emit(r, "gap")))
      .catch((e: unknown) => console.warn(`Gap fill failed: ${e instanceof Error ? e.message : String(e)}`));
  }
  connectedOnce = true;
});

let buffer: Row[] | null = []; // live trades that arrive during the backfill
const sub = ds.subscribe.tx.token(mint, { enriched: false });
const listener = sub.on((t) => {
  if (typeof t?.tx !== "string" || (t.type !== "buy" && t.type !== "sell")) return;
  if (buffer) buffer.push(fromLive(t));
  else emit(fromLive(t), "live");
});
console.log(`Joined ${sub.room}. Ctrl+C to stop.`);

try {
  for (const r of await history(1)) emit(r, "rest");
} catch (e) {
  console.warn(`Backfill failed, continuing live only: ${e instanceof Error ? e.message : String(e)}`);
}
const pending = (buffer ?? []).sort((a, b) => a.time - b.time);
buffer = null;
pending.forEach((r) => emit(r, "live"));

process.once("SIGINT", () => {
  listener.unsubscribe();
  ds.unsubscribe(sub.room);
  ds.disconnect();
  process.exit(0); // disconnect() alone does not stop auto-reconnect in SDK 0.5.0
});

The companion project adds pool scoping with POOL_ADDRESS, a MIN_USD filter, optional identity labels with ENRICHED=true, REST retries with backoff, periodic stats and a net-flow summary on exit.

Production pitfalls

Reconnects lose messages. The SDK reconnects with backoff and rejoins the room, but nothing replays the trades published while the socket was down. Treat each connected event after the first as a signal to fetch recent history and print only unseen trades.

An invalid key looks like a flaky network. The server can accept the handshake and then close immediately with no error payload, so the client reconnects in a loop. If you never see a trade after connecting to a busy token, check the key and your plan before debugging the network.

Volume is per trade, not per transaction. Grouping by signature merges the separate trades of a routed swap into one row and hides how much each wallet traded. Aggregate on your normalized trade rows instead.

Backfill and live must use the same filters. REST history accepts hideArb: true, which keeps only swap legs involving the requested token. The live room has no equivalent switch, so if you enable it on REST, your backfill and live tape count different things. Leave it off when you merge both sources, or apply the same rule to live rows yourself.

Tier rooms are cumulative. A whale trade appears in every tier room it qualifies for. Join one tier per purpose.

Enriched identity is not historical. Labels describe the wallet as it is now, not when it traded, and a partial identity is not corrected later. Enriched notifications can also arrive later or out of order compared with the plain room, so never block the tape on identity; render the trade and attach the label when present.

Exit cleanly. In SDK 0.5.0, disconnect() closes the sockets but auto-reconnect can reopen them. Exit the process after disconnecting, or create a bounded client with autoReconnect: false.

Liquidity is a separate stream. Adds and removals do not arrive on transaction rooms. Use the liquidity rooms and events: "liquidity" on REST history; the liquidity events guide covers them.

FAQ

Which plan do I need for live trades?

Datastream is available on Premium, Business and Enterprise plans. REST trade history works on any Data API plan, so a backfill-only tool does not need Datastream.

How do I follow one wallet instead of one token?

Use subscribe.tx.wallet(address) for that wallet across all tokens, or subscribe.tx.poolWallet(mint, pool, wallet) for one wallet in one pool. The wallet room uses a wallet-shaped payload with price, volume, from and to objects, so normalize it separately.

Why do I see the same signature twice?

Either the trade arrived from both the backfill and the socket, or the transaction contains several trades. Dedupe on signature plus wallet, side and amount, not on the signature alone.

Can I get only large trades?

For one token, filter on volume in your listener. Across all tokens, join subscribe.tx.whale(5000) or another tier; trades below $1,000 never reach whale rooms.

How far back can I backfill?

Page through getTokenTradeHistory with nextCursor and hasNextPage, up to 500 trades per page. Keep the scope, events and sortDirection the same across pages.

Can I turn trades into candles?

Yes for the current interval: sum volume and track the first, highest, lowest and last priceUsd. For history, load candles from the OHLCV endpoint instead of replaying trades. For live prices without trades, see the Solana price WebSocket guide.

References

  • Build a Solana trade feed with Datastream (guide)
  • Datastream protocol: join, leave, heartbeat, auth failures
  • Token transactions room
  • Pool transactions room
  • Wallet transactions room
  • Whale trades room
  • KOL trades room
  • Get token trades (REST)
  • Get pool-specific trades (REST)
  • @solana-tracker/data-api on npm

Companion project

The companion project streams trades for TOKEN_MINT or one pool, backfills and dedupes against REST, fills reconnect gaps with cursor pagination, and prints rolling stats.

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 Price WebSocket: Aggregated and Pool Rooms
Data API

Solana Price WebSocket: Aggregated and Pool Rooms

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