Build a Crypto Portfolio Dashboard Using the Crius API
Build a portfolio dashboard for Robinhood Chain wallets: holdings, live valuation in one batch call, realized and unrealized PnL, gas and confidence.
Crius 7 min read
Short answer: for each wallet, call GET /v1/robinhood/wallet/{address} to get its token holdings, ETH balance and PnL; merge positions across wallets, then value them all at the same block with one POST /v1/robinhood/prices call for up to 100 tokens. In our test this valued three wallets and 16 positions ($114,660 in total) in four requests.
This guide builds a multi-wallet portfolio view in Node.js and covers the details that make portfolio numbers trustworthy: consistent pricing, dust filtering, cost-basis coverage and price confidence. It ran against the live API in October 2026.
The wallet endpoint
curl "https://api.crius.sh/v1/robinhood/wallet/0x48e2e6dd7ad6d9ac1f090172662d5389b4fa8c1c?window=30d&min_value_usd=1" \ -H "x-api-key: $CRIUS_API_KEY"| Parameter | Values |
|---|---|
window |
1d, 7d, 30d (default) or all: the period for PnL statistics |
min_value_usd |
drop holdings worth less than this (airdropped dust, spam tokens) |
The response has four parts:
{ "address": "0xc56e22b48c421e08d26bad1bd0676832412a8b41", "native_balance": 1.72e-7, "native_value_usd": 0.0004, "holdings": [ { "token": { "address": "0x1319…7ccb", "symbol": "MEMECHAN" }, "balance": 1129.87, "value_usd": 0.0373, "value_eth": 1.58e-5, "price_confidence": "low", "pnl": null } ], "holdings_value_usd": 0.0532, "pnl": { "window": "30d", "trades": 42, "volume_usd": 40570.8, "realized_usd": 30511.4, "unrealized_usd": 0, "total_usd": 30511.4, "win_rate_pct": 100, "avg_hold_time_s": 496984, "gas_spent_usd": 0.343, "tokens_traded": 3, "tokens_with_known_cost": 2, "best_trade": { "symbol": "GIGATENEV", "pnl_usd": 30132.7 }, "worst_trade": { "symbol": "SI", "pnl_usd": 378.649 } }, "profile": { "wallet_age_days": 50.8, "tx_count": 1043, "fresh": false, "funded_by": { "address": "0x776c…5ade", "amount_eth": 0.0322 } }}That wallet is a real one: the top trader of GIGATENEV, which turned about $1,500 into $31,600 and then sold out. Its holdings are now worth five cents.
How PnL is calculated
Crius reconstructs every position from the wallet’s on-chain trades and transfers:
- Cost basis is the USD value of each buy at the block it happened. Average entry is the cost-weighted price of the tokens still held.
- Realized PnL comes from sells: proceeds minus the cost basis of the tokens sold.
- Unrealized PnL is the current value of what’s still held, minus its cost basis.
- Gas is the total fee for every transaction the wallet sent in the window, reported separately so you can choose whether to net it out.
A position’s cost is only known if the wallet acquired the tokens by trading. Tokens received by transfer (an airdrop, or a move from another of your wallets) have no purchase price, so their pnl is null, like the MEMECHAN dust above. When the cost is known, a holding’s pnl has realized_usd, unrealized_usd and avg_entry_usd. tokens_with_known_cost vs tokens_traded tells you how complete the PnL picture is.
Building the dashboard
const API = "https://api.crius.sh/v1/robinhood";const KEY = process.env.CRIUS_API_KEY;const WALLETS = [ "0x48e2e6dd7ad6d9ac1f090172662d5389b4fa8c1c", "0x2c10013678aafca9d90e914ba02f087bf449446e", "0xc56e22b48c421e08d26bad1bd0676832412a8b41",];
const req = async (path, body) => { const r = await fetch(`${API}${path}`, { method: body ? "POST" : "GET", headers: { "x-api-key": KEY, ...(body && { "content-type": "application/json" }) }, body: body && JSON.stringify(body), }); if (!r.ok) throw new Error(`${path}: ${r.status} ${await r.text()}`); return r.json();};
// 1. Holdings and 30-day PnL for every wallet, positions under $1 dropped server-side.const wallets = await Promise.all(WALLETS.map((w) => req(`/wallet/${w}?window=30d&min_value_usd=1`)));
// 2. Merge positions by token.const positions = new Map();let native = 0;for (const { data: w } of wallets) { native += w.native_value_usd ?? 0; for (const h of w.holdings) { const p = positions.get(h.token.address) ?? { symbol: h.token.symbol, balance: 0, realized: 0, unrealized: 0, costKnown: true }; p.balance += h.balance; if (h.pnl) { p.realized += h.pnl.realized_usd; p.unrealized += h.pnl.unrealized_usd; } else p.costKnown = false; positions.set(h.token.address, p); }}
// 3. One batch call prices every position at the same block.const tokens = [...positions.keys()];const prices = await req("/prices", { tokens: tokens.slice(0, 100) });
let total = native;const table = tokens.map((t) => { const p = positions.get(t), q = prices.data[t]; const value = q ? p.balance * q.usd : 0; total += value; return { symbol: p.symbol, balance: +p.balance.toPrecision(6), price: q?.usd ?? null, value_usd: Math.round(value * 100) / 100, chg_24h: q?.change_24h_pct?.toFixed(1) ?? "", confidence: q?.confidence ?? "none", pnl_usd: p.costKnown ? Math.round(p.realized + p.unrealized) : "n/a", };}).sort((a, b) => b.value_usd - a.value_usd);
const pnl = wallets.reduce((s, { data: w }) => ({ realized: s.realized + (w.pnl?.realized_usd ?? 0), unrealized: s.unrealized + (w.pnl?.unrealized_usd ?? 0), gas: s.gas + (w.pnl?.gas_spent_usd ?? 0),}), { realized: 0, unrealized: 0, gas: 0 });
console.log(`${WALLETS.length} wallets · ${tokens.length} positions · value $${total.toFixed(2)} · priced at block ${prices.meta.block}`);console.log(`30d PnL: realized $${pnl.realized.toFixed(0)}, unrealized $${pnl.unrealized.toFixed(0)}, gas $${pnl.gas.toFixed(2)}`);console.table(table.slice(0, 8));Output from our run:
3 wallets · 16 positions · value $114659.58 (incl. $87.70 ETH) · priced at block 9671565730d PnL: realized $12485, unrealized $19287, gas $4.48symbol balance price value_usd chg_24h confidence pnl_usdCREORA 18780700 0.00135048 25362.95 23.3 high 6087GIGATENEV 21548700 0.00115246 24833.98 5.4 high 5174HYDX 11949700 0.00186791 22320.91 -14.9 high 3760BORRO 34066500 0.000566472 19297.72 7.1 high 4872FTR 4444 2.82991 12576.12 low n/aHEDGE 5290540 0.000936389 4954 -5.6 high 7525LOUD 22592200 0.000156109 3526.85 2281.4 high 232Parabol 56010700 0.0000224101 1255.21 -4.2 high -2950Why price everything in one call
The wallet endpoint already returns value_usd per holding, so why call /prices at all? Two reasons:
- Consistency. Three wallet calls run at slightly different moments. One
/pricescall values every position at the same block, so the total is a true snapshot and the response’smeta.blocktells you which. - Refreshing. Holdings change only when the wallet trades; prices change every block. Fetch holdings every few minutes, or update them from the
wallet:<address>stream, and refresh prices every few seconds with a single cheap request.
Read the confidence field
Look at FTR in the table above: $12,576, the fifth-largest position, priced with low confidence and no 24-hour change. Confidence reflects how much real liquidity stands behind the price:
| Confidence | Meaning | What to do |
|---|---|---|
high |
Deep, actively traded pool | Use as is |
medium |
Usable but thinner liquidity | Show, maybe with a marker |
low |
Thin or stale pool; the last trade may not reflect what you could sell for | Show separately, exclude from the headline total, or value at zero |
A portfolio tool that adds low-confidence positions into its headline number overstates wealth; that FTR position could not be sold for anything near $12,500. A good default is two totals: “verified value” from high and medium confidence, and “including unverified” with everything.
Also watch for symbol collisions. One of the wallets in our test held a token with the symbol USDG that is not the real USDG stablecoin; always key positions by contract address, never by symbol.
Live updates and alerts
Polling every wallet every few seconds is wasteful when holdings change only when the wallet acts. Subscribe to wallet:<address> on the WebSocket API for each wallet in the portfolio. Every trade and token transfer arrives as it happens, so you can update the position in memory and refetch the wallet’s PnL only when something actually changed.
The same stream drives alerts:
- Large moves out. A transfer of more than a set share of a position to an address that isn’t one of yours.
- New tokens in. Unsolicited tokens are often spam or phishing bait; flag them instead of adding them to the total.
- Price thresholds. Combine
price:<token>subscriptions for your largest positions with a simple threshold check.
ws.send(JSON.stringify({ op: "subscribe", channels: WALLETS.map((w) => `wallet:${w}`) }));ws.onmessage = (ev) => { const m = JSON.parse(ev.data); if (m.ch.startsWith("wallet:") && !m.data.removed) refreshWallet(m.ch.slice(7)); // debounce in practice};Add a wallet’s history
For a performance chart, the wallet endpoint’s pnl gives you totals for 1d, 7d, 30d or all. For a trade list, use GET /wallet/{address}/trades, which returns each trade with its USD value at the time, paginated:
curl "https://api.crius.sh/v1/robinhood/wallet/0x48e2e6dd7ad6d9ac1f090172662d5389b4fa8c1c/trades?limit=20" \ -H "x-api-key: $CRIUS_API_KEY"Wallet analytics are part of the Starter plan and above; the free plan covers prices, trades and token data.
Frequently asked questions
Does this work for any wallet? Yes, any address on Robinhood Chain. No permission is needed because all of this is public on-chain data.
How do I include tokens received by transfer in PnL? They have no on-chain purchase price. Let users enter a cost basis manually, or value them at their price on the day they arrived using daily candles.
How often should I refresh?
Prices: every few seconds with /prices, or live with price:<token> streams. Holdings: every few minutes, or instantly with the wallet:<address> stream.
What does it cost?
Each wallet call is 5 credits and a /prices call is 1 credit. A three-wallet portfolio refreshed every minute uses about 23,000 credits a day.
Can I track tokenized stocks too? Yes. Tokenized stocks like TSLA and SPY are ordinary tokens on Robinhood Chain and appear in holdings with their prices.
Next steps
- API reference: wallets and devs and tokens and prices.
- Product overview: wallet analytics, token prices, real-time Robinhood Chain API.
- Related guides: real-time token prices and holder analysis with bubble maps.
Questions go to support@crius.sh.
Build with Crius
Robinhood Chain data in one API
Free plan with 1M credits a month. Your key in under a minute.
Related APIs
More from the blog
- PnLWallet analytics
How to Track Wallet PnL on Robinhood Chain
Realized and unrealized PnL, win rate and per-token results for any Robinhood Chain wallet: the average-cost method, the API calls and code to recompute it.
- Top tradersPnL
Track Robinhood Chain Smart Money Wallets and Top Traders
Find wallets that are consistently profitable across Robinhood Chain tokens: top traders by PnL, wallet tags, and a script that filters out bots and infrastructure.
- TutorialHistorical data
How to Get All Robinhood Chain Transactions Without Running a Node
Export every trade, transfer and liquidity event for a Robinhood Chain token or wallet with an indexed API. Tested Python and Node.js, no archive node.