AArtery
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) — NOT realizedPnlUsd + unrealizedPnlUsd, which is a different gross trading basis (see Choosing a metric below). You can still rank by volumeUsd or accountValueUsd.
  • Per-account dashboard. Show "PnL + volume + balance" per wallet without stitching three endpoints together.
  • Venue rollup. groupBy=provider to 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.

Note

Want a period ROI / return %? Two options:

  • ROI leaderboard (this endpoint): add roi=true with groupBy=wallet → each row gains mwrPct (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. With mode=live the 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}/roi uses 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 goalUseWhereNotes
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 displaysimpleRoiPctsameUn-weighted = PnL ÷ capital deployed. Most intuitive + stable on freshly-funded wallets. Recommended default.
Money-weighted returnmwrPctsameModified Dietz; reflects deposit/withdraw timing.
Time-weighted return (cross-user comparable)twrPctsameChain-linked at each cash flow; strips flow timing.
Gross trading PnL over a windowrealizedPnlUsd (+ unrealizedPnlUsd)/me/stats, /me/pnlFill-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 valueaccountValueUsd/me/statsPoint-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 onlytime-series {ts,cumulativeRealized}/me/pnl/curveFIFO closed-trade PnL; excludes unrealized. Different basis from the total curve — don't mix on one chart.

⚠️ Competition / payout PnL → netPnlUsd. ROI → simpleRoiPct. Using realizedPnlUsd + unrealizedPnlUsd for 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

ParameterTypeRequiredDescription
fromstringnoISO-8601 window start (inclusive). Omit for all-time.
tostringnoISO-8601 window end (exclusive). Omit for "until now".
groupBystringnoprovider (default) or wallet. wallet = the per-wallet leaderboard; provider rolls every wallet up by venue.
modestringnosnapshot (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.
navstringnoDEPRECATED alias of mode (same switch; mode wins when both are sent).
includestringnoComma 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.
roistringnofalse (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.
sortstringnoA 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.
platformstringnoall (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.
walletIdstringnoRepeatable. Restrict to specific wallet UUIDs (must belong to the caller org).
walletAddressstringnoRepeatable. Restrict to specific 0x addresses (must belong to the caller org).
providerstringnoOne 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 Σ closedPnl straight 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 — subtract tradingFeeUsd + builderFeeUsd for net. null for non-HL rows and on upstream error.
  • tradingFeeUsd / builderFeeUsd — Σ exchange trading fee and Σ builder fee over the window. Under platform=all both come from the same fill set as realizedPnlUsd (the same compute that powers /roi — the two endpoints can never disagree). Net realized = realizedPnlUsd − tradingFeeUsd − builderFeeUsd. Under platform=mine, builderFeeUsd is the builder fee on your attributed fills (from the hl_builder_fill export) but tradingFeeUsd is null — the trader's exchange fee isn't attributable per-builder; use platform=all for it. Both null for non-HL rows or on upstream error.
  • volumeUsd — traded notional over the window. For Hyperliquid it is Σ(px × sz) over the same fill set as realizedPnlUsd, so the two never disagree. For Polymarket it is Σ(price × qty) from locally-ingested fills.
  • tradeCount — number of fills over the window (Hyperliquid). null for 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: on unifiedAccount / portfolioMargin wallets (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. Under mode=snapshot (default) it comes from the latest hourly NAV snapshot (≤1h stale, persisted alongside accountValueUsd); null only for a wallet snapshotted before the field existed, a backfill-only wallet, or Polymarket. Under mode=live it is real-time. So realized + unrealized totals are complete in both modes — snapshot just trades ≤1h staleness for speed.
  • mwrPct / twrPct / simpleRoiPct / netDepositsUsd — present only with roi=true + groupBy=wallet. mwrPct is the money-weighted return (modified Dietz), twrPct the time-weighted return (chain-linked at each cash flow), and simpleRoiPct the simple un-weighted return = PnL ÷ capital deployed (startNav + netDeposits). All fractions (0.1 = +10%) and equal when there were no deposits/withdrawals. simpleRoiPct is the recommended figure to display — it ignores flow timing, so it stays stable on freshly-funded wallets where mwrPct/twrPct can 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, NOT realizedPnlUsd + 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). null when 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.
Note

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 NAVnetPnlUsd, 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-basedrealizedPnlUsd, volumeUsd, tradeCount, tradingFeeUsd, builderFeeUsd. Summed over [from, to) from exchange fills, ~real-time (≤30s).
  • Point-in-time, NAV-basedaccountValueUsd, unrealizedPnlUsd. The current whole-account snapshot, not windowed. ≤1h under mode=snapshot, ≤15s under mode=live.
  • Window return (roi=true) — mwrPct, twrPct, startNavUsd, endNavUsd, netDepositsUsd. NAV-based over [from, to).

Guarantees you can rely on:

  • endNavUsd === accountValueUsd when the window ends now (to omitted) — ROI and the displayed balance agree on the end value. (With a past to, endNavUsd is the NAV at to while accountValueUsd stays current — both are shown so there's no ambiguity.)
  • ROI is auditable from its anchors: mwrPct ≈ (endNavUsd − startNavUsd − netDepositsUsd) / base (modified-Dietz base), and mwrPct == twrPct when netDepositsUsd == 0.

Two things that are not a contradiction:

  • realizedPnlUsd is gross of fees. Net realized = realizedPnlUsd − tradingFeeUsd − builderFeeUsd. So accountValueUsd change ≠ realizedPnlUsd — NAV is net of fees and funding (funding isn't surfaced here) and also moves with unrealizedPnlUsd and deposits. Use roi=true (NAV-based) for the true net return; the fill columns are for activity/PnL attribution.
  • Freshness skew. Under mode=snapshot the NAV fields can lag the fill fields by up to an hour. For the tightest alignment between balance and PnL, pass mode=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.groupByprovider or wallet.
  • applied.sort{ field, dir }, the resolved sort (defaults filled in).
  • applied.platformall or mine.

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 (Σ exchange closedPnl of matched fills), tradeCount, and builderFeeUsd (the builder fee on those matched fills). tradingFeeUsd is null here (the exchange fee isn't attributable per-builder — see platform=all).
  • Whole-account columns: accountValueUsd and unrealizedPnlUsd are 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.
  • mine with 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 valueLeaderboard meaning
-realizedPnlUsdDefault. Biggest realized winners first.
-volumeUsdMost active traders first.
-accountValueUsdLargest balances first.
-unrealizedPnlUsdBiggest open-position gains first.
realizedPnlUsdBiggest 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

HTTPerror.codeWhen
400validation_failedBad from/to (not ISO-8601, or from > to), unknown groupBy, unknown sort field, or unknown provider.
400no_platform_attributionplatform=mine with no builder registered for the queried provider(s).
401unauthenticatedMissing / invalid Authorization header.
403insufficient_scopeKey lacks the read scope.
404not_foundA walletId / walletAddress filter names a wallet outside the caller org.
429quota_exceededOver 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.

Edit this page on GitHubLast updated
Competition stats · Artery API Docs