A leaderboard ranks traders against each other. The approach here is watchlist-based:
you define the set of addresses to track (traders you’ve discovered from the fill tape,
a curated list, addresses your users follow) and rank them with a single batched read
per refresh, plus live volume from the stream you already have open.
This scales cleanly and costs one request per cycle rather than one per trader.
1. Batch the portfolio reads
batchPortfolioStates takes a users array and returns one result slot per address, in
request order.
marginSummary.accountValue is perps margin only: it excludes vault equity, same
as on the wallet tracker.
On a leaderboard this is worse than on a single wallet page: a vault-heavy trader
doesn’t just display low, they can be ranked out of the board entirely, and the
mis-ranking is invisible because there’s nothing on screen to contradict it.Folding vault equity in requires one additional userVaultEquities call per watchlist
address on top of the existing batched read, so one extra Info call per wallet on every
refresh. At 50 wallets polled every 60 seconds that’s a real, ongoing CU cost, not a
one-time fix, so it’s a judgment call rather than a strict correctness bug to patch
silently:
- if your watchlist is small and user-pinned (a personal watchlist, not a
leaderboard), the scan is usually worth paying for: sum perps margin and vault
equity into a combined figure (not “Total Value”, since it still excludes spot
balances, see the wallet tracker),
and keep Perps Margin as its own column so the two numbers aren’t buried into
each other.
- if the set is large and refreshed often (a public leaderboard), label the column
Perps Margin rather than “Account Value”, and say plainly that the ranking
excludes vault equity. Don’t rank on a number you haven’t actually computed.
Whichever you choose, don’t call this column “Account Value” or “Total Value”
unqualified, since either name overclaims what a perps-only (or perps-plus-vault) figure
represents.
Two properties of that response shape matter:
Failure is isolated per user. A slot that failed has result: null and a populated
error; the rest of the batch still succeeds. One bad address never takes down the
board, but you must handle the null, or your ranking will throw on the first slot that
didn’t resolve.
Slots come back in request order, with user echoed on each, so you can either zip
by index or key by user. Keying by user is the more robust of the two.
The limit is 50 users per request. Beyond that the request fails; it does not
silently truncate. Chunk larger watchlists, and remember that each chunk is a separate
billed request.
2. Rank
Perps margin is the headline metric and comes straight from the batch. Label it
accordingly. As covered above, it excludes vault equity:
Keep the failed rows rather than dropping them silently. Render them unranked with a
“data unavailable” state. A trader vanishing from the board because of one timed-out
request looks like a bug to anyone watching a specific address.
3. Keep it live between polls
Refetching the batch every few seconds is the expensive way to make a leaderboard feel
live. The cheap way is to poll slowly and recompute unrealized PnL locally against live
mids in between.
You already have the positions from the batch, and the price store from the
wallet tracker.
That’s everything needed:
A 60-second poll with live uPnL in between gives a board that ticks continuously at
1/30th the request cost of a 2-second poll.
Pause polling when the tab is hidden. A leaderboard left open in a background tab
overnight is hundreds of requests nobody looked at.
4. Rank by live volume
Perps margin ranks wealth (with the vault-equity caveat above). To rank activity, accumulate traded notional per address
from the allFills stream, the same subscription already powering prices and the tape.
Because allFills is venue-wide, this observes every trader, not just your
watchlist, which makes it a discovery mechanism as much as a ranking one:
This volume is session-scoped: it counts from when your stream connected, and
resets on reconnect. Label it accordingly (“volume this session”, not “24h volume”). If
you need a persistent figure, accumulate into a datastore server-side with one
long-lived connection shared across all users, rather than one per browser tab.
5. Assemble the surface
Sensible columns: rank, address, perps margin (live) with an “excludes vault equity”
label, unrealized PnL, open position count, and session volume, with the address linking through to the
wallet tracker and
PnL surfaces for a full history on
whoever’s at the top.
Render a rank: null row with a dash and a “data unavailable” state rather than a
number. A trader whose batch slot timed out on this cycle will be ranked normally on the
next one, and showing them in last place in the meantime reads as a real ranking change to
anyone watching that address.
Cost summary
Putting the whole dashboard together, the steady-state cost of a running instance is
small and, importantly, flat in the number of traders you display:
The single allFills subscription doing triple duty is the load-bearing design decision
across this series. Adding traders to the watchlist grows only the batch; adding surfaces
to the dashboard costs nothing extra at all.
Where to go next