> ## 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

> Subscribe to live profit and loss updates for Solana wallets over WebSocket — track realized PnL, position changes, and trade activity in real time.

<Info>
  PnL V2 Datastream is available on **Premium, Business, and Enterprise** plans.\
  Connect to `wss://datastream.solanatracker.io/{apiKey}` with your Data API key.
</Info>

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.

<Info>
  **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.
</Info>

## Room Types

There are three rooms, each serving a different use case:

| Room                   | What it emits                                                | Use when                                               |
| ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------ |
| `pnl:{wallet}:{token}` | `tradeUpdate`, `balanceUpdate`, `priceUpdate` for one token  | You're tracking one specific position                  |
| `pnl:{wallet}`         | `tradeUpdate`, `balanceUpdate`, `priceUpdate` for all tokens | You want live updates on every position a wallet holds |
| `pnl:{wallet}:summary` | Total wallet PnL summary                                     | You want aggregated wallet-level PnL totals            |

<Note>
  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](#fastest-live-unrealized-pnl-combining-rooms).
</Note>

***

## Connecting

```javascript theme={null}
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:

```json theme={null}
{
  "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):

```json theme={null}
{
  "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

```javascript theme={null}
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.

```javascript theme={null}
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)}`);
  }
};
```

<Tip>
  Use `pnl:{wallet}` when building a live portfolio dashboard — you only need one room subscription per wallet.
</Tip>

***

## Room: pnl:\{wallet}:summary

Receive the wallet's total PnL summary whenever the aggregate changes. This room returns wallet-level totals, not individual positions.

```json theme={null}
{
  "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
    }
  }
}
```

```javascript theme={null}
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**:

| Stream                                     | What 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

```javascript theme={null}
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.

<Warning>
  Price streams can update often. In production, batch UI updates or throttle logs so every price tick does not flood your console or browser.
</Warning>

```javascript theme={null}
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)}`);
};
```

<Tip>
  **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.
</Tip>

### `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:

```json theme={null}
{
  "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:

```javascript theme={null}
ws.send(JSON.stringify({
  type: "leave",
  room: "pnl:DJqkHSmx9XosFpNqdi2DevtNgaf52oow4pFpJECv6D61:summary"
}));
```

***

## Choosing the Right Room

<AccordionGroup>
  <Accordion title="I'm tracking one specific position">
    Use `pnl:{wallet}:{token}`. Lowest noise — only fires for that token.
  </Accordion>

  <Accordion title="I'm building a live portfolio dashboard">
    Use `pnl:{wallet}`. One subscription covers all positions. You get token-level detail on every update.
  </Accordion>

  <Accordion title="I want a portfolio summary without per-token noise">
    Use `pnl:{wallet}:summary`. It returns aggregated wallet-level PnL totals without per-token position rows.
  </Accordion>

  <Accordion title="I want the fastest possible unrealized PnL ticks">
    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](#fastest-live-unrealized-pnl-combining-rooms).
  </Accordion>

  <Accordion title="Can I subscribe to multiple rooms at once?">
    Yes. Send multiple `join` messages. Each room operates independently.
  </Accordion>
</AccordionGroup>

***

## Related Guides

<CardGroup cols={2}>
  <Card title="PnL V2 Overview" href="/guides/pnl-v2/index" icon="chart-bar" iconType="duotone">
    Core PnL V2 concepts, endpoints, and how wallet indexing works.
  </Card>

  <Card title="Live Prices & Charts" href="/guides/datastream-prices" icon="bolt" iconType="duotone">
    `price:aggregated` and `price-by-token` rooms — pair these with PnL for live unrealized PnL.
  </Card>

  <Card title="Wallet Analysis" href="/guides/pnl-v2/wallet-analysis" icon="wallet" iconType="duotone">
    Analyze wallet PnL, positions, trade timing, and exposure with the REST API.
  </Card>

  <Card title="Wallet Tracking" href="/guides/wallet-tracking" icon="coins" iconType="duotone">
    Combine live wallet activity with PnL streams for a real-time portfolio dashboard.
  </Card>
</CardGroup>
