Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.solanatracker.io/llms.txt

Use this file to discover all available pages before exploring further.

PnL V2 Datastream is available on Premium, Business, and Enterprise plans.
Connect to wss://datastream.solanatracker.io/{apiKey} with your Data API key.
The PnL V2 Datastream pushes live trade and balance updates directly to your WebSocket client. Instead of polling the REST API, subscribe to a room and receive updates the moment they happen.
Plain terms: PnL means profit and loss. Realized PnL is from tokens already sold. Unrealized PnL is paper profit or loss on tokens still held. Cost basis is what the wallet paid for the current position.

Room Types

There are three rooms, each serving a different use case:
RoomWhat it emitsUse when
pnl:{wallet}:{token}tradeUpdate, balanceUpdate, priceUpdate for one tokenYou’re tracking one specific position
pnl:{wallet}tradeUpdate, balanceUpdate, priceUpdate for all tokensYou want live updates on every position a wallet holds
pnl:{wallet}:summaryTotal wallet PnL summaryYou want aggregated wallet-level PnL totals
PnL rooms emit tradeUpdate on every fill and balanceUpdate on balance changes. They also emit priceUpdate events, but less frequently. Think of priceUpdate as a catch-up message for quiet positions, not as a fast price feed. For the fastest unrealized PnL ticks, pair PnL rooms with a price stream.

Connecting

const ws = new WebSocket("wss://datastream.solanatracker.io/YOUR_API_KEY");

ws.onopen = () => {
  // subscribe to a room
  ws.send(JSON.stringify({
    type: "join",
    room: "pnl:WALLET_ADDRESS:TOKEN_ADDRESS"
  }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  console.log(msg.type, msg.data);
};

Room: pnl:{wallet}:{token}

Subscribe to a specific token position in a wallet. You’ll receive two message types: tradeUpdate — fires whenever a buy or sell is executed:
{
  "type": "message",
  "room": "pnl:DJqkH...D61:A7Fbn...pump",
  "data": {
    "type": "tradeUpdate",
    "wallet": "DJqkH...D61",
    "token": "A7Fbn...pump",
    "buyCount": 3,
    "sellCount": 1,
    "totalBuyUsd": 0.0257,
    "totalSellUsd": 0.0043,
    "avgCostPerToken": 0.000082,
    "currentBalance": 205.39,
    "currentPrice": 0.0000911,
    "currentValue": 0.01871,
    "holdingCostBasis": 0.01715,
    "realizedPnl": -0.00087,
    "unrealizedPnl": 0.00156,
    "proceeds": 0.0043,
    "totalTransactions": 4
  }
}
balanceUpdate — fires when the position value changes due to price movement (no new trade):
{
  "type": "message",
  "room": "pnl:DJqkH...D61:A7Fbn...pump",
  "data": {
    "type": "balanceUpdate",
    "wallet": "DJqkH...D61",
    "token": "A7Fbn...pump",
    "avgCostPerToken": 0.0000835,
    "currentBalance": 205.39,
    "currentPrice": 0.0000911,
    "currentValue": 0.01871,
    "holdingCostBasis": 0.01715,
    "unrealizedPnl": 0.00156
  }
}

Subscribe example

ws.send(JSON.stringify({
  type: "join",
  room: "pnl:DJqkHSmx9XosFpNqdi2DevtNgaf52oow4pFpJECv6D61:A7FbnhhkY2R6hZhkT2oexNNFQyGG7cBZJLWmC32spump"
}));

Room: pnl:{wallet}

Subscribe to all token positions for a wallet at once. Emits the same tradeUpdate and balanceUpdate messages as above, but for every token the wallet interacts with — you don’t need to know the token addresses in advance.
ws.send(JSON.stringify({
  type: "join",
  room: "pnl:DJqkHSmx9XosFpNqdi2DevtNgaf52oow4pFpJECv6D61"
}));

ws.onmessage = (event) => {
  const { data } = JSON.parse(event.data);
  if (!data) return;

  if (data.type === "tradeUpdate") {
    console.log(`Trade on ${data.token}: unrealized PnL $${data.unrealizedPnl.toFixed(4)}`);
  }
  if (data.type === "balanceUpdate") {
    console.log(`Price update for ${data.token}: value now $${data.currentValue.toFixed(4)}`);
  }
};
Use pnl:{wallet} when building a live portfolio dashboard — you only need one room subscription per wallet.

Room: pnl:{wallet}:summary

Receive the wallet’s total PnL summary whenever the aggregate changes. This room returns wallet-level totals, not individual positions.
{
  "type": "message",
  "room": "pnl:DJqkH...D61:summary",
  "data": {
    "averages": {
      "buy": 1.1751933473135445,
      "sell": 1.7625162156883138
    },
    "counts": {
      "buys": 1664,
      "sells": 745,
      "tokensHeldEver": 842,
      "tokensTraded": 862,
      "trades": 2409
    },
    "invested": 708.3246017180338,
    "openPositions": {
      "cost": 962.9218710198314,
      "value": 37.196319618271204
    },
    "pnl": {
      "realized": 604.7499789697601,
      "total": -421.18843262884593,
      "unrealized": -1025.938411598606
    },
    "proceeds": 1313.0745806877937,
    "roi": -59.46262936614906,
    "timing": {
      "firstTrade": 1704067810000,
      "lastTrade": 1778966213113
    }
  }
}
ws.send(JSON.stringify({
  type: "join",
  room: "pnl:DJqkHSmx9XosFpNqdi2DevtNgaf52oow4pFpJECv6D61:summary"
}));

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.room !== `pnl:${wallet}:summary`) return;
  const { data } = msg;

  console.log(`Total PnL: $${data.pnl.total.toFixed(2)}`);
  console.log(`Realized: $${data.pnl.realized.toFixed(2)}`);
  console.log(`Unrealized: $${data.pnl.unrealized.toFixed(2)}`);
  console.log(`ROI: ${data.roi.toFixed(2)}%`);
};

Fastest Live Unrealized PnL — Combining Rooms

PnL rooms give you the official cost basis, FIFO realized PnL, and balance changes when a trade lands. FIFO means “first in, first out”: older buys are matched to sells first. PnL rooms are not designed to be a tick-by-tick price feed. priceUpdate events are throttled and only fill in for positions that have gone quiet for a while. For a real-time portfolio UI where the unrealized PnL number updates on every price tick, combine the PnL room with a price stream:
StreamWhat it gives you
pnl:{wallet} (or pnl:{wallet}:{token})Live cost basis, FIFO realized PnL, balance changes, trade fills
price:aggregated:{token}Fastest cross-pool price — best for high-liquidity tokens
price-by-token:{token}Price from the primary (highest-liquidity) pool — best for single-pool / low-liquidity tokens
Use the PnL room as the source of truth for currentBalance and holdingCostBasis, and recompute unrealizedPnl = (currentBalance × livePrice) − holdingCostBasis whenever a price tick arrives.

Example: live unrealized PnL on a single position

const wallet = "DJqkHSmx9XosFpNqdi2DevtNgaf52oow4pFpJECv6D61";
const token  = "A7FbnhhkY2R6hZhkT2oexNNFQyGG7cBZJLWmC32spump";

const ws = new WebSocket("wss://datastream.solanatracker.io/YOUR_API_KEY");

const position = {
  balance: 0,
  costBasis: 0,
  realizedPnl: 0,
  livePrice: 0,
};

const recompute = () => {
  const value = position.balance * position.livePrice;
  const unrealized = value - position.costBasis;
  console.log(
    `value=$${value.toFixed(4)}  unrealized=$${unrealized.toFixed(4)}  realized=$${position.realizedPnl.toFixed(4)}`
  );
};

ws.onopen = () => {
  ws.send(JSON.stringify({ type: "join", room: `pnl:${wallet}:${token}` }));
  ws.send(JSON.stringify({ type: "join", room: `price:aggregated:${token}` }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type !== "message") return;
  const d = msg.data;

  if (d?.type === "tradeUpdate" || d?.type === "balanceUpdate") {
    position.balance   = d.currentBalance;
    position.costBasis = d.holdingCostBasis;
    if (d.type === "tradeUpdate") position.realizedPnl = d.realizedPnl;
    position.livePrice = d.currentPrice;
    recompute();
    return;
  }

  if (msg.room?.startsWith("price:aggregated:")) {
    position.livePrice = d.price;
    recompute();
  }
};

Example: live unrealized PnL across an entire wallet

Subscribe to pnl:{wallet} for trade and balance updates on every position, then dynamically subscribe to a price room for each token the wallet holds.
Price streams can update often. In production, batch UI updates or throttle logs so every price tick does not flood your console or browser.
const wallet = "DJqkHSmx9XosFpNqdi2DevtNgaf52oow4pFpJECv6D61";
const ws = new WebSocket("wss://datastream.solanatracker.io/YOUR_API_KEY");
const positions = new Map(); // token -> { balance, costBasis, realizedPnl, livePrice }
const subscribed = new Set();

const portfolioUnrealized = () => {
  let total = 0;
  for (const p of positions.values()) {
    total += p.balance * p.livePrice - p.costBasis;
  }
  return total;
};

const subscribePrice = (token) => {
  if (subscribed.has(token)) return;
  subscribed.add(token);
  // Use price:aggregated for high-liquidity tokens, price-by-token for memecoins / single-pool
  ws.send(JSON.stringify({ type: "join", room: `price:aggregated:${token}` }));
};

ws.onopen = () => {
  ws.send(JSON.stringify({ type: "join", room: `pnl:${wallet}` }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type !== "message") return;
  const d = msg.data;

  if (d?.type === "tradeUpdate" || d?.type === "balanceUpdate") {
    const p = positions.get(d.token) ?? { balance: 0, costBasis: 0, realizedPnl: 0, livePrice: 0 };
    p.balance   = d.currentBalance;
    p.costBasis = d.holdingCostBasis;
    p.livePrice = d.currentPrice;
    if (d.type === "tradeUpdate") p.realizedPnl = d.realizedPnl;
    positions.set(d.token, p);
    subscribePrice(d.token);
  }

  if (msg.room?.startsWith("price:aggregated:")) {
    const token = msg.room.split(":")[2];
    const p = positions.get(token);
    if (!p) return;
    p.livePrice = d.price;
    positions.set(token, p);
  }

  console.log(`Portfolio unrealized PnL: $${portfolioUnrealized().toFixed(4)}`);
};
Which price room?
  • price:aggregated:{token} — recommended default. Cross-pool median price; smoother and more accurate when a token trades on multiple pools.
  • price-by-token:{token} — primary-pool price. Slightly lower latency for memecoins or anything that effectively trades on a single pool.

priceUpdate payload (PnL room)

PnL rooms emit priceUpdate as a backstop when a position’s quote hasn’t moved in a while. Treat it as a periodic refresh, not a real-time feed:
{
  "type": "message",
  "room": "pnl:DJqkH...D61:A7Fbn...pump",
  "data": {
    "type": "priceUpdate",
    "wallet": "DJqkH...D61",
    "token": "A7Fbn...pump",
    "currentBalance": 205.39,
    "currentPrice": 0.0000911,
    "currentValue": 0.01871,
    "holdingCostBasis": 0.01715,
    "unrealizedPnl": 0.00156
  }
}

Unsubscribing

Leave any room by sending a leave message:
ws.send(JSON.stringify({
  type: "leave",
  room: "pnl:DJqkHSmx9XosFpNqdi2DevtNgaf52oow4pFpJECv6D61:summary"
}));

Choosing the Right Room

Use pnl:{wallet}:{token}. Lowest noise — only fires for that token.
Use pnl:{wallet}. One subscription covers all positions. You get token-level detail on every update.
Use pnl:{wallet}:summary. It returns aggregated wallet-level PnL totals without per-token position rows.
Combine pnl:{wallet} (or pnl:{wallet}:{token}) with price:aggregated:{token} or price-by-token:{token}. PnL rooms own the cost basis and balance; price rooms drive the live tick. See Combining Rooms.
Yes. Send multiple join messages. Each room operates independently.

PnL V2 Overview

Core PnL V2 concepts, endpoints, and how wallet indexing works.

Live Prices & Charts

price:aggregated and price-by-token rooms — pair these with PnL for live unrealized PnL.

Wallet Analysis

Analyze wallet PnL, positions, trade timing, and exposure with the REST API.

Wallet Tracking

Combine live wallet activity with PnL streams for a real-time portfolio dashboard.