BlogDeveloper

Hyperliquid Fills vs Trades: What's the Difference?

A Hyperliquid trade is one match; a fill is one wallet's side of it. What each record holds, why one trade makes two fills, and which API gives you each.

HydromancerTeam
Hyperliquid Fills vs Trades: What's the Difference?

A trade on Hyperliquid is one match: a buyer, a seller, one price, one size. A fill is one wallet's half of that match, with everything that wallet cares about attached: whether it opened or closed a position, what fee it paid, what PnL it realized. Every trade produces exactly two fills, one per wallet. That's the whole distinction, and it trips up more builders than you'd expect.

We pulled one minute of BTC both ways: 106 trades, 212 fills, and every trade ID showed up in exactly two fills. The numbers in this article come from tests on 9 November 2026; the method and the full results are in Test and Validation near the end.

Which one you want

If you're drawing a tape (the live list of trades) or an order flow dashboard, you want trades: one row per match, which side took the price, both wallets. If you're building a journal, a tax export or anything that computes PnL, you want fills, because direction, fee, realized PnL and the position before the fill only exist there.

Two things catch people out. Volume summed from fills is double counted, since every trade is two of them. And liquidations, TWAP slices and builder-routed orders are only marked on fills, never on trades. (A TWAP is a large order split into timed slices. A builder is an app that routes orders for its users.)

What a trade record holds

A trade has eight fields, and they're the same on Hyperliquid's recentTrades request and trades stream and on Hydromancer's trades stream and tradesByTime:

Field Meaning
coinThe market: a perp like BTC
, a HIP-3 market (a perp DEX
deployed on Hyperliquid by a
third party) like xyz:GOLD ,
or a spot pair by index like
@107
sideThe taker's side,
meaning the wallet whose
order matched at once
against a resting one: B
means it bought, A means it
sold
px, szPrice and size
timeBlock time in milliseconds
tidTrade ID, unique within one
coin and block time
hashTransaction hash. TWAP
slices show all zeros
users[buyer, seller]

That's enough to draw a tape, sum buy and sell volume per minute and see who was on each side. It says nothing about what the trade did to either wallet's position.

What a fill record holds

A fill repeats the trade's coin, px, sz, time, tid and hash, then adds the wallet's view:

Field Meaning
sideThis wallet's side, B or
A . The buyer's fill says B
, the seller's says A
dirWhat it did to the position:
Open Long , Close Short ,
Long > Short for a flip, Buy
and Sell for spot, and
liquidation labels
startPositionThe position size before
this fill
closedPnlRealized PnL from this fill,
before fees
fee, feeTokenThe fee paid; negative is a
rebate
crossedtrue for the taker's fill,
false for the maker's (the
wallet whose resting order
was hit)
oidThe order this fill belongs
to
builder, builderFeeThe app that routed the
order, and its builder fee
twapIdSet when the fill is one
slice of a TWAP order

So side means two different things depending on which record you're reading. On a trade it's the taker's side; on a fill it's the owner's side. A trade with side: "A" turns into one fill with side: "A" (the seller) and one with side: "B" (the buyer).

One trade, two fills

Hyperliquid API

Hyperliquid gives you trades by market and fills by wallet. recentTrades returns the latest trades for a coin, and userFillsByTime returns one wallet's fills in a time window. Take a recent BTC trade whose hash isn't all zeros (those are TWAP slices, which live behind a separate request, more below), look up both wallets at that millisecond, and the same tid shows up once on each side:

code
import requests

info = "https://api.hyperliquid.xyz/info"
trade = next(t for t in requests.post(info, json={
    "type": "recentTrades", "coin": "BTC"}).json() if int(t["hash"], 16))
print("trade:", trade["side"], trade["sz"], "@", trade["px"], trade["users"])
for wallet in trade["users"]:
    fills = requests.post(info, json={
        "type": "userFillsByTime", "user": wallet,
        "startTime": trade["time"] - 1, "endTime": trade["time"] + 1}).json()
    for f in fills:
        if f["tid"] == trade["tid"]:
            role = "taker" if f["crossed"] else "maker"
            print(wallet[:10], f["side"], f["dir"], role, "fee", f["fee"],
                  "closedPnl", f["closedPnl"])
code
trade: B 0.04285 @ 74407.0 ['0x525f1da70911fba0e53df2a716182aab9749091b', '0xb7bd9cef0e2826d783f2704d578efbaf705c2f3a']
0x525f1da7 B Open Long taker fee 2.971531 closedPnl 0.0
0xb7bd9cef A Close Long maker fee 0.102026 closedPnl -3.00807

The trade told you who matched whom. Only the fills tell you which wallet was the taker, what each paid, and what each did to its position.

One BTC trade from recentTrades (0.04285 at 74,407) shown next to its two fills: the buyer opened a long as the taker and paid $2.97, the seller closed a long as the maker and realized a $3.01 loss

About those all-zero hashes. They mark TWAP slices, and Hyperliquid serves a wallet's TWAP slice fills from a separate request, userTwapSliceFills, with a stream of the same name. We checked: a TWAP slice fill that userFillsByTime didn't return for its wallet came back from userTwapSliceFills. A journal or tax tool on Hyperliquid's API reads both. Hydromancer's userFillsByTime returns TWAP fills in the same list, with twapId set.

Hydromancer API

Hydromancer keeps both records for every market over the last 24 hours. tradesByTime returns {"trades": [...], "hasMore": ...} with each trade in the stream's shape, and fillsByTime returns a plain list of every fill in the market, each with its user. Both page at 2,000 rows; move startTime past the last row's time for the next page. Same minute, both forms:

code
import os, time, collections, requests

H = {"Authorization": "Bearer " + os.environ["HYDROMANCER_API_KEY"]}
end = int(time.time() * 1000) - 5000
body = {"coin": "BTC", "startTime": end - 60000, "endTime": end}
post = lambda t: requests.post("https://api.hydromancer.xyz/info",
                               headers=H, json={"type": t, **body}).json()
trades, fills = post("tradesByTime")["trades"], post("fillsByTime")
per_trade = collections.Counter(f["tid"] for f in fills)
print(len(trades), "trades,", len(fills), "fills in the same minute")
print("fills per trade:", dict(collections.Counter(per_trade.values())))
print("directions:", dict(collections.Counter(f["dir"] for f in fills)))
print("fills that closed a position with PnL:",
      sum(1 for f in fills if float(f["closedPnl"]) != 0))
print("fills through a builder:", sum(1 for f in fills if f["builder"]))
code
106 trades, 212 fills in the same minute
fills per trade: {2: 106}
directions: {'Open Long': 51, 'Close Long': 53, 'Open Short': 52, 'Close Short': 54, 'Short > Long': 1, 'Long > Short': 1}
fills that closed a position with PnL: 109
fills through a builder: 26

Every one of the 106 trades had exactly two fills, and the fills showed what the trades hid: 109 of them closed a position with realized PnL, and 26 came through a builder app. Hyperliquid's API has no single request for every fill in a market. You can piece it together the way the first snippet did, trades first and then each wallet's fills, as long as you stay inside the per-wallet limits and the rate budget. For a continuous market-wide record without that join, run your own node with --write-fills or download the node_fills_by_block files from the hl-mainnet-node-data S3 archive.

Side by side

Hyperliquid API Hydromancer API
Recent trades for a marketrecentTrades : the latest
trades (10 returned)
tradesByTime : the last 24
hours, 2,000 per page
Fills for one walletuserFills : the 2,000 most
recent. userFillsByTime : up
to 2,000 per call and the
10,000 most recent in total.
TWAP slices separately via
userTwapSliceFills
userFillsByTime : 2,000 per
page, from 28 July 2025,
TWAP fills included from 2
August 2025
Fills for a whole marketNo single request. Join
trades to wallet fills, or
own node with --write-fills
, or S3 node_fills_by_block
fillsByTime : the last 24
hours, 2,000 per page
Rate-limit cost20 weight, plus 1 per 20
fills returned, of 1,200 a
minute per IP
10 points of 20,000 a minute
(Growth plan) per page

If your app only needs its own users' fills, Hyperliquid's per-wallet requests do the job, provided you read the TWAP request too. Market-wide fills are where the two APIs part ways: which wallets closed at a loss this minute, which fills were TWAP slices, which orders came through which app.

Live: the trade feed vs the fill feed

Hyperliquid API

The trades stream is free and needs no key. Subscribe to a coin and every match arrives with both wallets; our order flow dashboard guide builds buy and sell volume per minute from it. For fills, userFills streams one wallet's fills and userTwapSliceFills its TWAP slices, with a cap of 10 distinct wallets across these user subscriptions per IP address.

Hydromancer API

allFills streams every fill in the coins you name. Each message carries a top-level fills list of [wallet, fill] pairs, and the fill has the same fields as userFills. Per-coin allFills is in every plan (20, 100 or 200 coins depending on tier); every coin at once is an add-on. It's a Hydromancer addition, not part of Hyperliquid's original API, and orders still go through Hyperliquid either way.

code
import json, os, time, collections
from websockets.sync.client import connect

url = "wss://api.hydromancer.xyz/ws?token=" + os.environ["HYDROMANCER_API_KEY"]
dirs, twap, start = collections.Counter(), 0, time.time()
with connect(url) as ws:
    ws.send(json.dumps({"type": "subscribe", "subscription":
                        {"type": "allFills", "coins": ["BTC"]}}))
    while time.time() < start + 30:
        m = json.loads(ws.recv())
        for wallet, f in m.get("fills", []):
            dirs[f["dir"]] += 1
            twap += f["twapId"] is not None
print(sum(dirs.values()), "fills in 30 s:", dict(dirs), "| TWAP slices:", twap)
code
962 fills in 30 s: {'Close Short': 172, 'Close Long': 364, 'Open Long': 290, 'Open Short': 114, 'Short > Long': 19, 'Long > Short': 3} | TWAP slices: 17

If your client runs longer than this one, it also has to answer Hydromancer's ping with {"type": "pong"}.

Side by side

In 60 seconds on BTC, listening to both at once:

Hyperliquid trades Hydromancer allFills
Records received123 trades246 fills
Fills per trade ID2 for every one of the 123
Distinct field names seen823, including dir ,
closedPnl , fee , crossed ,
builder , twapId and
liquidation (null unless the
fill was a liquidation)
TWAP slice fills in the minuteNo twapId ; only an all-zero
hash hints at one
41
Fills through a builderNot marked24
CostFree, no keyIncluded in every plan for
named coins

The trade feed tells you the market bought or sold. The fill feed tells you 41 of those 246 fills were TWAP slices and 24 were routed by apps, which changes what the flow means.

Which should you use?

You are building Read Because
Tape, order flow, volume per minuteTradesOne row per match, no double
counting
Trading journal, PnL, tax export for a walletThat wallet's fillsdir , closedPnl , fee and
startPosition
Copy trading or whale alerts on known walletsThose wallets' fillsDirection and position size,
per wallet
Market-wide analytics: TWAPs, builder share, who closed at a lossEvery fill in the marketOnly fills carry twapId ,
builder and closedPnl
Liquidation trackingFillsThe liquidation is marked on
the fill, not the trade

Test and Validation

Measured on 9 November 2026 from a machine in Singapore, against Hyperliquid's public API with no key and Hydromancer's API on a Growth plan key. Hydromancer requests were paced at one every 2.5 seconds and Hyperliquid bulk requests 4 seconds apart.

What we ran:

  • Same minute, REST. tradesByTime and fillsByTime on Hydromancer for one minute of BTC: 106 trades, 212 fills, every tid present in exactly two fills, no orphans. Of the 212 fills, 109 had a nonzero closedPnl and 26 carried a builder. A second run on a different minute gave 108 trades and 216 fills, again with no orphans.
  • One trade, two fills on Hyperliquid. recentTrades for BTC (10 trades returned), then userFillsByTime for both wallets of one trade in a 2-millisecond window around its block time. Both fills carried the trade's tid.
  • TWAP check. For a TWAP slice (all-zero hash), userFillsByTime for its taker returned nothing in that window, twice. userTwapSliceFills for the same wallet returned the fill. That's the basis for "read both".
  • Live, 60 seconds. Hyperliquid's trades stream and Hydromancer's allFills stream for BTC at the same time: 123 trades, 246 fills, two fills per tid for all 123. We saw 8 distinct field names on trades and 23 on fills, with liquidation present but null throughout (no liquidations in the minute). 41 fills carried a twapId and 24 a builder.
  • Negative checks. fillsByTime and allFills sent to Hyperliquid's info endpoint returned the same 422 a made-up type gets. The "no market-wide fills request" claim rests on Hyperliquid's documented request list, not on those 422s.

Field counts are the union of names we saw across the minute; optional fields like liquidation and cloid aren't on every fill. The coverage dates for Hydromancer's userFillsByTime (28 July 2025, TWAP fills from 2 August 2025) are from its docs, not measured.

The code shown in this guide is an example that proves the idea works, not a finished app, so you don't need to copy it line for line. The API requests are exact: the request types, fields and URLs are what each API expects. Feel free to be creative with what you will create!

FAQ

Is a fill the same as a trade on Hyperliquid?

No. A trade is one match between two wallets. A fill is one wallet's side of it, with that wallet's direction, fee and realized PnL. Every trade produces two fills.

Why does one trade show up twice in fills data?

Because both the buyer and the seller get a fill with the same trade ID. To count volume from fills, keep one side of each (time, coin, tid), or count trades instead. A tid on its own is only unique within one coin and block time.

Does side mean the same thing on fills and trades?

No. On a trade, side is the taker's side. On a fill, it is the side of the wallet that owns the fill.

Where do liquidations show up?

On fills. The liquidated wallet's fill carries a liquidation label in dir, and the live fill feed adds a liquidation field. In the trade feed a liquidation looks like any other match. Our minute had none, which is normal in a quiet market.

How do I get fills for a whole market on Hyperliquid?

Hyperliquid's API returns fills per wallet. You can join recent trades to each wallet's fills yourself, within the per-wallet limits. For a complete market record, run a node with --write-fills, download the S3 node_fills_by_block files, or use Hydromancer's fillsByTime and allFills.

Sources

Continue