CRIUS
PnLWallet analyticsTutorialPython

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.

Crius 8 min read

To track a wallet’s profit and loss on Robinhood Chain, call GET /v1/robinhood/wallet/{address} for a realized, unrealized and win-rate summary over 1, 7 or 30 days or all time, and /wallet/{address}/trades for every trade behind it. Crius computes PnL with the average-cost method in USD at the time of each trade, and this guide shows exactly how, so you can reproduce, audit or extend the numbers yourself.

Wallet PnL sounds simple: what you sold for minus what you paid. In practice it raises questions every PnL tool has to answer somehow. Which buy does a sell close? What price are unsold tokens worth? What about tokens that arrived by transfer, not by a trade? Here are the answers Crius uses, with code that recomputes them from raw trades.

The method: average cost, USD at trade time

For each token a wallet traded, Crius aggregates its buys and sells from the indexed trade tape (every swap on Uniswap v2, v3 and v4 and every launchpad bonding curve), attributed to the real trader rather than the router contract:

average entry = USD spent on buys / tokens bought
realized PnL = USD received from sells − tokens sold × average entry
unrealized PnL = tokens still held × current price − cost of those tokens
total PnL = realized + unrealized

Three rules make this robust:

  1. USD at trade time. Every trade is valued in USD at the moment it happened, using the quote it traded against (ETH, WETH, USDG or another token) and that block’s ETH price. A token bought for ETH when ETH was $2,400 keeps that cost even if ETH moves.
  2. Unrealized uses a liquidity-aware price. Open positions are valued at the token’s current price. For positions too large for the pool to absorb, the value is capped at what the market could actually pay, so a wallet holding 40% of a thin token isn’t shown as a millionaire.
  3. Unknown cost is reported, not guessed. If a wallet sold more tokens than it ever bought, the extra arrived by transfer (an airdrop, a gift, a wallet move) and has no known cost. Crius marks the position cost_basis: "unknown (sold tokens received by transfer)" and leaves it out of PnL rather than inventing a basis.

Average cost isn’t the only valid method. FIFO matches each sell to the oldest open buy and gives different realized numbers for positions built in steps. It is the most common one for trading analytics because it’s stable, order-independent and easy to explain. Every response states it in meta.pnl_method.

The wallet summary

Terminal window
curl "https://api.crius.sh/v1/robinhood/wallet/0xc56e22b48c421e08d26bad1bd0676832412a8b41?window=all&min_value_usd=1" \
-H "x-api-key: $CRIUS_API_KEY"
{
"profile": {
"first_seen_block": 53558073, "wallet_age_days": 50.77, "tx_count": 1043,
"fresh": false, "tags": [],
"funded_by": { "address": "0x776c…5ade", "amount_eth": 0.0322, "block": 53558073 },
"history": "complete"
},
"pnl": {
"window": "all",
"realized_usd": 35104.7, "unrealized_usd": 0, "total_usd": 35104.7,
"win_rate_pct": 75, "tokens_traded": 8, "tokens_with_known_cost": 8,
"trades": 944, "volume_usd": 80674.3, "gas_spent_usd": 32.55,
"best_trade": { "symbol": "GIGATENEV", "pnl_usd": 30132.7 },
"worst_trade": { "symbol": "CASHKITTY", "pnl_usd": -75.73 },
"avg_hold_time_s": 371340
},
"holdings": []
}

This is a real wallet that ranked first by PnL among one memecoin’s traders at time of writing (Oct 2026). A few fields are worth reading carefully:

  • window is 1d, 7d, 30d (default) or all. It limits which trades count, so a 7-day PnL is the result of trades made in the last 7 days, valued as described above.
  • win_rate_pct is the share of tokens with a positive total PnL, counted only over tokens_with_known_cost. Six of eight here.
  • gas_spent_usd covers every transaction the wallet sent in the window, not just swaps. Subtract it for a net figure; on Robinhood Chain it’s usually small, $32.55 across 944 trades for this wallet.
  • funded_by is the first ETH the wallet ever received, including internal transfers from contracts, which matters when you’re asking who is behind a wallet.

The wallet profile and wallet trades endpoints need a Starter key or higher.

Recomputing PnL from raw trades

The summary is convenient; the trade list is the ground truth. /wallet/{address}/trades returns every trade the wallet made across all tokens, newest first, each with its USD value at the time, gas, token tax and finality, and paginates with meta.next_cursor. This Node.js script fetches the summary, then rebuilds one token’s PnL from scratch:

// pnl.mjs: wallet PnL from the API summary and recomputed from raw trades. Node 18+.
const API = "https://api.crius.sh/v1/robinhood";
const KEY = process.env.CRIUS_API_KEY;
const WALLET = process.argv[2];
const TOKEN = process.argv[3];
const get = async (p) => {
const r = await fetch(`${API}${p}`, { headers: { "x-api-key": KEY } });
const b = await r.json();
if (!r.ok) throw new Error(`${r.status} ${b.error?.code}`);
return b;
};
// 1. Summary over a window (1d, 7d, 30d or all).
const { data: w } = await get(`/wallet/${WALLET}?window=all&min_value_usd=1`);
const p = w.pnl;
console.log(`realized $${p.realized_usd}, unrealized $${p.unrealized_usd}, win rate ${p.win_rate_pct}%`);
console.log(`${p.tokens_traded} tokens, ${p.trades} trades, gas $${p.gas_spent_usd}`);
// 2. Every trade of one token (newest first, paginated with meta.next_cursor).
const trades = [];
let cursor = "";
do {
const q = new URLSearchParams({ token: TOKEN, limit: "200" });
if (cursor) q.set("cursor", cursor);
const { data, meta } = await get(`/wallet/${WALLET}/trades?${q}`);
trades.push(...data);
cursor = meta.next_cursor ?? "";
} while (cursor);
// 3. Average cost: realized = sold USD - tokens sold x average buy price.
let bought = 0, boughtUsd = 0, sold = 0, soldUsd = 0;
for (const t of trades) {
if (t.type === "buy") { bought += t.amount_base; boughtUsd += t.value_usd; }
else { sold += t.amount_base; soldUsd += t.value_usd; }
}
const avg = boughtUsd / bought;
const realized = soldUsd - Math.min(sold, bought) * avg;
console.log(`${trades.length} trades: bought ${bought.toFixed(0)} for $${boughtUsd.toFixed(2)}, sold for $${soldUsd.toFixed(2)}`);
console.log(`average entry $${avg.toPrecision(4)}, realized PnL $${realized.toFixed(2)}`);
if (sold > bought * 1.000001) console.log("sold more than bought: some tokens arrived by transfer, cost unknown");
$ node pnl.mjs 0xc56e22b48c421e08d26bad1bd0676832412a8b41 0xc9675fe679c7d4e1b94416f66d61c96127eb964a
realized $35104.7, unrealized $0, win rate 75%
8 tokens, 944 trades, gas $32.5605
35 trades: bought 10997432 for $1514.14, sold for $31646.89
average entry $0.0001377, realized PnL $30132.75

The recomputed $30,132.75 matches the API’s best_trade to the cent. One buy of about 11 million tokens for $1,514, unwound over 34 sells for $31,647.

Per-token PnL for a whole wallet

The same idea across every token, in Python. Note that it groups by token address, not symbol:

"""pnl.py: realized PnL per token for a wallet, from its trade history (average-cost method)."""
import os, sys, collections, requests
API = "https://api.crius.sh/v1/robinhood"
WALLET = sys.argv[1]
s = requests.Session()
s.headers["x-api-key"] = os.environ["CRIUS_API_KEY"]
def trades(wallet, since_ms=None):
cursor = None
while True:
params = {"limit": 200}
if cursor: params["cursor"] = cursor
if since_ms: params["from_ts"] = since_ms
body = s.get(f"{API}/wallet/{wallet}/trades", params=params, timeout=60).json()
yield from body["data"]
cursor = body["meta"].get("next_cursor")
if not cursor:
return
pos = collections.defaultdict(lambda: {"sym": "", "b": 0.0, "bu": 0.0, "s": 0.0, "su": 0.0})
for t in trades(WALLET):
p = pos[t["token"]["address"]]
p["sym"] = t["token"]["symbol"]
if t["type"] == "buy":
p["b"] += t["amount_base"]; p["bu"] += t["value_usd"]
else:
p["s"] += t["amount_base"]; p["su"] += t["value_usd"]
rows = []
for token, p in pos.items():
if p["b"] == 0 or p["s"] > p["b"] * 1.000001:
continue # tokens received by transfer: cost unknown
avg = p["bu"] / p["b"]
rows.append((p["su"] - p["s"] * avg, p["sym"], p["b"], p["s"], avg))
for realized, sym, b, sold, avg in sorted(rows, reverse=True):
print(f"{sym:>12} realized ${realized:>10,.2f} avg entry ${avg:.3g} sold {sold / b:.0%} of position")
GIGATENEV realized $ 30,132.75 avg entry $0.000138 sold 100% of position
ROO realized $ 4,608.29 avg entry $2.62e-06 sold 100% of position
SI realized $ 378.65 avg entry $9.92e-05 sold 100% of position
PONIE realized $ 32.76 avg entry $0.000157 sold 100% of position
ROO realized $ 29.08 avg entry $2.53e-06 sold 100% of position
DPONS realized $ 9.99 avg entry $1.44e-05 sold 100% of position
ROO realized $ -11.11 avg entry $3.52e-06 sold 100% of position
CASHKITTY realized $ -75.73 avg entry $9.38e-05 sold 100% of position

The rows add up to the summary’s $35,104.70. The output also shows why addresses matter: this wallet traded three different tokens called ROO. Copycat tickers are everywhere on launchpad chains, and a PnL tool that groups by symbol will merge them.

PnL from the other side: a token’s traders

To rank wallets on one token, not inspect one wallet, use /token/{address}/traders. It applies the same method to every trader of the token and sorts by pnl, realized_pnl, volume, buys or roi, over 1h, 24h, 7d or all:

Terminal window
curl "https://api.crius.sh/v1/robinhood/token/0xc9675fe679c7d4e1b94416f66d61c96127eb964a/traders?sort=realized_pnl&window=7d&limit=10" \
-H "x-api-key: $CRIUS_API_KEY"

Each row has avg_buy_usd, avg_sell_usd, roi_pct, holding_time_s and wallet tags. The smart money guide builds a cross-token leaderboard from it.

Practical notes

  • Windows cut trades, not positions. A 7-day PnL ignores buys made before the window. For lifetime results per position, use window=all.
  • Bots inflate trade counts. Wallets with thousands of trades a month are usually arbitrage or market-making contracts; check is_contract and the trade count before treating a wallet as a human trader.
  • Multi-hop swaps are attributed to the wallet that actually received or paid the token, not the router. via on each trade lists the routers involved.
  • Taxed tokens. Each trade reports fees.token_tax.pct. PnL uses the amounts the wallet actually received and paid, so transfer taxes are already reflected.

Frequently asked questions

Does Crius use FIFO or average cost for PnL?

Average cost, with every trade valued in USD at the time it happened. The method is stated in meta.pnl_method on trader responses. You can compute FIFO yourself from /wallet/{address}/trades, which returns every trade with its USD value.

Why is PnL missing for some tokens in a wallet?

The wallet sold more of the token than it bought, so part of its balance arrived by transfer and has no known cost. Those positions are marked cost_basis: unknown and excluded rather than guessed.

Is gas included in PnL?

PnL is gross. gas_spent_usd reports the gas of every transaction the wallet sent in the window, so subtract it for a net figure.

Can I get PnL for a past period?

Yes: window=1d, 7d, 30d or all on the wallet profile, or filter /wallet/{address}/trades with from_ts and to_ts and aggregate yourself.

How fresh is the data?

Trades appear within about a second of their block. Each trade carries finality (latest, safe or finalized) if you need settled numbers only.

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