BlogDeveloper

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.

HydromancerTeam
Hyperliquid Oracle Price vs Mark Price: How Each Is Set

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:

  1. The oracle price, plus a 150-second exponential moving average of the gap between Hyperliquid's mid price and the oracle.
  2. The median of Hyperliquid's best bid, best ask and last trade.
  3. 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 measuresThe 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 fromWeighted median of up to 8
spot markets
Median of an oracle-based, a
book-based and a perp-based
input
UpdatesAbout every 3 secondsAt the same time as the
oracle
Used forFunding paymentsMargin, liquidations, TP/SL
triggers, unrealized PnL
Field nameoraclePxmarkPx

There's also midPx, the midpoint of Hyperliquid's best bid and best ask. It's neither of the above.

Two cards comparing Hyperliquid's oracle price ($74,986, sets funding) and mark price ($74,972, sets margin, liquidations, TP/SL and unrealized PnL) for BTC on 9 November 2026, with a $14 gap

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:

code
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"])
code
oracle 74986.0 mark 74972.0 mid 74970.5
mark minus oracle -14.0 USD
funding 0.0000125 premium -0.0002000373

To 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:

code
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")
code
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 s

Over 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:

code
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"])
code
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.90

Each 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 requestmetaAndAssetCtxs : every
perp on one DEX (235
markets, 73 KB)
assetContext : up to 20
named coins (BTC, ETH and
HYPE came back in 541 bytes)
Live updatesactiveAssetCtx : one coin
per subscription, about once
a second
perpPrices : up to 200 coins
per subscription, one
message per block
Block number on each priceNoYes
Rate-limit cost20 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:

code
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])
code
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 pricesNo request. Record the
stream yourself, or download
the S3 asset_ctxs daily
files
perpPriceHistoryByTime , one
row per round since August
2025
One hour of BTCNot from the API1,169 rows, 1 request, 10
points
One day of BTCAbout 15 requests
Block number per rowYes

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.

code
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"])
code
deployer sent:   oracle 4127.40 mark 4127.40 mark2 4128.90
network used:    oracle 4127.40 mark 4128.90 class Normal block 1214542043

On 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 dashboardMarkLiquidations and margin run
on mark. Pair it with
liquidationPx from
clearinghouseState
Funding calculator, funding arbitrage scannerOracle, plus premiumPayments are sized on
oracle; the premium is
measured against it
Trading chartMid or last trade, with mark
as a line
Users trade against the
book, but their stops
trigger on mark
Backtest of liquidations or stopsMark historyNeeds the mark at each
block, not candles
"Why was I liquidated" support toolMark at a block numberNeeds 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 metaAndAssetCtxs call: 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). One assetContext call for BTC, ETH and HYPE came back in 541 bytes.
  • Live, 60 seconds. activeAssetCtx on Hyperliquid (61 messages, 14 oracle or mark changes, median 3.1 seconds between changes) and perpPrices on Hydromancer at the same time (842 block messages, 19 rounds, median 222 milliseconds from block time to arrival on our connection).
  • History. perpPriceHistoryByTime for 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. oraclePriceHistoryByTime and perpPriceHistoryByTime for xyz:GOLD over the same window, compared at the same timestamp.
  • Negative checks. We sent perpPriceHistoryByTime to Hyperliquid's info endpoint and got the same 422 a 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 fields candleSnapshot returns.

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.

Sources

Continue