Skip to content

Surface parity for 1.12.0: webhooks, CVD, HIP-3 oracle, HIP-4 questions, wallet classification, core breadth, symbols - #18

Merged
0xFantomMenace merged 9 commits into
masterfrom
feat/surface-parity
Sep 29, 2026
Merged

0xFantomMenace merged 9 commits into
masterfrom
feat/surface-parity

Conversation

@0xFantomMenace

@0xFantomMenace 0xFantomMenace commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Brings the SDK to parity with the customer-facing API for the 1.12.0 release. Everything is recorded in the existing 1.12.0 CHANGELOG section; the version is unchanged.

Added

  • Webhooks: client.webhooks covers all 21 operations under /v1/webhooks, sync and async. That is the event catalog and plan limits; endpoints (list, create, delete, enable, rotate the secret, send a test); the delivery log and redelivery; subscriptions (list, create, update, delete, resume one, resume all); the dry-run and estimate previews; and watched addresses. The code comes from the earlier webhooks branch (Add the webhooks resource and a signature verifier #9, closed when history was rewritten), updated to the current API: limits() and the two resume routes are new, subscriptions report their pause state, and a resume returns the window that was missed.
  • Signature verification in oxarchive/webhook_signature.py (WebhookVerifier, verify_webhook() and related helpers):
    • It checks the raw request bytes, because the signature covers the exact bytes sent.
    • It accepts every v1= signature in the header, so a delivery signed during a secret rotation verifies under either secret.
    • It rejects a delivery whose timestamp is more than 5 minutes from now by default.
    • It compares signatures in constant time.
  • Cumulative volume delta for Hyperliquid core and HIP-3: cvd.history() returns one page with next_cursor; pass it back unchanged. cvd.iterate() follows the cursor for you.
  • HIP-3 oracle: external_price() and discovery_bounds().
  • HIP-4 questions: questions.list() and questions.get(), plus flat helpers.
  • Wallet classification for Hyperliquid core and HIP-3: wallets.classify(), with every filter, sort and offset-based paging.
  • Symbol universe: client.symbols.list().
  • Hyperliquid core breadth: client.hyperliquid.breadth, the same resource as HIP-3 breadth.
  • Full-depth L2 WebSocket channels orderbook_full and hip3_orderbook_full, with subscribe helpers. They also go into WsChannel; before, their messages failed to parse. Both channels are live-only, so replay() and multi_replay() reject them before sending, as they do for the live-only L4 channels.
  • Order-flow paging docs, folded in from HELD until the API returns next_cursor on order flow: document order-flow paging #14: the README example and the flow() docstring now page with next_cursor. flow() and aflow() already send cursor on Hyperliquid, HIP-3 and HIP-4, and new tests cover that.

Removed

Methods that called routes the API does not serve, so every call returned 404 (each route checked on the live API):

  • hyperliquid.hip4.l2_orderbook: get, history and diffs.
  • hyperliquid.hip4.orders.trigger_levels and trigger_levels_history.
  • spot.orders.flow, tpsl, trigger_levels and trigger_levels_history.
  • hyperliquid.hip3.liquidations.by_user.

Their async versions go too. The shared resources are split so each venue exposes only what it serves: Hip4OrdersResource, SpotOrdersResource and Hip3LiquidationsResource. A scan of every SDK method against the API contract finds no other unserved route.

Changed

Parameters the API never applied are removed; each was confirmed live to have no effect. Passing one raises TypeError before sending, so the call fails instead of returning unfiltered data as if the filter had worked:

  • side on trades.list().
  • user, status and order_type on spot.orders.history(). Core, HIP-3 and HIP-4 order history keep them.
  • depth on the history() methods of l2_orderbook, l4_orderbook and the Lighter l3_orderbook. It still applies to get().

lighter.l3_orderbook.get() now takes account, and history() takes granularity and account.

Fixed

These existing methods broke on live responses:

  • Trades pagination. trades.list() and alist() converted the cursor as if it were a timestamp. A trades cursor is an opaque string ("1790640000578_218303497631402"), so asking for the second page raised ValueError. The cursor is now sent back exactly as returned.
  • HIP-4 freshness. CoinFreshness.funding is now optional. HIP-4 has no funding, so hip4.get_freshness() failed validation on every response.
  • Symbol coverage. data_quality.symbol_coverage() upper-cased the symbol, turning HIP-3 km:US500 into a symbol with no data. It also left the HIP-4 # unencoded, which cut the path short. The symbol is now sent as given, URL-encoded.
  • Spot freshness. SpotTableFreshness expected a tables field the API never sends, so it was always empty. It now has the dataset fields the API returns, and tables lists them.

Notes

  • The WebSocket channel docs list historical replay for orderbook_full and hip3_orderbook_full, but the API refuses it. The SDK treats both channels as live-only and points stored full-depth history at the REST l2_orderbook routes.
  • Webhook write operations are tested only against a mocked transport.
  • The new read methods were checked against live responses: CVD, oracle, questions, classification, symbols, core breadth and the webhook list routes.

Checks

  • pytest: 354 passed, 181 of them new.
  • mypy --strict still fails on master (325 errors). This branch adds none (304 total), and the new modules type-check clean.
  • ruff: no new findings.
  • python -m build: passes. The package-contents check passes with planted stray files, and only oxarchive/ ships.

A trades cursor is an opaque string, "<ms>_<tid>" on Hyperliquid, HIP-3,
HIP-4 and spot and "<ms>_<tid>_<crossed>" on Lighter. trades.list() and
alist() converted it as a timestamp, so asking for the second page raised
ValueError (older releases sent a different number). The cursor now goes
back exactly as returned; integer and datetime cursors are still accepted.

CoinFreshness.funding is optional. The API leaves it out on HIP-4, which has
no funding, so client.hyperliquid.hip4.get_freshness() failed validation on
every response.
Folds in the docs from the order-flow paging branch. The API returns
next_cursor on order flow once this release is published.

- The flow() docstring says a page holds the oldest limit buckets and how to
  follow next_cursor with the same start, end and interval.
- The README order-flow example pages a full day at 1m.
- CHANGELOG: order flow pages with next_cursor.

No code change: flow() and aflow() already send cursor on Hyperliquid, HIP-3
and HIP-4.
orderbook_full (Hyperliquid core) and hip3_orderbook_full (HIP-3) stream
every price level of the book. A subscription opens with an l4_snapshot of
the whole book, then l4_batch messages of changed levels; a level with sz 0
has been removed. Both reach on_l4_snapshot() and on_l4_batch().

WsChannel lists both channels. Without them the subscribe acknowledgement,
snapshot and batch messages failed to parse. New helpers
subscribe_orderbook_full(), subscribe_hip3_orderbook_full() and their
unsubscribe_* counterparts, and FULL_DEPTH_L2_CHANNELS.

The server currently answers a replay request on these channels with an
error, so the docs point stored full-depth history at the REST
l2_orderbook.history() and diffs() routes. Replay is not blocked client side.
client.webhooks covers all 21 operations under /v1/webhooks, sync and async:
the event catalog and plan limits; endpoints (list, create, delete, enable,
rotate secret, test); the delivery log and redelivery; subscriptions (list,
create, update, delete, resume one, resume all); the dry-run and estimate
previews; and watched addresses (list, add, delete).

Ported from the earlier webhooks branch and reconciled with the current API
contract: limits(), resume_subscription() and resume_all_subscriptions() are
new; subscriptions carry their pause state; a resume returns the missed
window; the catalog's cost floor is an object; deliveries can be "failed";
create returns WebhookEndpointCreated with the secret, and the envelope notes
on create, rotate and redeliver are kept on the models. A configuration is
passed as config, a dict or a WebhookSubscriptionConfig.

oxarchive/webhook_signature.py verifies a delivery. It takes the raw request
bytes rather than a parsed object, because the signature covers the exact
bytes sent. It collects every v1 in the header, so a receiver holding either
secret keeps verifying through a rotation, enforces a replay window (5
minutes by default), and compares in constant time.

Write operations are tested against a mocked transport only.
…assification and symbols

- client.hyperliquid.cvd and client.hyperliquid.hip3.cvd: history() returns
  one page of CvdBucket with next_cursor, passed back unchanged with the
  same start, end and interval; iterate() follows the cursor and stops on
  the cursor rather than a short page. Interval and limit are checked
  against the API's accepted values before sending.
- client.hyperliquid.hip3.oracle: external_price() and discovery_bounds().
- client.hyperliquid.hip4.questions: list() (question id cursor) and get(),
  plus the flat helpers list_questions() and get_question().
- client.hyperliquid.wallets and client.hyperliquid.hip3.wallets: classify()
  with every filter, sort, limit and offset. date takes a date, a string or a
  datetime; a datetime without a time zone is UTC.
- client.symbols.list(): the public symbol universe, one SymbolEntry per
  market.

Every method has an a-prefixed async version. Models follow the API
contract and were checked against live responses.
orderbook_full and hip3_orderbook_full are live-only: the API refuses a
replay request on them. replay() and multi_replay() now raise ValueError
with FULL_DEPTH_LIVE_ONLY_ERROR before anything is sent, as they already do
for the live-only L4 channels, and the docs point stored full-depth history
at the REST l2_orderbook.history() and diffs() routes.
client.hyperliquid.breadth serves /v1/hyperliquid/breadth/above-vwap and
/current with the same BreadthResource as HIP-3 breadth: current(),
history() and their async versions. Core history begins 2026-08-24, and
core responses carry empty namespaces maps. Validation messages name the
venue.

The history() docstring now says start defaults to 24 hours before now, as
the API applies it, not 24 hours before end.

Also adds a test that the HIP-4 outcomes settlement filter is sent as
is_settled.
@0xFantomMenace 0xFantomMenace changed the title Surface parity for 1.12.0: webhooks, CVD, HIP-3 oracle, HIP-4 questions, wallet classification, symbols Surface parity for 1.12.0: webhooks, CVD, HIP-3 oracle, HIP-4 questions, wallet classification, core breadth, symbols Sep 29, 2026
Removed methods whose routes the API does not serve (each call was a 404):
hip4.l2_orderbook, hip4.orders.trigger_levels and trigger_levels_history,
spot.orders.flow, tpsl, trigger_levels and trigger_levels_history, and
hip3.liquidations.by_user, with their async versions. The shared resources
split accordingly: Hip4OrdersResource, SpotOrdersResource and
Hip3LiquidationsResource. A scan of every SDK method against the API
contract finds no other route it does not serve.

Removed parameters the API never applied, each confirmed against live
responses: side on trades.list(); user, status and order_type on spot order
history; depth on the L2, L4 and L3 history() methods. Passing one raises
TypeError before sending, since the methods keep **kwargs for the deprecated
coin keyword and would otherwise drop it silently.

data_quality.symbol_coverage() sends the symbol as given, URL-encoded, so
HIP-3 symbols keep their case and HIP-4 "#N" symbols reach the route.

SpotTableFreshness has the shape the API returns (a DataTypeFreshness per
dataset); tables is now a property listing them.

lighter.l3_orderbook.get() takes account; history() takes granularity and
account.
@0xFantomMenace
0xFantomMenace merged commit d3cd068 into master Sep 29, 2026
1 check passed
@0xFantomMenace
0xFantomMenace deleted the feat/surface-parity branch September 29, 2026 03:32
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