Skip to content

fix(hyperliquid-flow): fetch both default dex and xyz builder dex#3077

Merged
koala73 merged 1 commit into
mainfrom
fix/hyperliquid-dual-dex
Apr 14, 2026
Merged

fix(hyperliquid-flow): fetch both default dex and xyz builder dex#3077
koala73 merged 1 commit into
mainfrom
fix/hyperliquid-dual-dex

Conversation

@koala73
Copy link
Copy Markdown
Owner

@koala73 koala73 commented Apr 14, 2026

Why this PR?

Railway bundle logs 2026-04-14 04:10 UTC showed the new Hyperliquid seeder from PR #3074 always skipping with empty data:

```
[Hyperliquid-Flow] SKIPPED: validation failed (empty data) — seed-meta refreshed, existing cache TTL extended
[Hyperliquid-Flow] WARNING: 2 key(s) were expired/missing — EXPIRE was a no-op; manual seed required
```

Root cause: Hyperliquid splits commodity/FX perps into a separate "xyz" builder dex — not the default perp dex. The seeder only queried `{type:"metaAndAssetCtxs"}` which returns the 229-entry default universe (BTC, ETH, SOL, PAXG, and ~225 other crypto perps) but NONE of the `xyz:*` targets. Of 14 whitelisted assets, only 4 matched → `validateFn: assets.length >= 12` rejected the snapshot.

The MIT reference repo listed these with `xyz:` prefixes but didn't document the dex-parameter requirement. Verified live:

```bash
curl -sX POST https://api.hyperliquid.xyz/info -H 'Content-Type: application/json' \
-d '{"type":"metaAndAssetCtxs","dex":"xyz"}' | jq '.[0].universe | map(.name) | [.[]|select(startswith("xyz:"))] | length'

→ 63, including xyz:CL, xyz:BRENTOIL, xyz:GOLD, xyz:SILVER, xyz:EUR, xyz:JPY...

```

Fix

New `fetchAllMetaAndCtxs()` — parallel-fetches both dexes via `Promise.all`, validates each payload independently (with per-dex floors), and merges by concatenation. `xyz:` entries already carry the prefix in their universe names, so no rewriting needed.

New `validateDexPayload(raw, dexLabel, minUniverse)` — per-dex floor gate so the thinner xyz dex (~63 entries) doesn't false-trip the default floor of 50. Errors include the dex label for debuggability.

`validateUpstream()` — back-compat wrapper. Accepts either the legacy single-dex `[meta, assetCtxs]` tuple (used by `buildSnapshot` tests and still valid) or the merged `{universe, assetCtxs}` shape from `fetchAllMetaAndCtxs`.

Entry point now calls `fetchAllMetaAndCtxs()` with a comment explaining why both dexes are required.

Files

  • `scripts/seed-hyperliquid-flow.mjs` — dual-dex fetch + merge, per-dex validation, back-compat validateUpstream
  • `tests/hyperliquid-flow-seed.test.mjs` — 5 new tests: dual-dex merge, cross-dex error propagation, xyz floor accept/reject, merged-shape pass-through

Testing

  • `node --test tests/hyperliquid-flow-seed.test.mjs` → 37/37 pass
  • `npm run typecheck` → clean
  • `npm run typecheck:api` → clean
  • Live verification: xyz-dex endpoint returns all 10 expected commodity/FX symbols

Post-Deploy Monitoring & Validation

  • Logs: Next `seed-bundle-market-backup` run on Railway — `[Hyperliquid-Flow]` section should log `Done` (not `SKIPPED`) and include `recordCount: 14`.
  • Redis: `GET market:hyperliquid:flow:v1` → `assets.length >= 12`, includes xyz:CL/BRENTOIL/GOLD/EUR/JPY.
  • Health: `/api/health?domain=market` — `hyperliquidFlow` drops out of `ON_DEMAND_KEYS` tolerance within first successful cycle.
  • UI: CommoditiesPanel → Perp Flow tab shows commodity + FX sections populated (warmup badge may persist ~1h while volume baseline builds per the 12-sample gate).
  • Failure signal / rollback: if xyz-dex endpoint returns an unexpected shape, `validateDexPayload` will throw and the seeder degrades gracefully (TTL extended, seed-meta bumped with count=0). Revert = one-line change in fetch entry point.
  • Validation window: 2 cycles (10 min).
  • Owner: @koala73

Related

Root cause: Hyperliquid's commodity and FX perps (xyz:CL, xyz:BRENTOIL,
xyz:GOLD, xyz:SILVER, xyz:PLATINUM, xyz:PALLADIUM, xyz:COPPER, xyz:NATGAS,
xyz:EUR, xyz:JPY) live on a separate 'xyz' builder dex, NOT the default
perp dex. The MIT reference repo listed these with xyz: prefixes but
didn't document that they require {type:metaAndAssetCtxs, dex:xyz} as a
separate POST.

Production symptom (Railway bundle logs 2026-04-14 04:10):
  [Hyperliquid-Flow] SKIPPED: validation failed (empty data)

The seeder polled the default dex only, matched 4 of 14 whitelisted assets
(BTC/ETH/SOL/PAXG), and validateFn rejected snapshots with <12 assets.
Seed-meta was refreshed on the skipped path so health stayed OK but
market:hyperliquid:flow:v1 was never written.

Fix:
- New fetchAllMetaAndCtxs(): parallel-fetches both dexes and merges
  {universe, assetCtxs} by concatenation. xyz entries already carry the
  xyz: prefix in their universe names.
- New validateDexPayload(raw, dexLabel, minUniverse): per-dex floor so the
  thinner xyz dex (~63 entries) does not false-trip the default floor of
  50. Errors include the dex label for debuggability.
- validateUpstream(): back-compat wrapper — accepts either the legacy
  single-dex [meta, assetCtxs] tuple (buildSnapshot tests) or the merged
  {universe, assetCtxs} shape from fetchAllMetaAndCtxs.

Tests: 37/37 green. New tests cover dual-dex fetch merge, cross-dex error
propagation, xyz floor accept/reject, and merged-shape pass-through.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Apr 14, 2026 4:24am

Request Review

@koala73 koala73 merged commit 21d33c4 into main Apr 14, 2026
11 checks passed
@koala73 koala73 deleted the fix/hyperliquid-dual-dex branch April 14, 2026 04:28
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 14, 2026

Greptile Summary

This PR fixes the Hyperliquid seeder introduced in #3074, which was always skipping due to empty data because commodity/FX perps (xyz:*) live on a separate builder dex that the original single-dex fetch never reached. The fix adds fetchAllMetaAndCtxs to parallel-fetch both the default and xyz dexes, validates each with a per-dex universe floor (MIN_UNIVERSE_DEFAULT=50, MIN_UNIVERSE_XYZ=30), and merges the results before building the snapshot — ensuring all 14 whitelisted assets are available to satisfy the validateFn gate of ≥12.

Confidence Score: 5/5

  • Safe to merge — fix is correct, well-tested, and degrades gracefully on upstream failure.
  • Root cause is correctly addressed with parallel dual-dex fetching and per-dex validation floors. The back-compat validateUpstream wrapper maintains full test coverage of the existing snapshot logic. The only remaining findings are two P2 style comments about fetchImpl = fetch default parameters deviating from the (...args) => globalThis.fetch(...) pattern used elsewhere in the codebase — neither affects runtime correctness.
  • No files require special attention.

Important Files Changed

Filename Overview
scripts/seed-hyperliquid-flow.mjs Adds fetchAllMetaAndCtxs (parallel dual-dex fetch + merge), validateDexPayload (per-dex floor gate), and a back-compat validateUpstream that accepts both the legacy single-dex tuple and the new merged shape. Entry point updated to call fetchAllMetaAndCtxs. Logic is sound; two default parameters use fetchImpl = fetch instead of the (...args) => globalThis.fetch(...) pattern used elsewhere in the codebase.
tests/hyperliquid-flow-seed.test.mjs Adds 5 new tests covering dual-dex merge, cross-dex error propagation, xyz floor accept/reject, and merged-shape pass-through in validateUpstream. All existing tests continue to exercise the single-dex (back-compat) path via the tuple form. Coverage is appropriate for the new code paths.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Entry Point] --> B[fetchAllMetaAndCtxs]
    B --> C1[fetchHyperliquidMetaAndCtxs\ndex=undefined]
    B --> C2[fetchHyperliquidMetaAndCtxs\ndex='xyz']
    C1 & C2 --> D[Promise.all]
    D --> E1[validateDexPayload\nlabel='default', min=50]
    D --> E2[validateDexPayload\nlabel='xyz', min=30]
    E1 -->|pass| F[Merge universes\nand assetCtxs]
    E2 -->|pass| F
    E1 -->|fail: too small / shape error| G[throw → runSeed\nextends TTL, count=0]
    E2 -->|fail: too small / shape error| G
    F --> H[merged shape\nuniverse + assetCtxs]
    H --> I[buildSnapshot\nvalidateUpstream pass-through]
    I --> J[indexBySymbol → Map]
    J --> K[Iterate ASSETS whitelist\n14 symbols]
    K --> L[validateFn: assets.length >= 12]
    L -->|pass| M[Write to Redis\nCANONICAL_KEY]
    L -->|fail| G
Loading

Reviews (1): Last reviewed commit: "fix(hyperliquid-flow): fetch both defaul..." | Re-trigger Greptile

* @param {string|undefined} dex
* @param {typeof fetch} [fetchImpl]
*/
export async function fetchHyperliquidMetaAndCtxs(dex = undefined, fetchImpl = fetch) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 fetchImpl = fetch deviates from codebase convention

All other seed scripts either call globalThis.fetch(...) directly or define an explicit DEFAULT_FETCH = (...args) => globalThis.fetch(...args) wrapper — see seed-regulatory-actions.mjs for the canonical pattern. AGENTS.md reinforces this: the preferred call site is (...args) => globalThis.fetch(...args), not a bare fetch reference. Using fetch as a default parameter captures the module-scope binding rather than always resolving through globalThis at call time, which can silently fail in environments where fetch hasn't been populated on the global object yet.

Suggested change
export async function fetchHyperliquidMetaAndCtxs(dex = undefined, fetchImpl = fetch) {
export async function fetchHyperliquidMetaAndCtxs(dex = undefined, fetchImpl = (...args) => globalThis.fetch(...args)) {

Context Used: AGENTS.md (source)

* xyz: asset names already carry the `xyz:` prefix in their universe entries,
* so no rewriting is needed — just concatenate.
*/
export async function fetchAllMetaAndCtxs(fetchImpl = fetch) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Same fetchImpl = fetch default needed here

Same issue as fetchHyperliquidMetaAndCtxs above — the default should follow the (...args) => globalThis.fetch(...args) pattern used by seed-regulatory-actions.mjs and recommended in AGENTS.md.

Suggested change
export async function fetchAllMetaAndCtxs(fetchImpl = fetch) {
export async function fetchAllMetaAndCtxs(fetchImpl = (...args) => globalThis.fetch(...args)) {

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant