AArtery
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 normalized valueUsd per slice. Each slice also carries availableUsd (idle/withdrawable cash not in positions) and echoes the address it 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 navUsd per slice and summary.totalNavUsd for the cross-provider Total Assets — a clean, provider-consistent NAV (a freshly-funded wallet reports its cash, not $0). Don't hand-sum valueUsd + availableUsd: those fields' semantics differ per provider (see Aggregation semantics).
  • PnL reconciliation — pair with /v1/me/pnl to compare realized vs current value, or /v1/portfolio/pnl for 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 / hlUser per-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

ParameterTypeRequiredDescription
userstringconditionalDefault 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.
hlUserstringnoPer-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.
pmUserstringnoPer-provider override for Polymarket — the deposit-wallet address. Falls back to user.
providerstringnoComma-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].address is the 0x address actually queried for that provider. With pmUser / hlUser overrides PM and HL can resolve to different addresses; without them both echo user.
  • slices[i].native is the untransformed upstream payload. Polymarket returns an array (one row per position); Hyperliquid returns an object (the full clearinghouseState envelope including assetPositions[], marginSummary, crossMarginSummary, withdrawable, and time). Consumers that want raw venue data don't have to make two more calls.
  • slices[i].valueUsd is the USDC-denominated wallet value when the provider exposes one, else null. 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/stats reports. It is not just the main-dex marginSummary.accountValue shown in native: a wallet whose funds sit in spot or an xyz: builder dex has near-zero native equity but a real valueUsd.
  • slices[i].availableUsd is idle/withdrawable cash NOT in open positions, USD. Hyperliquid = clearinghouseState.withdrawable; Polymarket = the deposit wallet's on-chain idle collateral (pUSD + USDC.e). null when the provider didn't expose it. Note (Hyperliquid): this is a subset of valueUsd (a liquidity breakdown), not additive — see navUsd.
  • slices[i].navUsd is the clean, provider-consistent account NAV — the single number to use as "this provider's total assets" (identical to /v1/me/wallets/{id}/nav navUsdc). It resolves the valueUsd/availableUsd asymmetry:
    • Hyperliquid: navUsd = valueUsdvalueUsd is already the complete account value (cash included), so availableUsd is not added.
    • Polymarket: navUsd = valueUsd + availableUsd — positions plus idle cash (the two are disjoint). null only when the provider reported no value at all. Use navUsd (not valueUsd + availableUsd) for account totals — adding the two uniformly double-counts Hyperliquid.
  • slices[i].buckets (Hyperliquid) breaks valueUsd down 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 equals valueUsd.
  • slices[i].accountMode (Hyperliquid) echoes HL's userAbstraction mode. On unifiedAccount / portfolioMargin wallets the account is ONE pool on the spot clearinghouse (the spot USDC total is marked live, uPnL included), so valueUsd = Σ spot value + vault + stakingequal to HL's own account value. The perp bucket shows the per-dex accountValue as a view carved out of the spot bucket (buckets still sum to valueUsd). 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 carry usd: null). Powers a Balances tab from the same call. Raw shape also available via GET /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) with accountValueUsd + unrealizedPnlUsd. Powers a reserve breakdown; raw per-dex state via GET /v1/hyperliquid/info/clearinghouse-state?dex=.
  • summary.totalNavUsd is the cross-provider Total Assets — Σ per-slice navUsd. This is the correct grand total (no double-count); prefer it over any hand-rolled sum of totalValueUsd/totalAvailableUsd.
  • summary.totalValueUsd sums slices' valueUsd (provider-specific holdings value). Kept as a breakdown; slices with valueUsd: null are excluded but still counted in slices. providersReporting is the count of slices that contributed. Not the grand total — use totalNavUsd.
  • summary.totalAvailableUsd sums idle/withdrawable cash (availableUsd) across providers — HL withdrawable + PM idle collateral. A liquidity breakdown; do not add it to totalValueUsd for a grand total (that double-counts HL) — use totalNavUsd.
  • 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 AND slices[i].note describing the failure, with valueUsd: null.
  • fetchedAt is when the server completed the fan-out. Both legs are fetched in parallel via Promise.allSettled; the slower one bounds the total response time.

Error responses

HTTPerror.codeWhen
400validation_faileduser missing or not a 0x-40-hex address.
400unknown_providerprovider= contains a name not in the supported set.
401unauthenticatedMissing / invalid Authorization header.
403insufficient_scopeKey lacks the read scope.
429quota_exceededOver 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.

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

ParameterTypeRequiredDescription
userstringconditionalDefault 0x address, applied to every provider without an override. Required unless hlUser / pmUser cover each requested provider.
hlUserstringnoHyperliquid native-address override. Falls back to user.
pmUserstringnoPolymarket deposit-wallet override. Falls back to user.
providerstringnoA single provider (hyperliquid or polymarket) for one series. Omit to fan out to all.
windowstringnoCurve 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) at t.
  • netDepositsUsdc (HL) = HL's canonical net-deposit basis (accountValue − pnlHistory); null/absent for PM unbound reconstruction. Subtract it from navUsdc for a deposit-neutral PnL view.
  • source echoes which path served the series: hl_grid_primary (HL canonical grid through its latest paired NAV/cash-flow point) or pm_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

ParameterTypeRequiredDescription
userstringconditionalDefault 0x address, applied to every provider without an override. Required unless hlUser / pmUser cover each requested provider.
hlUserstringnoHyperliquid native-address override. Falls back to user.
pmUserstringnoPolymarket deposit-wallet override. Falls back to user.
providerstringnoA single provider (hyperliquid or polymarket) for a non-merged series. Omit to fan out to all providers and return merged.
windowstringnoCurve window: 1D, 1W, 1M, ALL. Default ALL. Ignored when windows is set.
windowsstringnoBatched 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].sourcesnapshot / hl_portfolio / pm_user_pnl — which path produced the curve.
  • series[i].bound is true when the address is a wallet bound to your org (the local-snapshot path), false for 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_deposits since 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"), Polymarket user-pnl (source: "pm_user_pnl"). A bound wallet on the fallback path carries a note explaining there are no local snapshots yet.
  • merged is 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 (no provider filter); with ?provider=hyperliquid (or polymarket) the single series is returned and merged is null.
  • warnings[] carries soft per-provider failures so one bad provider doesn't fail the whole curve.
  • asOf is 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=hyperliquid expands to all three HL sub-providers server-side; applied.provider echoes back the family label 'hyperliquid' so the chip selection round-trips intact.
  • Per-sub-provider breakdown still available. Pass ?groupBy=provider (without a provider= 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.provider keeps the sub-provider enum; binding via provider=hyperliquid canonicalizes to hyperliquid_perp on the way in. resolveOrgByAddress matches 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.

Edit this page on GitHubLast updated
Portfolio · Artery API Docs