On this page
Portfolio
GET /v1/portfolio aggregates wallet positions across every provider Artery
supports for on-chain identities. One call fans out — currently Polymarket
- Hyperliquid — and returns a unified envelope with both the raw upstream
payload (
native) and a normalizedvalueUsdper slice. Each slice also carriesavailableUsd(idle/withdrawable cash not in positions) and echoes theaddressit resolved for that provider.
A companion endpoint GET /v1/portfolio/pnl
returns the wallet's cumulative PnL time series over a window, using the same
address-resolution model.
When to use this
- Account dashboards — show "total USDC across all venues" for a wallet
without writing per-venue glue code. Use
navUsdper slice andsummary.totalNavUsdfor the cross-provider Total Assets — a clean, provider-consistent NAV (a freshly-funded wallet reports its cash, not $0). Don't hand-sumvalueUsd + availableUsd: those fields' semantics differ per provider (see Aggregation semantics). - PnL reconciliation — pair with
/v1/me/pnlto compare realized vs current value, or/v1/portfolio/pnlfor the cumulative curve. - Risk monitoring — sum exposure across Polymarket bets and HL perps for a watched address.
- Mixed addresses — a wallet whose Polymarket deposit-wallet differs from
its native Hyperliquid EOA can pass
pmUser/hlUserper-provider overrides in one call.
For per-wallet trade history (fills, fees, etc.) use
/v1/me/fills — it shares the same wallet identity model.
Request
bashcurl -H "Authorization: Bearer art_live_<your-key>" \
"https://api.artery.questflow.ai/v1/portfolio?user=0x9d84cbc7eb19c4cbb24f3866c70a5fcab502d1dc&provider=polymarket,hyperliquid"Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
user | string | conditional | Default EVM address (0x + 40 hex chars) applied to every provider without an override. Required unless a per-provider address (hlUser / pmUser) covers each requested provider. Lowercased server-side before forwarding. |
hlUser | string | no | Per-provider override for Hyperliquid — the user's native HL account (Privy user.address). Use when the HL address differs from the Polymarket deposit-wallet. Falls back to user. |
pmUser | string | no | Per-provider override for Polymarket — the deposit-wallet address. Falls back to user. |
provider | string | no | Comma-separated subset of polymarket, hyperliquid. Omit to fan out to every supported provider. |
Polymarket and Hyperliquid often use different addresses for the same
user (PM deposit-wallet vs HL native EOA). Pass pmUser / hlUser to query
each with its own address in a single call; each slice echoes the address
it resolved. If a per-provider override covers every requested provider,
user is optional.
Unknown provider names return 400 unknown_provider with the offending
values listed. The full set lives in the providers field of every
response so you can discover-then-filter.
Response
jsonc{
"user": "0x9d84cbc7eb19c4cbb24f3866c70a5fcab502d1dc",
"providers": ["polymarket", "hyperliquid"],
"slices": [
{
"provider": "polymarket",
"address": "0x9d84cbc7eb19c4cbb24f3866c70a5fcab502d1dc",
"native": [
// Verbatim Polymarket position array — one row per market the
// wallet has any size on. Keys match Polymarket's own API.
],
"valueUsd": 1234.56,
"availableUsd": 250.00,
"navUsd": 1484.56,
"note": null
},
{
"provider": "hyperliquid",
"address": "0x9d84cbc7eb19c4cbb24f3866c70a5fcab502d1dc",
"native": {
// Verbatim HL `clearinghouseState` (main perp dex) — assetPositions[],
// marginSummary, crossMarginSummary, withdrawable, time.
},
"valueUsd": 5678.90,
"availableUsd": 1200.00,
"navUsd": 5678.90,
"buckets": [
{ "kind": "perp", "usd": 78.90 },
{ "kind": "spot", "usd": 5400.00 },
{ "kind": "vault", "usd": 200.00 },
{ "kind": "staking", "usd": 0 }
],
"accountMode": "unifiedAccount",
"note": null
}
],
"summary": {
"totalNavUsd": 7163.46,
"totalValueUsd": 6913.46,
"totalAvailableUsd": 1450.00,
"providersReporting": 2
},
"warnings": [],
"fetchedAt": "2026-05-19T07:00:00.000Z"
}Aggregation semantics
slices[i].addressis the 0x address actually queried for that provider. WithpmUser/hlUseroverrides PM and HL can resolve to different addresses; without them both echouser.slices[i].nativeis the untransformed upstream payload. Polymarket returns an array (one row per position); Hyperliquid returns an object (the fullclearinghouseStateenvelope includingassetPositions[],marginSummary,crossMarginSummary,withdrawable, andtime). Consumers that want raw venue data don't have to make two more calls.slices[i].valueUsdis the USDC-denominated wallet value when the provider exposes one, elsenull. Polymarket returns position notionals; Hyperliquid returns the complete account value — perp equity across every dex (main + HIP-3/builder) + spot holdings + vault deposits + staking — the same number/v1/me/statsreports. It is not just the main-dexmarginSummary.accountValueshown innative: a wallet whose funds sit in spot or anxyz:builder dex has near-zeronativeequity but a realvalueUsd.slices[i].availableUsdis idle/withdrawable cash NOT in open positions, USD. Hyperliquid =clearinghouseState.withdrawable; Polymarket = the deposit wallet's on-chain idle collateral (pUSD + USDC.e).nullwhen the provider didn't expose it. Note (Hyperliquid): this is a subset ofvalueUsd(a liquidity breakdown), not additive — seenavUsd.slices[i].navUsdis the clean, provider-consistent account NAV — the single number to use as "this provider's total assets" (identical to/v1/me/wallets/{id}/navnavUsdc). It resolves thevalueUsd/availableUsdasymmetry:- Hyperliquid:
navUsd = valueUsd—valueUsdis already the complete account value (cash included), soavailableUsdis not added. - Polymarket:
navUsd = valueUsd + availableUsd— positions plus idle cash (the two are disjoint).nullonly when the provider reported no value at all. UsenavUsd(notvalueUsd + availableUsd) for account totals — adding the two uniformly double-counts Hyperliquid.
- Hyperliquid:
slices[i].buckets(Hyperliquid) breaksvalueUsddown by sub-account:perp/spot/vault/staking. Use it to reconcile against the HL app — each bucket maps to an HL portfolio tab, and the sum equalsvalueUsd.slices[i].accountMode(Hyperliquid) echoes HL'suserAbstractionmode. OnunifiedAccount/portfolioMarginwallets the account is ONE pool on the spot clearinghouse (the spot USDCtotalis marked live, uPnL included), sovalueUsd = Σ spot value + vault + staking— equal to HL's own account value. Theperpbucket shows the per-dex accountValue as a view carved out of thespotbucket (buckets still sum tovalueUsd). Legacy modes (default,disabled,dexAbstraction) keep separate ledgers —valueUsd = perp + spot + vault + staking.slices[i].spotBalances(Hyperliquid) — per-coin spot balances (coin,total,hold,usd; unpriced coins carryusd: null). Powers a Balances tab from the same call. Raw shape also available viaGET /v1/hyperliquid/info/spot-clearinghouse-state.slices[i].perpByDex(Hyperliquid) — per-dex perp equity view (dex=''for the main perp, HIP-3 dexs by name) withaccountValueUsd+unrealizedPnlUsd. Powers a reserve breakdown; raw per-dex state viaGET /v1/hyperliquid/info/clearinghouse-state?dex=.summary.totalNavUsdis the cross-provider Total Assets — Σ per-slicenavUsd. This is the correct grand total (no double-count); prefer it over any hand-rolled sum oftotalValueUsd/totalAvailableUsd.summary.totalValueUsdsums slices'valueUsd(provider-specific holdings value). Kept as a breakdown; slices withvalueUsd: nullare excluded but still counted inslices.providersReportingis the count of slices that contributed. Not the grand total — usetotalNavUsd.summary.totalAvailableUsdsums idle/withdrawable cash (availableUsd) across providers — HLwithdrawable+ PM idle collateral. A liquidity breakdown; do not add it tototalValueUsdfor a grand total (that double-counts HL) — usetotalNavUsd.warnings[]carries soft failures — provider 404, upstream timeout, partial parse — so one bad provider doesn't 5xx the whole response. When a provider call fails entirely you'll see a warning ANDslices[i].notedescribing the failure, withvalueUsd: null.fetchedAtis when the server completed the fan-out. Both legs are fetched in parallel viaPromise.allSettled; the slower one bounds the total response time.
Error responses
| HTTP | error.code | When |
|---|---|---|
| 400 | validation_failed | user missing or not a 0x-40-hex address. |
| 400 | unknown_provider | provider= contains a name not in the supported set. |
| 401 | unauthenticated | Missing / invalid Authorization header. |
| 403 | insufficient_scope | Key lacks the read scope. |
| 429 | quota_exceeded | Over the portfolio.read SKU quota — see X-Quota-Sku, X-Quota-Limit, X-Quota-Remaining, and X-Quota-Reset. |
Worked example
bash# A wallet active on both Polymarket + HL perps
curl -H "Authorization: Bearer art_live_<your-key>" \
"https://api.artery.questflow.ai/v1/portfolio?user=0xabc...&provider=polymarket,hyperliquid"jsonc{
"user": "0xabc...",
"providers": ["polymarket", "hyperliquid"],
"slices": [
{
"provider": "polymarket",
"address": "0xabc...",
"native": [
{
"conditionId": "0xCONDITION_ID",
"outcome": "YES",
"size": "150.00",
"avgPrice": "0.62",
"currentValue": "93.00"
}
],
"valueUsd": 93.0,
"availableUsd": 40.0,
"navUsd": 133.0,
"note": null
},
{
"provider": "hyperliquid",
"address": "0xabc...",
"native": {
"assetPositions": [
{ "type": "oneWay", "position": { "coin": "BTC", "szi": "0.5", "entryPx": "65000", "positionValue": "33850.5", "unrealizedPnl": "350.78" } }
],
"marginSummary": { "accountValue": "12450.78", "totalNtlPos": "33850.5" },
"withdrawable": "8400.21",
"time": 1779163200000
},
// unifiedAccount: the 12100.00 margining the BTC position is held on the
// spot balance, so it's excluded from `spot` (available 0) and counted
// once via `perp` = the full accountValue. Nothing counts twice.
"valueUsd": 12450.78,
"availableUsd": 8400.21,
"navUsd": 12450.78,
"buckets": [
{ "kind": "perp", "usd": 12450.78 },
{ "kind": "spot", "usd": 0 },
{ "kind": "vault", "usd": 0 },
{ "kind": "staking", "usd": 0 }
],
"accountMode": "unifiedAccount",
"note": null
}
],
"summary": {
"totalNavUsd": 12583.78,
"totalValueUsd": 12543.78,
"totalAvailableUsd": 8440.21,
"providersReporting": 2
},
"warnings": [],
"fetchedAt": "2026-05-19T07:00:00.000Z"
}Note how valueUsd (12450.78) is not native.marginSummary.accountValue
plus spot — on a unifiedAccount wallet the perp margin already sits on the
spot balance, so the buckets sum to the same total the HL app shows.
When the same wallet has no Polymarket activity:
jsonc{
"user": "0xabc...",
"providers": ["polymarket", "hyperliquid"],
"slices": [
{ "provider": "polymarket", "address": "0xabc...", "native": [], "valueUsd": null, "availableUsd": 40.0, "navUsd": 40.0, "note": null },
{ "provider": "hyperliquid", "address": "0xabc...", "native": { ... }, "valueUsd": 12450.78, "availableUsd": 8400.21, "navUsd": 12450.78, "note": null }
],
"summary": { "totalNavUsd": 12490.78, "totalValueUsd": 12450.78, "totalAvailableUsd": 8440.21, "providersReporting": 1 },
"warnings": [],
"fetchedAt": "2026-05-19T07:00:00.000Z"
}valueUsd: null + empty native[] is the canonical "no positions" shape —
not an error. summary ignores the null and the total still makes sense.
NAV time series (by address)
GET /v1/portfolio/nav returns a by-address NAV (account-value) curve — no
binding required (visitor profile view). Same address params as /v1/portfolio
(user / hlUser / pmUser) plus window (1D/1W/1M/ALL, default ALL).
Per provider: Hyperliquid = the HL portfolio grid (canonical
accountValueHistory, matches HL's own chart) through its latest paired
NAV/cash-flow point; points carry netDepositsUsdc (= accountValue − pnlHistory). Polymarket = official
user-pnl anchored to the current NAV (external cash flows aren't replayed for
unbound addresses — noted per series). Per-provider failures land in
warnings.
Request
bashcurl -H "Authorization: Bearer art_live_<your-key>" \
"https://api.artery.questflow.ai/v1/portfolio/nav?user=0xabc...&window=1M"Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
user | string | conditional | Default 0x address, applied to every provider without an override. Required unless hlUser / pmUser cover each requested provider. |
hlUser | string | no | Hyperliquid native-address override. Falls back to user. |
pmUser | string | no | Polymarket deposit-wallet override. Falls back to user. |
provider | string | no | A single provider (hyperliquid or polymarket) for one series. Omit to fan out to all. |
window | string | no | Curve window: 1D, 1W, 1M, ALL. Default ALL. |
Response
jsonc{
"window": "ALL",
"providers": ["hyperliquid"],
"series": [
{
"provider": "hyperliquid",
"address": "0x1234abcd0000000000000000000000000000abcd",
"source": "hl_grid_primary", // | "pm_reconstructed" (PM)
"points": [
{ "t": 1782259200, "navUsdc": 10250.42, "netDepositsUsdc": 10000 },
{ "t": 1782345600, "navUsdc": 10480.13, "netDepositsUsdc": 10000 }
]
// "note": "..." // optional caveat (e.g. PM deposit steps as PnL steps)
}
],
"warnings": [], // per-provider upstream failures (never a 5xx)
"asOf": "2026-07-08T10:00:00.000Z"
}t= unix seconds, ascending.navUsdc= account value (USD) att.netDepositsUsdc(HL) = HL's canonical net-deposit basis (accountValue − pnlHistory); null/absent for PM unbound reconstruction. Subtract it fromnavUsdcfor a deposit-neutral PnL view.sourceechoes which path served the series:hl_grid_primary(HL canonical grid through its latest paired NAV/cash-flow point) orpm_reconstructed(PM official pnl anchored to current NAV). An unpaired live account value does not create a curve point.
Cumulative PnL time series
GET /v1/portfolio/pnl returns a wallet-dimension cumulative Profit/Loss
curve over a window — each point is { t, pnl } where t is unix seconds
and pnl is cumulative USD since the window start. It resolves addresses the
same way as /v1/portfolio (user default + optional hlUser / pmUser
overrides) and either returns one series per provider plus a merged
cross-provider curve, or a single non-merged provider series.
Request
bashcurl -H "Authorization: Bearer art_live_<your-key>" \
"https://api.artery.questflow.ai/v1/portfolio/pnl?user=0xabc...&window=1M"Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
user | string | conditional | Default 0x address, applied to every provider without an override. Required unless hlUser / pmUser cover each requested provider. |
hlUser | string | no | Hyperliquid native-address override. Falls back to user. |
pmUser | string | no | Polymarket deposit-wallet override. Falls back to user. |
provider | string | no | A single provider (hyperliquid or polymarket) for a non-merged series. Omit to fan out to all providers and return merged. |
window | string | no | Curve window: 1D, 1W, 1M, ALL. Default ALL. Ignored when windows is set. |
windows | string | no | Batched multi-window. Comma-separated subset of 1D,1W,1M,ALL (e.g. 1D,1W,1M,ALL). Returns one byWindow map per series computed from a single data fetch — one request populates a whole window selector, and switching windows is instant on the client. Takes precedence over window. |
Response
With window (single):
jsonc{
"window": "1M",
"providers": ["polymarket", "hyperliquid"],
"series": [
{
"provider": "polymarket",
"address": "0xabc...",
"bound": true,
"source": "snapshot",
"points": [
{ "t": 1779076800, "pnl": 0 },
{ "t": 1779080400, "pnl": 12.50 },
{ "t": 1779084000, "pnl": 9.80 }
]
},
{
"provider": "hyperliquid",
"address": "0xdef...",
"bound": false,
"source": "hl_portfolio",
"points": [
{ "t": 1779076800, "pnl": 0 },
{ "t": 1779080400, "pnl": -40.00 },
{ "t": 1779084000, "pnl": 120.30 }
]
}
],
"merged": [
{ "t": 1779076800, "pnl": 0 },
{ "t": 1779080400, "pnl": -27.50 },
{ "t": 1779084000, "pnl": 130.10 }
],
"warnings": [],
"asOf": "2026-05-19T07:00:00.000Z"
}With windows=1D,ALL (batched) — each series carries a byWindow map, each
window independently anchored to 0 at its own start; merged is keyed by window:
jsonc{
"windows": ["1D", "ALL"],
"providers": ["hyperliquid"],
"series": [
{
"provider": "hyperliquid",
"address": "0xdef...",
"bound": true,
"byWindow": {
"1D": { "source": "snapshot", "points": [{ "t": 1779076800, "pnl": 0 }, { "t": 1779080400, "pnl": 12.3 }] },
"ALL": { "source": "snapshot", "points": [{ "t": 1777000000, "pnl": 0 }, { "t": 1779080400, "pnl": -40.0 }] }
}
}
],
"merged": null,
"warnings": [],
"asOf": "2026-05-19T07:00:00.000Z"
}Data-source policy (own-node-first)
series[i].source∈snapshot/hl_portfolio/pm_user_pnl— which path produced the curve.series[i].boundistruewhen the address is a wallet bound to your org (the local-snapshot path),falsefor an unbound address served from the provider's official by-address API.- Bound wallets are served from our LOCAL hourly NAV snapshots:
netPnl(t) = Δnav − Δnet_depositssince the window start. A deposit bumps both NAV and net-deposits, so it doesn't read as a gain. Because every provider shares the same basis and time grid, PM + HL series merge cleanly (source: "snapshot"). - Unbound addresses (or a bound wallet without snapshots yet) fall back to
the provider's official by-address series — Hyperliquid
portfolio.pnlHistory(source: "hl_portfolio"), Polymarketuser-pnl(source: "pm_user_pnl"). A bound wallet on the fallback path carries anoteexplaining there are no local snapshots yet. mergedis the Σ of all in-scope per-provider cumulative series on the union time grid (each series forward-filled to its last-known value). It is present only when more than one provider is in scope (noproviderfilter); with?provider=hyperliquid(orpolymarket) the single series is returned andmergedisnull.warnings[]carries soft per-provider failures so one bad provider doesn't fail the whole curve.asOfis the server-side completion timestamp.
Hyperliquid rollup across sub-providers
A wallet can be bound under any of the three Hyperliquid deployments
(hyperliquid_perp, hyperliquid_hip4, hyperliquid_dex). When an org has
multiple HL bindings for the same EOA, the customer-facing PnL surfaces
(/v1/me/pnl and the portal tables) treat them as a single
"Hyperliquid" family:
- Filter family-wide.
/v1/me/pnl?provider=hyperliquidexpands to all three HL sub-providers server-side;applied.providerechoes back the family label'hyperliquid'so the chip selection round-trips intact. - Per-sub-provider breakdown still available. Pass
?groupBy=provider(without aprovider=filter) to see one row per fine-grained provider — useful when you actually want to attribute PnL back to perp vs hip4 vs dex. The portal collapses these visually into one "Hyperliquid" row; raw API consumers see the unrollup'd data. - Storage stays fine-grained.
customer_wallets.providerkeeps the sub-provider enum; binding viaprovider=hyperliquidcanonicalizes tohyperliquid_perpon the way in.resolveOrgByAddressmatches fills across every HL sub-provider, so a single binding covers perp + hip4 + dex without duplicate rows.
The /v1/portfolio endpoint above is already family-flat — it returns
one slices[i] per family (polymarket, hyperliquid), so there is no
sub-provider exposure here. The rollup notes apply to
/v1/me/pnl and the portal
PnL pages.
Quota
This endpoint is metered under the portfolio.read SKU. Each successful
response counts as 1 unit. Per-tier daily caps live in the
rate limits page.