Crypto Portfolio Tracker Development
A simple portfolio tracker — this is a data aggregator. A complex one — this is a real-time system that aggregates on-chain balances from dozens of networks, positions on centralized exchanges, NFTs, DeFi positions, and calculates it all in a single base currency. Complexity difference is roughly 10x.
Data Sources
On-Chain Balances
EVM networks: native balance via eth_getBalance, ERC-20 balances more complex — one RPC call returns balance of one token on one address. With 50 tokens × 5 networks = 250 calls. Solution — Multicall3 (deployed on all EVM networks at address 0xcA11bde05977b3631167028862bE2a173976CA11):
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({ chain: mainnet, transport: http() });
const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
const ERC20_ABI = parseAbi(["function balanceOf(address) view returns (uint256)"]);
async function getTokenBalances(
walletAddress: `0x${string}`,
tokenAddresses: `0x${string}`[]
) {
const calls = tokenAddresses.map((tokenAddress) => ({
address: tokenAddress,
abi: ERC20_ABI,
functionName: "balanceOf" as const,
args: [walletAddress],
}));
return client.multicall({ contracts: calls });
}
One RPC call instead of 50 — critical for rate limits and latency.
Alternative: Alchemy/Moralis Token API. One request returns all ERC-20 balances for address with metadata (symbol, decimals, USD value). Paid, but saves development time.
DeFi Positions
This is the most complex part of tracker. Liquidity position in Uniswap V3, collateral in Aave, staked tokens in Curve — each protocol stores data differently.
Approach 1: direct calls to protocol contracts. For Uniswap V3:
// Position by NFT ID
const position = await nftPositionManager.positions(tokenId);
// { token0, token1, fee, tickLower, tickUpper, liquidity, ... }
// Recalculate to tokens via tick math (complex)
// Better via Uniswap V3 SDK
import { Pool, Position } from "@uniswap/v3-sdk";
Approach 2: The Graph subgraphs. Most DeFi protocols have official subgraphs — GraphQL API over on-chain data:
query GetAavePositions($address: String!) {
user(id: $address) {
reserves {
reserve {
symbol
underlyingAsset
}
currentATokenBalance
currentVariableDebt
currentStableDebt
}
}
}
Approach 3: position aggregators — DeBank API, Zapper API, Zerion API. Already integrated with hundreds of protocols. Make sense if you don't want to maintain each DeFi integration yourself.
CEX Balances
Exchange APIs return balances instantly, but require read-only API key from user:
import ccxt from "ccxt";
async function getBinanceBalances(apiKey: string, secret: string) {
const exchange = new ccxt.binance({ apiKey, secret, sandbox: false });
const balance = await exchange.fetchBalance();
return balance.total; // { BTC: 0.5, ETH: 2.3, USDT: 1000 }
}
CCXT supports 100+ exchanges with unified interface — indispensable library for trackers.
Price Data
For USD conversion current prices are needed. Sources:
CoinGecko API — free plan 30 requests/min, 10k/month. Sufficient for small trackers. Pro plan removes limits.
async function getPrices(coinIds: string[]): Promise<Record<string, number>> {
const ids = coinIds.join(",");
const data = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`
).then(r => r.json());
return Object.fromEntries(
Object.entries(data).map(([id, val]: [string, any]) => [id, val.usd])
);
}
On-chain prices (Chainlink): for real-time without CoinGecko dependency. Chainlink Price Feeds available on all major EVM networks.
Refresh Pipeline Architecture
Data needs updating, but not RPC hammering every second. Strategy:
| Data Type | Update Frequency | Reason |
|---|---|---|
| On-chain balances | Every 30–60 sec | New block |
| CEX balances | Every 30 sec | API limits |
| DeFi positions | Every 2–5 min | Change slowly |
| Token prices | Every 10–30 sec | Critical for P&L |
| Historical P&L | Background job, 1/hour | Heavy computation |
Background jobs via Redis + BullMQ with separate queues for different data types. Cache results in Redis, frontend reads from cache.
WebSocket for Real-Time
For users who opened tracker in browser — Server-Sent Events or WebSocket with push updates instead of polling:
// Server-Sent Events endpoint
app.get("/api/portfolio/stream", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
const interval = setInterval(async () => {
const portfolio = await getPortfolioSnapshot(req.user.id);
res.write(`data: ${JSON.stringify(portfolio)}\n\n`);
}, 10000);
req.on("close", () => clearInterval(interval));
});
Storing Historical Data
For P&L tracking snapshots of portfolio over time are needed. TimescaleDB (PostgreSQL extension) is ideal:
CREATE TABLE portfolio_snapshots (
user_id UUID,
snapshot_at TIMESTAMPTZ NOT NULL,
total_usd NUMERIC(20, 2),
breakdown JSONB -- { "ETH": { "amount": 2.3, "usd": 7820 }, ... }
);
SELECT create_hypertable('portfolio_snapshots', 'snapshot_at');
Query P&L for period:
SELECT
time_bucket('1 day', snapshot_at) AS day,
last(total_usd, snapshot_at) AS end_of_day_value
FROM portfolio_snapshots
WHERE user_id = $1 AND snapshot_at > NOW() - INTERVAL '30 days'
GROUP BY day ORDER BY day;
MVP development timeline with support for 5–6 EVM networks, main DeFi protocols, and 3–4 exchanges: 1–2 weeks.







