Solana Tracker LogoSolana Tracker
Swap
Developers
⌘K
Affiliate
All Resources
Solana Wallet Portfolio API: Holdings and Live Balances
Data APITypeScriptJune 1, 202611 min readUpdated October 7, 2026Solana Tracker

Solana Wallet Portfolio API: Holdings and Live Balances

Use the Solana wallet portfolio API to fetch token holdings, USD and SOL value, risk and 24h change, then follow live balance changes over Datastream.

  • wallets,
  • portfolio,
  • data-api,
  • datastream,
  • typescript
Solana Data API›Solana RPC›

The Solana wallet portfolio API is GET /wallet/{owner} on the Solana Tracker Data API, exposed as client.getWallet(owner) in @solana-tracker/data-api 0.5.0. One call returns every token a wallet holds with its balance, USD value, token metadata, pools, price-change events and risk score, plus the wallet total in USD and SOL. For live updates without polling, the Datastream wallet:{address}:balance room pushes the new balance whenever a token amount changes.

This page covers current holdings and their value. Trading profit is a different question with a different endpoint: see the Solana wallet PnL API for realized and unrealized PnL, ranking wallets by PnL for leaderboards, and wallet identity enrichment for labeling who a wallet is.

Choosing a Solana wallet portfolio API endpoint

The Solana Data API has several wallet routes. They differ in payload size, caching and what they are for.

NeedMethodEndpointNotes
Full holdings with metadata, pools, riskgetWallet(owner)GET /wallet/{owner}Default choice for a portfolio view
Lightweight balances and pricesgetWalletBasic(owner)GET /wallet/{owner}/basicNon-cached; flat rows with address, balance, value, price, marketCap, liquidity
Very large walletsgetWalletPage(owner, page)GET /wallet/{owner}/page/{page}250 tokens per request, same shape as getWallet
Historical portfolio valuegetWalletChart(owner)GET /wallet/{owner}/chartLegacy value series with 24h and 30d change; may return HTTP 400 when no chart can be built
Live balance changessubscribe.wallet(w).balance()Datastream roomPremium plan or higher; amounts only, no prices
Trading profitgetPnlV2WalletOverview(w)GET /v2/pnl/wallets/{w}Cost basis and realized PnL, not holdings

If you only need raw token accounts and amounts, standard Solana RPC methods return them, but you then have to resolve mints, decimals, prices and liquidity yourself. The wallet endpoint does that join server-side, which is the point of using it.

Use getWallet when a user opens a wallet page. Use getWalletBasic when you poll many wallets and only need balances and prices, because it skips metadata and risk. Use the chart route only for a portfolio-value sparkline: the docs state it is not equivalent to the PnL v2 trade charts.

Response shape

The SDK types the response as WalletResponse. Trimmed to the fields this guide uses:

interface WalletResponse {
  tokens: WalletTokenDetail[];
  total: number;      // wallet value in USD
  totalSol: number;   // the same value expressed in SOL
  timestamp: string;  // typed in the SDK; treat as optional at runtime
}

interface WalletTokenDetail {
  token: { name: string; symbol: string; mint: string; decimals: number; image?: string };
  pools?: Array<{
    poolId: string;
    market: string;
    liquidity: { quote: number; usd: number };
    price: { quote: number; usd: number };
    marketCap: { quote: number; usd: number };
    lastUpdated: number;
  }>;
  events?: Record<'1m' | '5m' | '15m' | '30m' | '1h' | '24h' /* and more */, { priceChangePercentage: number }>;
  risk?: { score: number };
  balance: number;    // UI amount, decimals applied
  value: number;      // USD value of the balance
}

Trimmed from the documented example response (one of four tokens, one of several pools):

{
  "tokens": [
    {
      "token": { "name": "Native Solana", "symbol": "SOL", "mint": "So11111111111111111111111111111111111111111", "decimals": 9 },
      "pools": [
        {
          "poolId": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv",
          "market": "raydium-clmm",
          "liquidity": { "quote": 14187388.72, "usd": 14187388.72 },
          "price": { "quote": 181.49, "usd": 181.49 },
          "lastUpdated": 1760274181116
        }
      ],
      "events": { "24h": { "priceChangePercentage": -0.72 } },
      "risk": { "score": 0 },
      "balance": 0.019911506,
      "value": 3.6138035170672844
    }
  ],
  "total": 3.7951372323268373,
  "totalSol": 0.02091100438964804
}

There is no per-row "share of portfolio" field. Compute it as value / total. There is also no unit price on the row itself; either read pools[].price.usd or derive the price the endpoint used as value / balance. The example below uses the derived price so a live balance change is revalued exactly the way the snapshot valued it.

Build a wallet tracker in TypeScript

Create a project 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

Your .env needs a Data API key from the Data API dashboard. The Datastream key is optional and only enables live mode.

# .env.example
ST_API_KEY=
ST_DATASTREAM_KEY=
WALLET_ADDRESS=FbMxP3GVq8TQ36nbYgx4NP9iygMpwAwFWJwW81ioCiSF
MIN_VALUE_USD=1

The REST client has no retry and no timeout of its own, so the script wraps the call. It validates the address before spending a request, prints the snapshot, and only opens a WebSocket when a Datastream key is present.

import { Client, Datastream, DataApiError, RateLimitError, ValidationError } from '@solana-tracker/data-api';
import type { WalletResponse } from '@solana-tracker/data-api';

const apiKey = process.env.ST_API_KEY?.trim();
if (!apiKey) { console.error('Missing ST_API_KEY'); process.exit(1); }
const wallet = process.env.WALLET_ADDRESS?.trim() || 'FbMxP3GVq8TQ36nbYgx4NP9iygMpwAwFWJwW81ioCiSF';
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet)) { console.error('WALLET_ADDRESS is not base58'); process.exit(1); }
const minValue = Number(process.env.MIN_VALUE_USD ?? 1);

const NATIVE_SOL = 'So11111111111111111111111111111111111111111';
const WRAPPED_SOL = 'So11111111111111111111111111111111111111112';
const client = new Client({ apiKey });
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

async function fetchWallet(attempts = 4): Promise<WalletResponse> {
  for (let i = 1; ; i++) {
    let timer: NodeJS.Timeout | undefined;
    try {
      const timeout = new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error('timeout')), 20_000); });
      return await Promise.race([client.getWallet(wallet), timeout]);
    } catch (err) {
      const retryable = err instanceof RateLimitError
        || (err instanceof DataApiError && !(err instanceof ValidationError) && (err.status ?? 500) >= 500)
        || !(err instanceof DataApiError);
      if (!retryable || i >= attempts) throw err;
      const retryAfterMs = err instanceof RateLimitError && err.retryAfter ? err.retryAfter * 1000 : 0;
      await sleep(Math.max(retryAfterMs, Math.min(8_000, 500 * 2 ** (i - 1))));
    } finally {
      clearTimeout(timer);
    }
  }
}

type Holding = { symbol: string; balance: number; value: number; unitPrice: number | null };
let holdings = new Map<string, Holding>();

function load(snapshot: WalletResponse) {
  holdings = new Map(snapshot.tokens.map((row) => [row.token.mint, {
    symbol: row.token.symbol || row.token.mint.slice(0, 4),
    balance: row.balance,
    value: row.value,
    unitPrice: row.balance > 0 ? row.value / row.balance : null,
  }]));
}

try {
  const snapshot = await fetchWallet();
  load(snapshot);
  for (const row of [...snapshot.tokens].sort((a, b) => b.value - a.value)) {
    if (row.value < minValue) continue;
    const share = snapshot.total > 0 ? (row.value / snapshot.total) * 100 : 0;
    console.log(row.token.symbol.padEnd(10), row.balance.toFixed(4).padStart(18), `$${row.value.toFixed(2)}`.padStart(14), `${share.toFixed(1)}%`);
  }
  console.log(`Total $${snapshot.total.toFixed(2)} (${snapshot.totalSol.toFixed(3)} SOL), ${snapshot.tokens.length} tokens`);
} catch (err) {
  console.error('Wallet request failed:', err instanceof DataApiError ? `HTTP ${err.status}` : (err as Error).message);
  process.exit(1);
}

const dsKey = process.env.ST_DATASTREAM_KEY?.trim();
if (dsKey) {
  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 connectedOnce = false;
  ds.on('connected', () => {
    // After a reconnect, updates sent while offline are gone: re-read the snapshot.
    if (connectedOnce) fetchWallet().then(load).catch((e) => console.warn('refresh failed', (e as Error).message));
    connectedOnce = true;
  });
  ds.on('error', () => console.error('Datastream error (check key and plan)'));

  const listener = ds.subscribe.wallet(wallet).balance().on((update) => {
    const mint = update.token === WRAPPED_SOL && !holdings.has(WRAPPED_SOL) ? NATIVE_SOL : update.token;
    const h = holdings.get(mint);
    if (!h) { console.log(`new mint ${mint}: ${update.amount} (re-fetch snapshot to price it)`); return; }
    h.balance = update.amount;
    if (h.unitPrice !== null) h.value = update.amount * h.unitPrice;
    console.log(new Date().toISOString().slice(11, 19), h.symbol, '->', update.amount, `$${h.value.toFixed(2)}`);
  });

  process.once('SIGINT', () => { listener.unsubscribe(); ds.disconnect(); process.exit(0); });
}

The companion project splits the same flow into a reusable retry helper and a formatted table with 24h change, top-pool liquidity and risk score, and it re-fetches the snapshot on new mints and on a timer.

Live balances over Datastream

subscribe.wallet(address).balance() joins wallet:{address}:balance. Each message is { wallet, token, amount }. Three details decide whether your numbers stay correct:

  • amount is the balance after the change, as a UI amount. It is not a delta and not raw base units. Replace the stored balance; do not add to it. Compute the delta yourself if you want to display one.
  • SOL arrives under the wrapped SOL mint. The balance rooms report native SOL as So11111111111111111111111111111111111111112, while the REST wallet response lists native SOL as So11111111111111111111111111111111111111111. Map one to the other, or the SOL row never updates.
  • There is no price in the message. A balance stream tells you amounts, not value. Revalue with the last known price and refresh the snapshot periodically, or pair it with a price stream as described in the Solana price WebSocket guide.

For a single token, subscribe.wallet(address).tokenBalance(mint) joins the narrower wallet:{address}:{mint}:balance room with the same payload. The SDK reconnects with capped, jittered backoff and rejoins rooms on its own, but it cannot replay messages sent while the socket was down. Treat every connected event after the first as a signal to re-read REST.

If you need raw on-chain writes instead of indexed balances, with the slot and signature of every change, stream the wallet's token accounts directly with a Yellowstone gRPC account subscribe.

ApproachFreshnessCostBest for
Poll getWalletYour intervalOne request per pollOccasional views, many wallets
Poll getWalletBasicYour interval, non-cachedLighter payloadBalance checks at scale
Balance room plus periodic snapshotPush for amounts, interval for pricesOne socket plus occasional requestsA wallet someone is watching right now

Production pitfalls

Do not treat value as profit. total is a mark-to-market of current holdings. A wallet that received an airdrop has value without any trade. For cost basis and realized gains use the Solana wallet PnL API, and keep the two panels on separate loading and error states.

Key on the mint, never the symbol. Symbols collide and can be empty. The example falls back to a short mint when symbol is blank.

Filter dust before you render. Wallets collect spam tokens with tiny or zero value. A MIN_VALUE_USD threshold keeps the table readable without dropping data you still have in memory.

Value is not liquidation value. value multiplies the balance by a price. A large position in a thin pool cannot be sold at that price. Show pools[].liquidity.usd next to large positions so the reader can judge.

Handle optional fields. pools, events and risk are optional in the SDK types, and timestamp is typed but missing from the published example response. Guard every nested read.

Rate limits and timeouts are yours to manage. The SDK throws RateLimitError with retryAfter in seconds when it is available, DataApiError with status for HTTP failures, and ValidationError for bad input. Retry 429 and 5xx with backoff, do not retry 4xx, and set a per-attempt timeout.

Never log the Datastream URL. It contains your key. Log connection state, not configuration.

FAQ

Which endpoint returns the total value of a Solana wallet?

GET /wallet/{owner} (getWallet) returns total in USD and totalSol in SOL alongside every token row. getWalletBasic returns the same totals with a lighter, non-cached token list.

Is the balance in raw units or decimals?

Both the REST balance and the Datastream amount are UI amounts with decimals applied. Convert with the mint's decimals only if you need raw integer units.

Why does SOL not update in my live view?

The balance room uses the wrapped SOL mint for SOL, while the REST response uses the native SOL id ending in 1111. Map the wrapped mint to your native SOL row.

Can I get historical portfolio value?

getWalletChart returns a legacy portfolio-value series with 24h and 30d change, and may return HTTP 400 when it cannot build a chart. For trade-based PnL over time, use the PnL v2 history and performance routes.

Does live balance streaming need a higher plan?

Yes. Datastream rooms, including wallet balance, are available on Premium, Business and Enterprise plans. The REST wallet endpoints work with a Data API key.

How do I show who a wallet belongs to?

Holdings do not include identity. Use identity enrichment on trade history and holders, covered in the wallet identity guide.

References

  • Get wallet tokens
  • Get basic wallet information
  • Get wallet tokens with pagination
  • Get wallet portfolio chart
  • Datastream wallet balance room
  • Wallet tracking guide
  • @solana-tracker/data-api on npm

Companion project

The solana-wallet-portfolio-api example prints a formatted holdings table and, with ST_DATASTREAM_KEY set, follows balance changes live with reconnect-aware snapshot refreshes.

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 Wallet Identity API: Label Traders and Holders
Data API

Solana Wallet Identity API: Label Traders and Holders

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