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.

Hyperliquid runs two prices for every perp, and they aren't interchangeable. The oracle price is what the asset is worth on spot markets right now, and it decides funding. The mark price is Hyperliquid's own estimate of a fair price for the perp, and it decides margin, liquidations, take-profit and stop-loss triggers, and the unrealized PnL you see on an open position. (A perp is a futures contract with no expiry date. It's most of what trades on Hyperliquid.)
When we checked, BTC's oracle price was $74,986 and its mark price was $74,972. Fourteen dollars apart, about 2 basis points, where a basis point is 0.01%. Across the 179 markets quoting both, the median gap was 8 basis points and the widest was 74. Small numbers, but they land on different things. Read the wrong one and your liquidation alerts fire at the wrong moment, or your funding math comes out off.
Every number in this article comes from tests we ran on 9 November 2026. The method and the full results are in Test and Validation near the end.
Which one your app needs
If you're building a liquidation monitor, you want the mark price. Positions get liquidated on mark, not on the last trade and not on the oracle. If you're building anything around funding, you want the oracle: each hourly payment is position size × oracle price × funding rate.
A chart that plots the wrong one will look subtly off, because the mark can sit a few basis points away from the oracle for minutes at a time, and much further on a thin market.
And if you want to backtest any of this, you need the history, which Hyperliquid's API doesn't keep. More on that below.
How the oracle price is set
About every 3 seconds, each validator publishes an oracle price for every perp. It takes the spot mid prices on Binance (weight 3), OKX (2), Bybit (2), Kraken, KuCoin, Gate, MEXC and Hyperliquid's own spot market (1 each) and computes a weighted median. The exchange then takes a stake-weighted median across all the validators. Two medians stacked on top of each other, so one exchange or one validator can only move the result so far.
Which exchanges count depends on the coin. For something like BTC, whose main spot market is elsewhere, Hyperliquid's own spot price is left out. For something like HYPE, whose main spot market is Hyperliquid, the outside exchanges are left out until they have enough liquidity.
The oracle's own job is funding. Every hour, each position pays or receives position size × oracle price × funding rate, and the premium behind that rate (how far the perp is trading above or below spot) is measured against the oracle too. It also feeds into the mark price, which is next.
How the mark price is set
The mark is the median of three numbers:
- The oracle price, plus a 150-second exponential moving average of the gap between Hyperliquid's mid price and the oracle.
- The median of Hyperliquid's best bid, best ask and last trade.
- A weighted median of perp mid prices on Binance (3), OKX (2), Bybit (2), Gate (1) and MEXC (1).
If only two of those exist, a 30-second exponential moving average of the second one steps in as the third input. The mark updates whenever the validators publish a new oracle round, so both prices move about every 3 seconds.
Why a median of three? Because it's hard to shove around. Someone sweeping Hyperliquid's book moves input 2 immediately, but input 1 only follows through a smoothed average and input 3 comes from other exchanges entirely.
One burst of trading moves the last price a lot and the mark a little, which is what you want from the number your liquidations run on.
At a glance
| Oracle price | Mark price | |
|---|---|---|
| What it measures | The asset's spot price across eligible markets, including Hyperliquid's own spot market for native assets | A fair price for the perp on Hyperliquid |
| Built from | Weighted median of up to 8 spot markets | Median of an oracle-based, a book-based and a perp-based input |
| Updates | About every 3 seconds | At the same time as the oracle |
| Used for | Funding payments | Margin, liquidations, TP/SL triggers, unrealized PnL |
| Field name | oraclePx | markPx |
There's also midPx, the midpoint of Hyperliquid's best bid and best ask. It's neither of the above.

Reading both prices live
Hyperliquid API
metaAndAssetCtxs gives you the oracle, mark and mid price for every perp on Hyperliquid's main DEX (or on a named HIP-3 DEX, more on those below) in one request, along with funding, premium and open interest:
import requests
meta, ctxs = requests.post("https://api.hyperliquid.xyz/info",
json={"type": "metaAndAssetCtxs"}).json()
for asset, ctx in zip(meta["universe"], ctxs):
if asset["name"] == "BTC":
gap = float(ctx["markPx"]) - float(ctx["oraclePx"])
print("oracle", ctx["oraclePx"], "mark", ctx["markPx"],
"mid", ctx["midPx"])
print("mark minus oracle", round(gap, 1), "USD")
print("funding", ctx["funding"], "premium", ctx["premium"])oracle 74986.0 mark 74972.0 mid 74970.5
mark minus oracle -14.0 USD
funding 0.0000125 premium -0.0002000373To watch them change, subscribe to activeAssetCtx. It sends one coin's context about once a second, but the two prices only move when a new validator round lands:
import json, time
from websockets.sync.client import connect
seen, start = [], time.time()
with connect("wss://api.hyperliquid.xyz/ws") as ws:
ws.send(json.dumps({"method": "subscribe", "subscription":
{"type": "activeAssetCtx", "coin": "BTC"}}))
while time.time() < start + 30:
m = json.loads(ws.recv())
if m.get("channel") == "activeAssetCtx":
c = m["data"]["ctx"]
if not seen or seen[-1] != (c["oraclePx"], c["markPx"]):
seen.append((c["oraclePx"], c["markPx"]))
print(time.strftime("%H:%M:%S"), "oracle, mark:", *seen[-1])
print(len(seen), "distinct oracle and mark pairs in 30 s")12:40:13 oracle, mark: 74975.0 74961.0
12:40:14 oracle, mark: 74974.0 74960.0
12:40:20 oracle, mark: 74975.0 74960.0
12:40:24 oracle, mark: 74974.0 74960.0
12:40:27 oracle, mark: 74973.0 74959.0
12:40:33 oracle, mark: 74972.0 74956.0
12:40:35 oracle, mark: 74958.0 74944.0
12:40:38 oracle, mark: 74950.0 74936.0
12:40:41 oracle, mark: 74946.0 74926.0
9 distinct oracle and mark pairs in 30 sOver 60 seconds we got 61 messages carrying 14 price changes, one every 3.1 seconds on median. There's no block number in the message, so you can't say which block a price belongs to.
Hydromancer API
assetContext ({"type": "assetContext", "coins": ["BTC", "ETH"]}) returns the same three prices for up to 20 coins at once, keyed by coin, along with open interest and volume but without funding or premium. For live updates there's the perpPrices stream: one message per block, roughly every 70 milliseconds, with the prices list filled only on blocks that carry a new round. Every row names its block:
import json, os, time
from websockets.sync.client import connect
url = "wss://api.hydromancer.xyz/ws?token=" + os.environ["HYDROMANCER_API_KEY"]
start = time.time()
with connect(url) as ws:
ws.send(json.dumps({"method": "subscribe", "subscription":
{"type": "perpPrices", "coins": ["BTC"]}}))
while time.time() < start + 30:
m = json.loads(ws.recv())
if m.get("type") == "ping":
ws.send(json.dumps({"type": "pong"}))
for r in m.get("data", {}).get("prices", []):
print("block", r["blockNumber"], "oracle", r["oraclePx"],
"mark", r["markPx"], "external perp", r["extPerpPx"])block 1214542145 oracle 74967.00 mark 74952.00 external perp 74934.05
block 1214542186 oracle 74960.00 mark 74942.00 external perp 74933.95
block 1214542235 oracle 74959.00 mark 74942.00 external perp 74927.50
block 1214542276 oracle 74959.00 mark 74942.00 external perp 74927.40
block 1214542336 oracle 74954.00 mark 74939.00 external perp 74926.40
block 1214542374 oracle 74940.00 mark 74925.00 external perp 74911.35
block 1214542435 oracle 74939.00 mark 74924.00 external perp 74910.05
block 1214542462 oracle 74939.00 mark 74924.00 external perp 74904.80
block 1214542509 oracle 74939.00 mark 74920.00 external perp 74904.90Each line is a new round and the block it arrived in. In our 60-second run that was 842 block messages and 19 rounds, each arriving a median 222 milliseconds after its block time on our connection. perpPrices is a Hydromancer addition, not part of Hyperliquid's original API.
Side by side
| Hyperliquid API | Hydromancer API | |
|---|---|---|
| Current prices in one request | metaAndAssetCtxs : everyperp on one DEX (235 markets, 73 KB) | assetContext : up to 20named coins (BTC, ETH and HYPE came back in 541 bytes) |
| Live updates | activeAssetCtx : one coinper subscription, about once a second | perpPrices : up to 200 coinsper subscription, one message per block |
| Block number on each price | No | Yes |
| Rate-limit cost | 20 weight of 1,200 a minute per IP | 2 points of 20,000 a minute (Growth plan) |
For a live display, either works. The block number starts to matter the day someone asks why they were liquidated and you need to show the mark that was in force at that exact block.
Getting the history
Hyperliquid API
There's no request for past oracle or mark prices. candleSnapshot doesn't help either: a candle's open, high, low and close are trade prices, and there's no oracle or mark field in it.
If you want to stay on Hyperliquid alone, you have two options. Record activeAssetCtx yourself from today onward. Or pull Hyperliquid's S3 archive: s3://hyperliquid-archive/asset_ctxs/[date].csv.lz4 holds daily asset context files, uploaded about once a month, requester-pays, with a warning in the docs that updates aren't guaranteed and some data is missing.
Hydromancer API
perpPriceHistoryByTime returns {"prices": [...], "height": ...}, one row per oracle round, oldest first, each with its block number:
import os, time, statistics, requests
H = {"Authorization": "Bearer " + os.environ["HYDROMANCER_API_KEY"]}
end = int(time.time() * 1000)
rows = requests.post("https://api.hydromancer.xyz/info", headers=H, json={
"type": "perpPriceHistoryByTime", "coin": "BTC",
"startTime": end - 3600000, "endTime": end}).json()["prices"]
gaps = [b["time"] - a["time"] for a, b in zip(rows, rows[1:])]
bps = [(float(r["markPx"]) / float(r["oraclePx"]) - 1) * 1e4 for r in rows]
print(len(rows), "price rounds in the last hour")
print("median gap between rounds", statistics.median(gaps), "ms")
print("mark vs oracle: median", round(statistics.median(bps), 2), "bps,",
"widest", round(max(bps, key=abs), 2), "bps")
print("first row:", rows[0])1169 price rounds in the last hour
median gap between rounds 3035.0 ms
mark vs oracle: median -2.27 bps, widest -4.41 bps
first row: {'time': 1794195650955, 'blockNumber': 1214491364, 'dex': 'hyperliquid', 'coin': 'BTC', 'oraclePx': '75026.00', 'markPx': '75003.00', 'extPerpPx': '74996.55'}One hour of BTC came back as 1,169 rounds in a single request. A page is up to 2,000 rows, call it 100 minutes of one coin, so a day is about 15 requests; move startTime past the last row's time for each next page. The coins form takes up to 20 markets at once but caps at 100 rounds per coin, which makes it good for a recent window and wrong for long history. oraclePriceHistory gives you the newest rounds first when you only want the last few. Coverage starts in early August 2025 for Hyperliquid's own markets and on 24 February 2026 for accepted HIP-3 rounds.
Side by side
| Hyperliquid API | Hydromancer API | |
|---|---|---|
| Past oracle and mark prices | No request. Record the stream yourself, or download the S3 asset_ctxs dailyfiles | perpPriceHistoryByTime , onerow per round since August 2025 |
| One hour of BTC | Not from the API | 1,169 rows, 1 request, 10 points |
| One day of BTC | About 15 requests | |
| Block number per row | Yes |
A liquidation backtest, or a "was I liquidated fairly" check, needs the mark at a specific block. Hyperliquid's API serves live prices only, so you either start recording today or fetch past rounds from an archive that kept them.
HIP-3 markets: the price the deployer sent vs the price the network used
HIP-3 markets are perp DEXes that third parties deploy on Hyperliquid, like the stock and commodity perps on xyz. They don't use Hyperliquid's oracle. The deployer submits its own oracle and mark prices every round, and the network decides what to accept.
So there are two histories. Hydromancer's oraclePriceHistoryByTime returns what the deployer submitted, as a plain list, including a second mark input (markPx2) when the deployer sends one. perpPriceHistoryByTime returns what the network accepted and used for margining, tagged with an updateClass of Deployer, Fallback or Normal. For Hyperliquid's own markets the two are the same round.
import os, time, requests
H = {"Authorization": "Bearer " + os.environ["HYDROMANCER_API_KEY"]}
end = int(time.time() * 1000)
body = {"coin": "xyz:GOLD", "startTime": end - 120000, "endTime": end}
post = lambda t: requests.post("https://api.hydromancer.xyz/info",
headers=H, json={"type": t, **body}).json()
sent = post("oraclePriceHistoryByTime")[-1]
used = post("perpPriceHistoryByTime")["prices"][-1]
print("deployer sent: oracle", sent["oraclePx"], "mark", sent["markPx"],
"mark2", sent.get("markPx2"))
print("network used: oracle", used["oraclePx"], "mark", used["markPx"],
"class", used["updateClass"], "block", used["blockNumber"])deployer sent: oracle 4127.40 mark 4127.40 mark2 4128.90
network used: oracle 4127.40 mark 4128.90 class Normal block 1214542043On GOLD the deployer sent two mark inputs, 4,127.40 and 4,128.90, and the network used 4,128.90. A risk tool that reads the submitted price is one step removed from the price liquidations actually run on. Hyperliquid's own API shows the current accepted prices for a HIP-3 DEX through metaAndAssetCtxs with a dex field, but keeps no history of either; a node run with --write-hip3-oracle-updates records the submitted prices to disk, not the accepted ones.
Which price should your app read?
| App | Price to read | Why |
|---|---|---|
| Liquidation monitor, risk dashboard | Mark | Liquidations and margin run on mark. Pair it with liquidationPx fromclearinghouseState |
| Funding calculator, funding arbitrage scanner | Oracle, plus premium | Payments are sized on oracle; the premium is measured against it |
| Trading chart | Mid or last trade, with mark as a line | Users trade against the book, but their stops trigger on mark |
| Backtest of liquidations or stops | Mark history | Needs the mark at each block, not candles |
| "Why was I liquidated" support tool | Mark at a block number | Needs history with block numbers |
Test and Validation
Everything above was 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, inside both APIs' published limits.
What we ran:
- Current prices. One
metaAndAssetCtxscall: 235 markets in 73 KB, of which 179 quoted both an oracle and a mark. Median absolute gap 8.2 basis points, widest 74.2 (RSR). OneassetContextcall for BTC, ETH and HYPE came back in 541 bytes. - Live, 60 seconds.
activeAssetCtxon Hyperliquid (61 messages, 14 oracle or mark changes, median 3.1 seconds between changes) andperpPriceson Hydromancer at the same time (842 block messages, 19 rounds, median 222 milliseconds from block time to arrival on our connection). - History.
perpPriceHistoryByTimefor the last hour of BTC: 1,169 rows, median 3.0 seconds between rounds, one request, 10 rate-limit points. A full 2,000-row page spanned 102.7 minutes, which is where "about 15 requests a day" comes from. - HIP-3.
oraclePriceHistoryByTimeandperpPriceHistoryByTimeforxyz:GOLDover the same window, compared at the same timestamp. - Negative checks. We sent
perpPriceHistoryByTimeto Hyperliquid'sinfoendpoint and got the same422a made-up request type gets. That proves the name doesn't exist there, nothing more. The claim that Hyperliquid's API has no price history rests on its documented request list and on the fieldscandleSnapshotreturns.
What we didn't measure: how the validators produce their rounds, and the exchange weights. Both come from Hyperliquid's docs, linked under Sources. Rate limits quoted in the tables are from the two APIs' docs on the test date.
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
What is the difference between the oracle price and the mark price on Hyperliquid?
The oracle price is a weighted median of spot prices on eligible markets and sets funding. The mark price is the median of three inputs, one of them the oracle, and sets margin, liquidations, TP/SL triggers and unrealized PnL.
How often does Hyperliquid update the oracle price?
About every 3 seconds. In our test the gap between rounds was 3.0 seconds on median, and the mark price updated in the same round.
Does Hyperliquid use the mark price or the oracle price for funding?
The oracle price. The hourly payment is position size × oracle price × funding rate, and the premium behind the rate compares Hyperliquid's impact prices with the oracle.
Can I get historical mark prices from Hyperliquid's API?
Not from the API. Record the activeAssetCtx stream yourself or download the S3 asset_ctxs files. Hydromancer's perpPriceHistoryByTime has one row per round since August 2025.
Why is the mark price different from the last trade?
Because it is a median of three inputs, two of which come from outside Hyperliquid's book. A single trade can move the last price without moving the mark.
Related reading
- Hyperliquid funding rates: how they work and their history
- Hyperliquid liquidation data: how it works and the API
- Hyperliquid oracle price data: the Hydromancer product page
- Hyperliquid fills vs trades: where liquidations show up in the data
- Hyperliquid data feeds: every real-time stream explained
Sources
- Hyperliquid docs: Oracle, Robust price indices, Funding, Historical data, Info endpoint
- Hydromancer docs: assetContext, perpPriceHistoryByTime, oraclePriceHistoryByTime, perpPrices
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 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.

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.