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.

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 |
|---|---|
coin | The 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 |
side | The taker's side, meaning the wallet whose order matched at once against a resting one: Bmeans it bought, A means itsold |
px, sz | Price and size |
time | Block time in milliseconds |
tid | Trade ID, unique within one coin and block time |
hash | Transaction 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 |
|---|---|
side | This wallet's side, B orA . The buyer's fill says B, the seller's says A |
dir | What it did to the position:Open Long , Close Short ,Long > Short for a flip, Buyand Sell for spot, andliquidation labels |
startPosition | The position size before this fill |
closedPnl | Realized PnL from this fill, before fees |
fee, feeToken | The fee paid; negative is a rebate |
crossed | true for the taker's fill,false for the maker's (thewallet whose resting order was hit) |
oid | The order this fill belongs to |
builder, builderFee | The app that routed the order, and its builder fee |
twapId | Set 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:
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"])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.00807The 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.

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:
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"]))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: 26Every 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 market | recentTrades : the latesttrades (10 returned) | tradesByTime : the last 24hours, 2,000 per page |
| Fills for one wallet | userFills : the 2,000 mostrecent. userFillsByTime : upto 2,000 per call and the 10,000 most recent in total. TWAP slices separately via userTwapSliceFills | userFillsByTime : 2,000 perpage, from 28 July 2025, TWAP fills included from 2 August 2025 |
| Fills for a whole market | No single request. Join trades to wallet fills, or own node with --write-fills, or S3 node_fills_by_block | fillsByTime : the last 24hours, 2,000 per page |
| Rate-limit cost | 20 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.
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)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: 17If 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 received | 123 trades | 246 fills |
| Fills per trade ID | 2 for every one of the 123 | |
| Distinct field names seen | 8 | 23, including dir ,closedPnl , fee , crossed ,builder , twapId andliquidation (null unless thefill was a liquidation) |
| TWAP slice fills in the minute | No twapId ; only an all-zerohash hints at one | 41 |
| Fills through a builder | Not marked | 24 |
| Cost | Free, no key | Included 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 minute | Trades | One row per match, no double counting |
| Trading journal, PnL, tax export for a wallet | That wallet's fills | dir , closedPnl , fee andstartPosition |
| Copy trading or whale alerts on known wallets | Those wallets' fills | Direction and position size, per wallet |
| Market-wide analytics: TWAPs, builder share, who closed at a loss | Every fill in the market | Only fills carry twapId ,builder and closedPnl |
| Liquidation tracking | Fills | The 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.
tradesByTimeandfillsByTimeon Hydromancer for one minute of BTC: 106 trades, 212 fills, everytidpresent in exactly two fills, no orphans. Of the 212 fills, 109 had a nonzeroclosedPnland 26 carried abuilder. A second run on a different minute gave 108 trades and 216 fills, again with no orphans. - One trade, two fills on Hyperliquid.
recentTradesfor BTC (10 trades returned), thenuserFillsByTimefor both wallets of one trade in a 2-millisecond window around its block time. Both fills carried the trade'stid. - TWAP check. For a TWAP slice (all-zero hash),
userFillsByTimefor its taker returned nothing in that window, twice.userTwapSliceFillsfor the same wallet returned the fill. That's the basis for "read both". - Live, 60 seconds. Hyperliquid's
tradesstream and Hydromancer'sallFillsstream for BTC at the same time: 123 trades, 246 fills, two fills pertidfor all 123. We saw 8 distinct field names on trades and 23 on fills, withliquidationpresent but null throughout (no liquidations in the minute). 41 fills carried atwapIdand 24 abuilder. - Negative checks.
fillsByTimeandallFillssent to Hyperliquid'sinfoendpoint returned the same422a 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.
Related reading
- How to get a Hyperliquid wallet's trade history
- How to build an order flow dashboard for Hyperliquid
- How to build a trading journal for Hyperliquid
- Hyperliquid liquidation data
- Hyperliquid oracle price vs mark price: the price liquidations run on
- Hyperliquid data feeds: every real-time stream explained
Sources
- Hyperliquid docs: Info endpoint, Subscriptions, Historical data, node README
- Hydromancer docs: tradesByTime, fillsByTime, userFillsByTime, allFills, trades
Read next

Hyperliquid Mempool: Guides and Best Streaming Practices
The Hyperliquid mempool is the set of transactions waiting for the next block. What it holds, who can see it, and how a node streams it to disk.

Hyperliquid Oracle Price vs Mark Price: How Each Is Set
Hyperliquid's oracle price sets funding; the mark price sets margin and liquidations. How each is calculated, how often it updates, where to get history.

How to Build a Hyperliquid TWAP Execution Tool
Build a TWAP execution tool on Hyperliquid: native TWAP orders vs your own slicing, tracking slice fills, and seeing every active TWAP in a market.