Repository navigation
Docs: spotMetaAndAssetCtxs arrays are not parallel (320 vs 711) — and positions 0-70 line up, which hides it #308
Description
Summary
spot_meta_and_asset_ctxs() returns [meta, assetCtxs], and the docstring lays the two
arrays out adjacently. They are not parallel: today meta["universe"] has 320 entries
and assetCtxs has 711. The perp sibling meta_and_asset_ctxs() is parallel (232 vs
232), so carrying the perp intuition across gives you silently wrong prices.
This is a docs request, not a code defect — the SDK returns the raw payload, which is
correct. But the SDK's own __init__ already encodes the difference, so the knowledge
exists in the codebase and just isn't in the docstring:
# perps: the asset id IS the list position
for asset, asset_info in enumerate(meta["universe"]):
self.coin_to_asset[asset_info["name"]] = asset + offset
# spot: the asset id comes from a FIELD, not from the position
for spot_info in spot_meta["universe"]:
asset = spot_info["index"] + 10000Reproduce (no keys, ~6 lines)
import json, urllib.request
def info(b):
r = urllib.request.Request("https://api.hyperliquid.xyz/info",
data=json.dumps(b).encode(), headers={"Content-Type": "application/json"})
return json.loads(urllib.request.urlopen(r).read())
meta, ctxs = info({"type": "spotMetaAndAssetCtxs"})
uni = meta["universe"]
print(len(uni), len(ctxs)) # 320 711
print(sum(1 for i, e in enumerate(uni) if ctxs[i]["coin"] == e["name"])) # 71 <- by position
print(sum(1 for e in uni if ctxs[e["index"]]["coin"] == e["name"])) # 320 <- by index fieldMeasured on mainnet 2026-08-02.
Why it's worth a docstring line: positions 0-70 line up
That is what makes it a trap rather than an inconvenience. Anything you eyeball while
testing is in the aligned region. The first divergence is at position 71:
universe[71] = @72 i.e. NOCEX/USDC zip -> markPx 1.0 correct -> 0.73944
universe[72] = RANK/USDC (index 73) zip -> markPx 0.73944 correct -> 0.059251
universe[73] = OMNIX/USDC (index 74) zip -> markPx 0.059251 correct -> 0.0000075
Each pair receives its neighbour's price. Note NOCEX getting 1.0 — a number that looks
like a stablecoin and invites exactly the wrong conclusion; and OMNIX wrong by four
orders of magnitude while still returning a valid float. No exception is raised in any
of these cases.
How it cost me: I concluded a stable/stable pair was trading at a mid of 0.1293 and spent
time reasoning about a dislocation that did not exist. The pair was real; the price
belonged to a different token.
Suggested change
Two lines in the spot_meta_and_asset_ctxs docstring, e.g. after the Returns: block:
Note:
universeand the asset-contexts list are not parallel arrays and generally
have different lengths (320 vs 711 at time of writing). Join oncoin, or index the
contexts list by the universe entry's ownindexfield —assetCtxs[entry["index"]].
This differs frommeta_and_asset_ctxs(perps), where list position is the asset id.
Happy to open a PR with exactly that if it's welcome.
Also possible: return a mapping
A spot_ctx_by_coin() helper returning {coin: ctx} would make the correct join the
default one. I'd lean toward the docstring first, since changing the return shape of an
existing method is the more invasive option and the raw passthrough is defensible.
Context, so you can weigh how much to trust the above: I trade a very small book
programmatically and write up venue behaviour that cost me something to learn. Full
write-up with four other findings (Bybit positionIdx, HL's $10 minimum being charged at
the limit price, CCTP bridge costs): https://github.com/massimiliano1991/venue-metrology
If this is already documented somewhere I didn't look, or considered obvious, saying so
is a perfectly useful answer and I'll take it as one.
Edited 2026-08-02: the name field you will actually see for universe[71] is the literal string @72; NOCEX/USDC is derived from meta["tokens"] via the pair's token indices. Clarified so the repro output matches this text exactly.
Activity
massimiliano1991 commentedon Aug 4, 2026
Re-measured today (2026-08-05), mainnet, and adding the prior art I should have looked for first.
Current numbers
Where the trap is exactly.
ctxs[i]["coin"] == universe[i]["name"]holds for exactly thefirst 71 entries (i = 0..70) and breaks at i = 71:
The correct join is
ctxs[ universe[i]["index"] ], which matches 322/322.Reproduction (no key, ~2s):
Why it is worth a docstring line even though nothing is broken in the SDK. The failure is
silent and delayed: a caller who zips the two arrays gets correct results for the first 71
pairs and wrong ones afterwards, with no exception. Concretely, right now
@72zippedpositionally picks up the context of
@71, whosemidPxisnull, while its realmidPxis0.731745— so the symptom shows up as a missing price, not as a mismatch.Prior art, which makes this uncontroversial. ccxt already does the correct join in
python/ccxt/hyperliquid.py,fetch_spot_markets:So the reference multi-venue implementation has known this for a while; the knowledge just is not
in the docstring of
Info.spot_meta_and_asset_ctxs, which is where someone reaching for thisendpoint from this SDK will look. That is the whole of the request: one sentence saying the
second array is indexed by
universe[i]["index"], not by position.Happy to send it as a PR if that is preferred — just say which wording you want.
Correct comment: index-field join is proven right, not just SDK parity