Skip to main content
The wallet tracker answers what does this address hold right now?: open perp positions with entry, mark, and liquidation prices, spot balances, resting orders, and a live fill feed that ticks as the address trades. This page introduces three patterns the rest of the series reuses: reading account state, maintaining a live mid-price store, and subscribing to a user-scoped WebSocket channel.
All examples use the info() helper and the key-holding proxy from the series overview.

1. Read account state

You have two options, and the choice is about how much normalization you want. portfolioState is a Uniblock synthetic type: one call returns perps, spot, and account-abstraction mode composed into a single object, with each section isolated so a failure in one doesn’t take down the others.
Note that perpetuals and spot are nullable. A section that failed upstream comes back null rather than throwing, so guard before you read into it. clearinghouseState and spotClearinghouseState are the native Hyperliquid reads, if you’d rather compose them yourself:
Either way, the perp payload has the same shape:

2. Render the positions table

Positions live one level down, under assetPositions[].position. The field that trips people up is szi, which is the signed position size, so its sign is what tells you long from short. There is no side field.
liquidationPx is legitimately null for some positions. Cross-margin positions don’t each have their own liquidation price, since the whole account is collateral. Render a dash, not a zero.
Spot balances are flatter:

3. Show open orders

side is "B" for bid (buy) and "A" for ask (sell), Hyperliquid’s convention throughout, in fills as well as orders. sz is what’s left unfilled; origSz is what the order was placed for, so origSz - sz is the filled portion.
Use frontendOpenOrders rather than openOrders unless you have a reason not to. Both return the same resting orders, but frontendOpenOrders adds order-type and trigger metadata, and it’s served by all four providers, whereas openOrders is not served by GoldRush. If your request fails over to GoldRush, frontendOpenOrders keeps working.

4. Keep mid prices live

Positions are only half the picture: unrealizedPnl in the snapshot was true at time, and it drifts the moment the market moves. To keep it live you need a current mark for each coin. There is no allMids WebSocket channel. This is the single most common wrong assumption when building on this stack: the REST type exists, so people look for the matching stream and don’t find one. The working pattern is a hybrid:
  • Seed and correct from REST allMids on a slow interval (30 seconds is plenty).
  • Update tick-by-tick from the allFills WebSocket channel, taking each fill’s price as the latest trade price for its coin.
Two things to note about that subscription, both covered in full in the next section: allFills needs no parameters (it’s the whole venue), and its payload arrives as [address, fill] tuples, which is why the loop destructures past the address.
allFills is a busy stream, with thousands of events a minute at peak. Don’t re-render on every message. Batch price updates and flush on a timer (250ms works well) so a busy market doesn’t pin your UI thread.
With a live price map, unrealized PnL becomes a derivation rather than a fetch:
Because szi is signed, that one expression is correct for both directions: a short has negative szi, so a falling price yields a positive product.

5. Stream this wallet’s fills

Finally, subscribe to the address’s own fills for a live activity feed.
userFills takes addresses: string[], not user: string. This diverges from Hyperliquid’s native WebSocket contract, and it fails quietly: send the native { type: "userFills", user: "0x..." } form and the subscription is accepted but never delivers anything. The same applies to orderUpdates. If a user-scoped stream is silent, check this first.
The payload diverges too. Fills arrive as [address, fill] tuples under a fills key, not in Hyperliquid’s native { data: { user, fills } } envelope:
Because allFills and userFills share that envelope, one parser handles both:
The dir field is worth surfacing directly in the feed. It is a pre-formatted description of what the fill did to the position, which saves you inferring it from startPosition and szi.
Subscriptions are matched for unsubscribe by their exact body. Send { method: "unsubscribe", subscription: { type: "userFills", addresses: [address] } } with the identical addresses array you subscribed with, or the subscription (and its per-minute billing) stays open. When a user switches wallets, unsubscribe the old address before subscribing the new one.

Reconnecting

The stream will drop. Handle it with exponential backoff, and re-send every active subscription on reconnect, because the server keeps no state across connections:

Next

The wallet tracker shows the present. Next, PnL analytics reconstructs the past from fill history.