Every exchange, one number: build a cross-exchange portfolio tracker

Your funds live on three exchanges and none of their dashboards agree. Pull balances from all of them with one API, price everything in USDT, and print the number that matters.

By CCXT Team

If you trade on more than one exchange, you know the ritual: log in here, log in there, add numbers in your head, forget the dust on that third account. The whole point of a unified API is that "how much do I actually have?" should be one script — read-only keys, no order permissions, nothing to break.

The plan: fetch balances from every exchange, merge per currency, price everything in USDT off one venue's tickers, print a table and a total.

import ccxt from 'ccxt';

const ids = ['binance', 'kraken', 'bybit'];
const totals = {};

for (const id of ids) {
    const exchange = new ccxt[id]({
        apiKey: process.env[`${id.toUpperCase()}_APIKEY`],
        secret: process.env[`${id.toUpperCase()}_SECRET`],
    });
    const balance = await exchange.fetchBalance();
    for (const [currency, amount] of Object.entries(balance.total)) {
        if (amount > 0) {
            totals[currency] = (totals[currency] ?? 0) + amount;
        }
    }
    await exchange.close();
}

// price everything in USDT off one venue's tickers
const pricing = new ccxt.binance();
const tickers = await pricing.fetchTickers();
let portfolio = 0;
for (const [currency, amount] of Object.entries(totals)) {
    const price = (currency === 'USDT') ? 1 : tickers[`${currency}/USDT`]?.last;
    if (!price) continue;                    // no USDT market — see notes below
    const value = amount * price;
    portfolio += value;
    console.log(`${currency.padEnd(8)} ${amount.toFixed(6).padStart(14)} ≈ ${value.toFixed(2).padStart(12)} USDT`);
}
console.log(`${'TOTAL'.padEnd(8)} ${' '.repeat(14)} ≈ ${portfolio.toFixed(2).padStart(12)} USDT`);
await pricing.close();

Run it with read-only keys (see the API-key checklist — a tracker never needs trade permission) and you get something like:

BTC          0.084310 ≈      9021.17 USDT
ETH          1.250000 ≈      4187.50 USDT
SOL         12.400000 ≈      2145.20 USDT
USDT      1830.550000 ≈      1830.55 USDT
TOTAL                 ≈     17184.42 USDT

Honest edges of a 40-line tracker

  • balance.total is spot-wallet truth, not net worth. Derivatives margin, staked/earn balances, and funds in open orders are reported differently per venue (fetchBalance takes params for other account types — e.g. a type of 'swap' or 'funding' on exchanges that split wallets). Loop account types if you use them.
  • The "no USDT market" skip. Small assets may only trade against BTC or not at all on your pricing venue. The robust version falls back to X/BTC × BTC/USDT, or prices each balance on the exchange it lives on.
  • Stablecoins at 1.0 is a simplification we made only for USDT itself. During a depeg, your tracker will happily lie to you — price USDC and friends through their real markets.
  • Cache the tickers. One fetchTickers() per run is fine; per-currency fetchTicker calls are how a tracker becomes a rate-limit problem.

From here it's a small step to a cron job that appends a CSV row per hour — and suddenly you have the portfolio history chart no exchange will ever give you across venues. If you want it live instead, watchBalance from the WebSocket post pushes every change as it happens.