How trader and wallet PnL is computed on Robinhood Chain
How the Crius API computes realized and unrealized PnL, ROI, cost basis and win rate for Robinhood Chain traders and wallets, worked through on real trades.
Crius computes PnL with the average-cost method on the trade tape: every buy and sell is valued in USD at the moment it happened, the tokens sold are charged at the average price paid for all tokens bought, and what is still held is valued at the current price. Realized PnL is what the sells made over that cost; unrealized PnL is the current value of the balance over its cost. Top traders of a token, a wallet’s profile, holder snapshots and holder cohorts all use the same rules.
A trader’s PnL, worked through
CHARGE's top three traders by PnL
Who made the most on Charge AI (0x7b8892cea949bc60d494681d1bc3ea96071ab0b8), and how is their PnL calculated?
curl "https://api.crius.sh/v1/robinhood/token/0x7b8892cea949bc60d494681d1bc3ea96071ab0b8/traders?limit=3" \ -H "x-api-key: $CRIUS_API_KEY"const res = await fetch("https://api.crius.sh/v1/robinhood/token/0x7b8892cea949bc60d494681d1bc3ea96071ab0b8/traders?limit=3", { headers: { "x-api-key": process.env.CRIUS_API_KEY },});const { data, meta } = await res.json();import os, requests
r = requests.get( "https://api.crius.sh/v1/robinhood/token/0x7b8892cea949bc60d494681d1bc3ea96071ab0b8/traders?limit=3", headers={"x-api-key": os.environ["CRIUS_API_KEY"]},)body = r.json()print(body["data"])Real response 200 Captured Oct 28, 2026 · 91 lines (arrays trimmed)
{ "data": [ { "address": "0x9e38fedc40987c19396cc20ea59eb2b1b7928881", "avg_buy_usd": 0.00000842823, "avg_sell_usd": 0.0000532304, "balance": 0, "bought": 29080767.335830362668108408, "bought_eth": 0.099, "bought_usd": 245.099, "buys": 1, "cost_basis": "complete", "first_trade_ts": 1791983208000, "gas_spent_usd": 0.0875722, "holding_time_s": 118676, "last_trade_ts": 1792101884000, "pnl_usd": 1302.88, "rank": 1, "realized_pnl_usd": 1302.88, "roi_pct": 531.573, "sells": 7, "sold": 29080767.335830362668108408, "sold_eth": 0.635695, "sold_usd": 1547.98, "tags": [], "unrealized_pnl_usd": 0 }, { "address": "0x7c79ad74d36dea424d08a5d326271bf03da81f6a", "avg_buy_usd": 0.00000790248, "avg_sell_usd": 0.0000343631, "balance": 0, "bought": 46475393.982901663958413733, "bought_eth": 0.1485, "bought_usd": 367.271, "buys": 2, "cost_basis": "complete", "first_trade_ts": 1791983183000, "gas_spent_usd": 0.0712204, "holding_time_s": 125872, "last_trade_ts": 1792109055000, "pnl_usd": 994.45, "rank": 2, "realized_pnl_usd": 994.45, "roi_pct": 270.767, "sells": 4, "sold": 37582304.39247174197236032, "sold_eth": 0.528147, "sold_usd": 1291.44, "tags": [], "unrealized_pnl_usd": 0 }, { "address": "0x83055f3e7be4e289733bbc8c7bd42a6443177f07", "avg_buy_usd": 0.000023621, "avg_sell_usd": 0.0000471805, "balance": 0, "bought": 35569497.20243776424709037, "bought_eth": 0.3465, "bought_usd": 840.189, "buys": 3, "cost_basis": "complete", "first_trade_ts": 1791988168000, "gas_spent_usd": 0.0631376, "holding_time_s": 4039, "last_trade_ts": 1791992207000, "pnl_usd": 838, "rank": 3, "realized_pnl_usd": 838, "roi_pct": 99.7395, "sells": 3, "sold": 35569497.20243776424709037, "sold_eth": 0.691944, "sold_usd": 1678.19, "tags": [], "unrealized_pnl_usd": 0 } ], "meta": { "block": 100406823, "block_age_ms": 829, "block_ts": 1793225080000, "chain": "robinhood", "finalized_block": 100397649, "lag_ms": 64, "next_cursor": "3", "pnl_method": "average cost; USD at trade time; unrealized at current price", "safe_block": 100401329, "window": "all" }}What this tells you
- Rank 1,
0x9e38fedc40987c19396cc20ea59eb2b1b7928881: one buy of 29,080,767 CHARGE forbought_usd: 245.099, then seven sells of all of it forsold_usd: 1547.98. Realized PnL is 1,547.98 − 245.099 =realized_pnl_usd: 1302.88, androi_pct: 531.573is 1,302.88 ÷ 245.099. - Rank 2,
0x7c79ad74d36dea424d08a5d326271bf03da81f6a: bought 46,475,393 tokens for $367.271 (avg_buy_usd: 0.00000790248) and sold 37,582,304 of them for $1,291.44. The sold tokens cost 37,582,304 × 0.00000790248 ≈ $296.99, sorealized_pnl_usdis 994.45. - Rank 2 also has
balance: 0although it sold only part of what it bought. The other 8.9 million tokens left the wallet without a sale (a transfer), so they count neither as a sale nor as a loss. gas_spent_usd(0.0875722 for rank 1) is reported separately and not subtracted frompnl_usd.cost_basis: "complete"on all three: every token they sold was bought, so the numbers are exact.
The formulas
For one wallet and one token over the window (bought and sold in tokens, bought_usd and sold_usd at trade time):
| Field | How it is computed |
|---|---|
avg_buy_usd | bought_usd ÷ bought: the average cost of one token |
realized_pnl_usd | sold_usd − min(sold, bought) × avg_buy_usd |
unrealized_pnl_usd | Current value of balance − min(balance, bought − sold) × avg_buy_usd |
pnl_usd | realized_pnl_usd + unrealized_pnl_usd |
roi_pct | pnl_usd ÷ bought_usd × 100 |
holding_time_s | From the first trade to now while the wallet still holds the token, otherwise from the first trade to the last |
- USD at trade time. Each trade’s
value_usdis the quote amount times the quote’s USD price at that block. ETH-quoted trades use that block’s ETH/USD (eth_usd_at_tradeon the trade tape). Later price moves never change a past trade’s value. - Current value. The balance is valued at the token’s current price, capped at what the price pool’s quote side could pay out. Ten million tokens in a pool holding $50 of ETH are never worth more than $50.
- Windows.
/traderstakeswindow=1h,24h,7dorall(the default); the wallet profile takes1d,7d,30d(the default) orall. Only trades inside the window count.
When PnL is unknown
When a wallet sold more than it bought, the extra tokens came from somewhere else (a transfer, an airdrop, a launch allocation) and their cost is unknown. Crius doesn’t guess: cost_basis says "unknown (sold tokens received by transfer)", realized_pnl_usd, unrealized_pnl_usd, pnl_usd and roi_pct are null, and the row sorts last when you rank by PnL.
Tokens received by transfer and still held are different: they are part of the balance, valued at the current price, with no cost against them, so they show up as unrealized gain. A dev wallet holding its launch allocation is the common case. Use the dev tag and exclude=dev when you rank traders (see holder classes).
Wallet PnL
The wallet profile applies the same per-token formulas to every token the wallet traded in the window and adds them up. Tokens with an unknown cost are left out of the totals.
pnl field | Meaning |
|---|---|
realized_usd, unrealized_usd, total_usd | Sums over the tokens with a known cost |
win_rate_pct | Share of those tokens with a positive total PnL (per token, not per trade) |
tokens_traded, tokens_with_known_cost | How many tokens the wallet traded, and how many the totals cover |
best_trade, worst_trade | The token with the highest and lowest total PnL |
gas_spent_usd, gas_scope | Gas for all transactions the wallet sent in the window, trades or not |
avg_hold_time_s | Average time from first to last trade per token |
A creator wallet with no trades in 30 days
What did CHARGE's creator wallet (0x3ac82d45ff9e4963cefbc3f74dbe293f34c927ca) make or lose in the last 30 days?
curl "https://api.crius.sh/v1/robinhood/wallet/0x3ac82d45ff9e4963cefbc3f74dbe293f34c927ca" \ -H "x-api-key: $CRIUS_API_KEY"const res = await fetch("https://api.crius.sh/v1/robinhood/wallet/0x3ac82d45ff9e4963cefbc3f74dbe293f34c927ca", { headers: { "x-api-key": process.env.CRIUS_API_KEY },});const { data, meta } = await res.json();import os, requests
r = requests.get( "https://api.crius.sh/v1/robinhood/wallet/0x3ac82d45ff9e4963cefbc3f74dbe293f34c927ca", headers={"x-api-key": os.environ["CRIUS_API_KEY"]},)body = r.json()print(body["data"])Real response 200 Captured Oct 28, 2026 · 84 lines (arrays trimmed)
{ "data": { "account_type": "smart_account", "address": "0x3ac82d45ff9e4963cefbc3f74dbe293f34c927ca", "delegate": "0x63c0c19a282a1b52b07dd5a65b58948a07dae32b", "holdings": [ { "balance": 25, "pnl": null, "price_confidence": "low", "token": { "address": "0xfaaf8ffa8a29ceb6c85aff8d3961f65ae1bfc775", "logo": null, "symbol": "USDG" }, "value_eth": 0.00000500064, "value_usd": 0.0117789 }, { "balance": 25, "pnl": null, "price_confidence": "low", "token": { "address": "0x06993ae1230d3755b54f25022028daa66772bc3d", "logo": null, "symbol": "USDG" }, "value_eth": 0.00000500061, "value_usd": 0.0117789 } ], "holdings_truncated": false, "holdings_value_usd": 0.0235578, "is_contract": false, "label": null, "native_balance": 0.71473787532832551, "native_value_usd": 1683.56, "pnl": { "avg_hold_time_s": null, "best_trade": null, "gas_scope": "all transactions sent by the wallet in the window", "gas_spent_usd": 0.198772, "realized_usd": 0, "tokens_traded": 0, "tokens_with_known_cost": 0, "total_usd": 0, "trades": 0, "unrealized_usd": 0, "volume_usd": 0, "win_rate_pct": null, "window": "30d", "worst_trade": null }, "profile": { "first_seen_block": 91351250, "first_seen_ts": 1792283372000, "fresh": false, "funded_by": { "address": "0xd782268f600403843484b70710daee6846fc0db8", "amount_eth": 0.008148386337868892, "block": 87601233, "label": "bridge:Relay solver", "ts": 1791903516300 }, "history": "complete", "tags": [ "dev", "smart_account" ], "tx_count": 14, "wallet_age_days": 10.8994 } }, "meta": { "balances_block": 100406864, "block": 100406864, "block_age_ms": 1301, "block_ts": 1793225084000, "chain": "robinhood", "finalized_block": 100397649, "lag_ms": 59, "safe_block": 100401329 }}What this tells you
pnl.window: "30d"is the default window.trades: 0andtokens_traded: 0: the wallet made no trades in it, sorealized_usd,unrealized_usdandtotal_usdare 0.win_rate_pct,best_trade,worst_tradeandavg_hold_time_sarenull: with no tokens traded there is nothing to rate.gas_spent_usd: 0.198772is still counted, becausegas_scopeis “all transactions sent by the wallet in the window”, not only trades.- Each holding has
pnl: nullhere: neither of the two tokens it holds was traded in the window.
Holder endpoints use the same cost basis. The holder snapshot adds cost_basis_usd and unrealized_pnl_usd to each holder at the snapshot’s price, and the holder cohort adds pnl_usd to each wallet that held through the window.
Use it in your app
-
Rank outside traders.
/traders?sort=pnl&window=7d&exclude=dev,snipergives the best traders of the week who neither created nor sniped the token. -
Show the basis. Next to a PnL number, show
bought_usd,sold_usdandcost_basis, so users can see what it rests on. -
Subtract gas if your users care.
pnl_usd − gas_spent_usdis the net result after fees paid to the chain.
// Net PnL of a token's top traders this week, outside traders only (Free plan key).const url = `https://api.crius.sh/v1/robinhood/token/${token}/traders?sort=pnl&window=7d&exclude=dev,sniper&limit=10`;const { data } = await (await fetch(url, { headers: { "x-api-key": process.env.CRIUS_API_KEY } })).json();
for (const t of data) { if (t.pnl_usd === null) continue; // cost unknown: tokens arrived by transfer const net = t.pnl_usd - (t.gas_spent_usd ?? 0); console.log(t.rank, t.address, `pnl $${t.pnl_usd}`, `net $${net.toFixed(2)}`, `roi ${t.roi_pct}%`);}Frequently asked questions
Does Crius PnL include gas?
No. PnL is computed from the USD value of the trades. Gas is reported next to it as gas_spent_usd so you can subtract it yourself: per token on /traders, and for every transaction the wallet sent in the window on the wallet profile.
Is PnL FIFO or average cost?
Average cost. Every token bought costs the average USD price of all the wallet's buys of that token, so the order of buys and sells doesn't change the result. Each response says so in meta.pnl_method on /traders.
Why is a trader's PnL null?
The wallet sold more tokens than it bought: the extra tokens arrived by transfer, airdrop or launch allocation, so their cost is unknown. Those rows have cost_basis: "unknown (sold tokens received by transfer)" and null PnL, and they sort last when you rank by PnL.
Is the wallet win rate per trade?
Per token. win_rate_pct is the share of tokens with a known cost whose total PnL (realized + unrealized) is positive in the window. Ten losing trades and one big winning trade on the same token count as one win if the token's total PnL is positive.
Which trades count toward a wallet's PnL?
The trades attributed to it as the trader: the address whose balance changed, not the router, solver or ERC-4337 bundler that sent the transaction. See trader attribution.
Related
Get your free Crius API key
Every example on this page works with a free key: higher rate limits than the anonymous plan, holders, wallets and the WebSocket stream. No card needed.
- Robinhood Chain (4663) only, from our own archive nodes
- REST + WebSocket
- Pay on-chain or by card