On this page
Competition stats
GET /v1/me/stats is a trading-competition leaderboard over the caller
org's own wallets. For each wallet (or rolled up per provider) it returns five
metrics over a custom window — realized PnL, traded volume, account value,
unrealized PnL, and trade count — and lets you sort them into a ranked
table with one call. It is org-scoped: the bearer key only ever sees its own
org's wallets.
When to use this
- Run a competition. Rank participants over the contest window. For PnL
that matches what users see on the venue (Hyperliquid's official portfolio
PnL), rank by
netPnlUsd(roi=true,sort=-netPnlUsd) — NOTrealizedPnlUsd + unrealizedPnlUsd, which is a different gross trading basis (see Choosing a metric below). You can still rank byvolumeUsdoraccountValueUsd. - Per-account dashboard. Show "PnL + volume + balance" per wallet without stitching three endpoints together.
- Venue rollup.
groupBy=providerto compare Hyperliquid vs Polymarket contribution across the org.
For per-market PnL drill-down (one row per coin / condition, realized vs
unrealized split) use /v1/me/pnl.
/me/stats is the ranked summary; /me/pnl is the breakdown.
Want a period ROI / return %? Two options:
- ROI leaderboard (this endpoint): add
roi=truewithgroupBy=wallet→ each row gainsmwrPct(money-weighted / modified Dietz) +twrPct(time-weighted) +simpleRoiPct+netPnlUsd+netDepositsUsd, computed from the HL portfolio grid (canonical basis: nav = accountValueHistory, net deposits = accountValue − pnlHistory — matches HL's own UI), end-anchored on the row's account value; sortable (sort=-mwrPct), one call. Withmode=livethe whole row (account value + PnL + ROI) is anchored on the same real-time NAV — self-consistent (refresh granularity ≈12–15s from caching). - Precise single wallet:
/v1/me/wallets/{id}/roiuses exact ledger cash flows — use it when you need the exact number for one wallet.
Don't derive ROI from the raw accountValueUsd + PnL columns: those have no
starting NAV and no in-window deposits/withdrawals, so a mid-window
deposit would read as profit. realizedPnlUsd / fees remain fine for a gross
"PnL over the window" column.
Choosing a PnL / ROI metric
Several PnL/return figures exist because they answer different questions. Pick by
goal — don't invent your own basis (e.g. realized + unrealized, or
PnL ÷ accountValue), it won't match the venue:
| Your goal | Use | Where | Notes |
|---|---|---|---|
| PnL that matches the venue (competition, leaderboard, payouts) | netPnlUsd | /me/stats?roi=true or /me/wallets/{id}/roi | = endNav − startNav − netDeposits = Hyperliquid's official portfolio PnL (net of all fees + funding, spot included). The number the user sees on HL. |
| Return % to display | simpleRoiPct | same | Un-weighted = PnL ÷ capital deployed. Most intuitive + stable on freshly-funded wallets. Recommended default. |
| Money-weighted return | mwrPct | same | Modified Dietz; reflects deposit/withdraw timing. |
| Time-weighted return (cross-user comparable) | twrPct | same | Chain-linked at each cash flow; strips flow timing. |
| Gross trading PnL over a window | realizedPnlUsd (+ unrealizedPnlUsd) | /me/stats, /me/pnl | Fill-derived, gross (before fees/funding); a different basis — NOT the official figure. Fine for a "trading PnL" column, wrong for official-aligned ranking. |
| Current account value | accountValueUsd | /me/stats | Point-in-time NAV (perp + spot + vault + staking). |
| PnL curve over time — TOTAL (realized + unrealized) | time-series {t,pnl} | /me/wallets/{id}/pnl-curve (1 wallet) · /portfolio/pnl (cross-provider merge) | NAV-based ΔNAV − Δnet-deposits = the provider's own PnL chart. Node-first, official-aligned. |
| PnL curve over time — REALIZED only | time-series {ts,cumulativeRealized} | /me/pnl/curve | FIFO closed-trade PnL; excludes unrealized. Different basis from the total curve — don't mix on one chart. |
⚠️ Competition / payout PnL →
netPnlUsd. ROI →simpleRoiPct. UsingrealizedPnlUsd + unrealizedPnlUsdfor competition PnL will diverge from HL's own numbers (it omits funding/fees and uses a fill-realized, not NAV-based, definition).
Request
bash# Per-wallet leaderboard, ranked by realized PnL over a contest window
curl -H "Authorization: Bearer art_live_<your-key>" \
"https://api.artery.questflow.ai/v1/me/stats?from=2026-06-01T00:00:00Z&to=2026-06-16T00:00:00Z&groupBy=wallet&sort=-realizedPnlUsd"Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | no | ISO-8601 window start (inclusive). Omit for all-time. |
to | string | no | ISO-8601 window end (exclusive). Omit for "until now". |
groupBy | string | no | provider (default) or wallet. wallet = the per-wallet leaderboard; provider rolls every wallet up by venue. |
mode | string | no | snapshot (default) or live — ROW freshness. snapshot reads accountValueUsd/unrealizedPnlUsd from the latest hourly NAV snapshot (fast over many wallets, ≤1h stale). live fetches real-time getState per wallet AND (with roi=true) anchors the whole row's PnL/ROI on that same live NAV — self-consistent rows; refresh ≈12–15s from caching. |
nav | string | no | DEPRECATED alias of mode (same switch; mode wins when both are sent). |
include | string | no | Comma list of extras. pnlCurve (needs roi=true + groupBy=wallet): each row gains a pnlCurve sparkline — windowed cumulative PnL, anchored 0 at the window start, downsampled to ≤50 points ({t, pnl}, t unix seconds) — computed from the SAME series the ROI math already used, so ~50 wallets' sparklines cost ONE request and zero extra upstream calls. |
roi | string | no | false (default) or true. true adds mwrPct + twrPct + simpleRoiPct + netPnlUsd + netDepositsUsd per wallet — HL computed from the HL portfolio grid (canonical basis, matches HL's own UI), end-anchored on the row's account value. Only with groupBy=wallet. Single-wallet /roi uses exact ledger flows. |
sort | string | no | A field name with optional - prefix for descending. Fields: realizedPnlUsd, unrealizedPnlUsd, accountValueUsd, volumeUsd, mwrPct, twrPct, simpleRoiPct, netPnlUsd (last four need roi=true). Default -realizedPnlUsd. Nulls always sort last. |
platform | string | no | all (default) or mine. mine restricts the fill-derived columns (volumeUsd, realizedPnlUsd, tradeCount) to fills placed through a builder you registered (portal → Wallets → Platform Attributions). accountValueUsd/unrealizedPnlUsd stay whole-account. With no builder registered, mine returns 400 no_platform_attribution. |
walletId | string | no | Repeatable. Restrict to specific wallet UUIDs (must belong to the caller org). |
walletAddress | string | no | Repeatable. Restrict to specific 0x addresses (must belong to the caller org). |
provider | string | no | One of polymarket, hyperliquid, hyperliquid_perp, hyperliquid_hip4, hyperliquid_dex. hyperliquid expands to the whole HL family. |
from/to are validated as ISO-8601; from > to is 400 validation_failed.
An unknown walletId / walletAddress (not in the caller org) is
404 not_found, matching /v1/me/pnl.
Response
jsonc{
"total": {
"accountValueUsd": 6012.88,
"volumeUsd": 21841.5,
"realizedPnlUsd": 412.07,
"unrealizedPnlUsd": 88.4,
"tradingFeeUsd": 9.12,
"builderFeeUsd": 1.83
},
"data": [
{
"walletId": "8f2c…",
"address": "0xab…01",
"accountValueUsd": 5012.88,
"volumeUsd": 18402.1,
"realizedPnlUsd": 380.55,
"unrealizedPnlUsd": 64.2,
"tradingFeeUsd": 7.71,
"builderFeeUsd": 1.54,
"tradeCount": 142
},
{
"walletId": "1d7e…",
"address": "0xcd…02",
"accountValueUsd": 1000.0,
"volumeUsd": 3439.4,
"realizedPnlUsd": 31.52,
"unrealizedPnlUsd": 24.2,
"tradingFeeUsd": 1.41,
"builderFeeUsd": 0.29,
"tradeCount": 17
}
],
"window": { "from": "2026-06-01T00:00:00.000Z", "to": "2026-06-16T00:00:00.000Z" },
"applied": { "groupBy": "wallet", "sort": { "field": "realizedPnlUsd", "dir": "desc" }, "platform": "all" }
}The example above uses groupBy=wallet. The default is groupBy=provider, which
rolls every wallet up by venue — each row then carries provider and walletCount
(how many of your wallets rolled up) instead of walletId / address:
jsonc// groupBy=provider (default) — one row per venue
"data": [
{ "provider": "hyperliquid_perp", "walletCount": 3, "accountValueUsd": 6012.88, "volumeUsd": 21841.5, "realizedPnlUsd": 412.07, "unrealizedPnlUsd": 88.4, "tradeCount": 159 },
{ "provider": "polymarket", "walletCount": 2, "accountValueUsd": 1284.6, "volumeUsd": 3439.4, "realizedPnlUsd": 31.52, "unrealizedPnlUsd": null, "tradeCount": null }
]Field semantics
realizedPnlUsd— realized PnL over[from, to). For Hyperliquid this isΣ closedPnlstraight from the wallet's exchange fills, so it matches HL's own UI and includes perp + HIP-3 + spot with correct windowing (positions opened before the window still realize correctly). Gross of fees — subtracttradingFeeUsd+builderFeeUsdfor net.nullfor non-HL rows and on upstream error.tradingFeeUsd/builderFeeUsd— Σ exchange trading fee and Σ builder fee over the window. Underplatform=allboth come from the same fill set asrealizedPnlUsd(the same compute that powers/roi— the two endpoints can never disagree). Net realized =realizedPnlUsd − tradingFeeUsd − builderFeeUsd. Underplatform=mine,builderFeeUsdis the builder fee on your attributed fills (from thehl_builder_fillexport) buttradingFeeUsdisnull— the trader's exchange fee isn't attributable per-builder; useplatform=allfor it. Bothnullfor non-HL rows or on upstream error.volumeUsd— traded notional over the window. For Hyperliquid it isΣ(px × sz)over the same fill set asrealizedPnlUsd, so the two never disagree. For Polymarket it isΣ(price × qty)from locally-ingested fills.tradeCount— number of fills over the window (Hyperliquid).nullfor non-HL rows.accountValueUsd— current NAV, point-in-time (not windowed). For Hyperliquid it spans perp + spot + vault + staking across every HL dex the wallet uses, and is account-mode aware: onunifiedAccount/portfolioMarginwallets (HL's current default) perp margin is held on the spot balance, so it is counted once — the figure matches the HL app's own portfolio total, not the sum of the Perps and Spot tabs. For Polymarket it is collateral value. For the per-bucket breakdown of any single wallet, use/v1/portfolio.unrealizedPnlUsd— mark-to-market PnL of open positions, point-in-time. For Hyperliquid summed across all perp dexs. Undermode=snapshot(default) it comes from the latest hourly NAV snapshot (≤1h stale, persisted alongsideaccountValueUsd);nullonly for a wallet snapshotted before the field existed, a backfill-only wallet, or Polymarket. Undermode=liveit is real-time. Sorealized + unrealizedtotals are complete in both modes — snapshot just trades ≤1h staleness for speed.mwrPct/twrPct/simpleRoiPct/netDepositsUsd— present only withroi=true+groupBy=wallet.mwrPctis the money-weighted return (modified Dietz),twrPctthe time-weighted return (chain-linked at each cash flow), andsimpleRoiPctthe simple un-weighted return = PnL ÷ capital deployed (startNav + netDeposits). All fractions (0.1= +10%) and equal when there were no deposits/withdrawals.simpleRoiPctis the recommended figure to display — it ignores flow timing, so it stays stable on freshly-funded wallets wheremwrPct/twrPctcan destabilise (capital that arrived near the window edge).netPnlUsd=endNavUsd − startNavUsd − netDepositsUsd= account value change minus net deposits — Hyperliquid’s OFFICIAL portfolio-PnL definition (net of all fees + funding, spot included). Use it for competition/leaderboard PnL, NOTrealizedPnlUsd + unrealizedPnlUsd(a different, gross trading-PnL basis). NAV-based and cash-flow-adjusted (so they already reflect fees + funding), computed from the persisted hourly NAV series — snapshot-granular (flows attributed to the hour, ≤1h end anchor).nullwhen the series can't anchor the window start (e.g. a wallet with no snapshot history yet). For an exact single-wallet figure use/v1/me/wallets/{id}/roi, which uses exact ledger flows.netDepositsUsd= net deposits − withdrawals inside the window.
mode=snapshot (default) vs mode=live (nav= is a deprecated alias of
mode — same switch, mode wins when both are sent). Under snapshot, both
accountValueUsd AND unrealizedPnlUsd come from the latest hourly NAV
snapshot, so the leaderboard stays fast no matter how many wallets you rank —
figures can be up to an hour stale. Under mode=live the account value is
real-time getState per wallet, and with roi=true the whole row is
anchored on that same live NAV — netPnlUsd, simpleRoiPct, mwrPct,
twrPct are recomputed per request against it, so account value, PnL and ROI
move together (self-consistent rows for a polling competition board).
Effective refresh granularity is ≈12–15s (response SWR cache + getState
cache); window start anchors / flows stay grid-granular (they are historical
values and should not tick). Cost: one getState per wallet.
All USD fields are additive: total is the merged sum across every matched
wallet; data is the per-group breakdown. Nullable metrics that are null on a
row are simply excluded from the corresponding total (and sort to the bottom).
How the fields reconcile
The columns come from different sources, so read them with this model:
- Windowed, fill-based —
realizedPnlUsd,volumeUsd,tradeCount,tradingFeeUsd,builderFeeUsd. Summed over[from, to)from exchange fills, ~real-time (≤30s). - Point-in-time, NAV-based —
accountValueUsd,unrealizedPnlUsd. The current whole-account snapshot, not windowed. ≤1h undermode=snapshot, ≤15s undermode=live. - Window return (
roi=true) —mwrPct,twrPct,startNavUsd,endNavUsd,netDepositsUsd. NAV-based over[from, to).
Guarantees you can rely on:
endNavUsd === accountValueUsdwhen the window ends now (toomitted) — ROI and the displayed balance agree on the end value. (With a pastto,endNavUsdis the NAV attowhileaccountValueUsdstays current — both are shown so there's no ambiguity.)- ROI is auditable from its anchors:
mwrPct ≈ (endNavUsd − startNavUsd − netDepositsUsd) / base(modified-Dietz base), andmwrPct == twrPctwhennetDepositsUsd == 0.
Two things that are not a contradiction:
realizedPnlUsdis gross of fees. Net realized =realizedPnlUsd − tradingFeeUsd − builderFeeUsd. SoaccountValueUsdchange ≠realizedPnlUsd— NAV is net of fees and funding (funding isn't surfaced here) and also moves withunrealizedPnlUsdand deposits. Useroi=true(NAV-based) for the true net return; the fill columns are for activity/PnL attribution.- Freshness skew. Under
mode=snapshotthe NAV fields can lag the fill fields by up to an hour. For the tightest alignment between balance and PnL, passmode=live(NAV ≤15s vs fills ≤30s).
applied
applied echoes back the effective query so a client can render the active
state without re-parsing its own request:
applied.groupBy—providerorwallet.applied.sort—{ field, dir }, the resolved sort (defaults filled in).applied.platform—allormine.
Platform attribution (platform=mine)
Web3 wallets trade on many frontends. platform=mine answers "how much of
this activity came through my product?": it keeps only fills whose
order-level builder matches one you registered (the same registration
/v1/me/pnl?platform=mine uses — register once via the portal or
POST /v1/me/platform-attributions).
- Filtered columns:
volumeUsd,realizedPnlUsd(Σ exchangeclosedPnlof matched fills),tradeCount, andbuilderFeeUsd(the builder fee on those matched fills).tradingFeeUsdisnullhere (the exchange fee isn't attributable per-builder — seeplatform=all). - Whole-account columns:
accountValueUsdandunrealizedPnlUsdare point-in-time properties of the wallet — balances and open positions can't be split by the platform that routed an order, so they are NOT filtered. - For Hyperliquid, the builder address isn't in HL's fill feed — Artery resolves it from each order's on-chain action during ingestion, so newly bound wallets get attribution backfilled automatically.
minewith no registered builder fails loudly (400 no_platform_attribution) rather than returning all-zero rows you might misread as "no activity".
Sorting
sort ranks the data rows. Pass a bare field for ascending, or prefix - for
descending:
sort value | Leaderboard meaning |
|---|---|
-realizedPnlUsd | Default. Biggest realized winners first. |
-volumeUsd | Most active traders first. |
-accountValueUsd | Largest balances first. |
-unrealizedPnlUsd | Biggest open-position gains first. |
realizedPnlUsd | Biggest realized losers first. |
Rows whose sort field is null (e.g. a non-HL wallet ranked by
realizedPnlUsd) always land at the end, regardless of direction, so an
unknown value never outranks a real one.
Caching
Responses carry cache-control: private, max-age=15, stale-while-revalidate=30.
Hyperliquid metrics are additionally cached server-side per (wallet, window)
for ~30s, so a leaderboard that polls every few seconds does not fan out to the
exchange on every request.
Error responses
| HTTP | error.code | When |
|---|---|---|
| 400 | validation_failed | Bad from/to (not ISO-8601, or from > to), unknown groupBy, unknown sort field, or unknown provider. |
| 400 | no_platform_attribution | platform=mine with no builder registered for the queried provider(s). |
| 401 | unauthenticated | Missing / invalid Authorization header. |
| 403 | insufficient_scope | Key lacks the read scope. |
| 404 | not_found | A walletId / walletAddress filter names a wallet outside the caller org. |
| 429 | quota_exceeded | Over the pnl.read SKU quota. |
Quota
Metered under the pnl.read SKU — each successful response counts as 1 unit.
Per-tier caps live on the rate limits page.