indexer

HyperVapor Documentation

Everything you need to read pool data, watch on-chain events, and integrate with the HyperVapor protocol — REST API, smart contracts, and webhooks, on one page.

Introduction

HyperVapor is a multi-pool leveraged prediction protocol on HyperEVM mainnet (chainId 999). Each pool tracks one price symbol and lets users take a long or short position against other LPs in the same pool, at a leverage tier of their choosing. Settlement moves 1% of the losing side's notional exposure to the winner whenever the price ticks past a threshold or the interval elapses.

This page covers three integration surfaces:

Quickstart

Hit the API directly — no key, no signup.

bash
# 1. List every pool
curl https://xxm.xyz/api/v1/pools | jq '.data[].label'

# 2. Latest 10 settlements on a pool (take the id from step 1)
POOL=$(curl -s https://xxm.xyz/api/v1/pools | jq -r '.data[0].id')
curl "https://xxm.xyz/api/v1/pools/$POOL/rebalances?limit=10" | jq

# 3. A user's full position history
USER=0x0000000000000000000000000000000000000000
curl "https://xxm.xyz/api/v1/user/history/$USER?limit=20" | jq

REST API

Public read-only endpoints under https://xxm.xyz/api/v1/. Every endpoint with a Try it button runs live in your browser.

Conventions

  • Response shape. Success returns { success: true, data, ...extra }. Errors return { error: string } with a non-2xx status.
  • Pool filter. ?pool= accepts a 32-byte pool id or a 20-byte pool address. Omit to query across all pools.
  • Pagination. limit (default 50, max 1000) and offset.
  • Time format. ISO-8601 UTC. from / to filters accept the same.
  • Big numbers. All on-chain amounts are decimal strings, never JSON numbers. Wrap with BigInt() on the client.

System

Health and process metadata.

GET/api/v1/health

App liveness probe.

Also reports `protocolVersion` and `chainId`. Assert on these in CI: fee fields changed meaning between v3.0 and v3.1 without changing name, so pointing a client at the wrong deployment yields plausible, wrong numbers rather than an error.

Pools

Registry of pool contracts deployed by the v3.1 factory and their metadata / config events. Each pool exposes the leverage tiers it was deployed with (`leverageTiers`) and its current fee rates.

GET/api/v1/pools

List every pool registered in the indexer.

`leverageTiers` are the leverage values a user may select when depositing into this pool. They are fixed at deploy time and are per-pool, so read them here rather than hard-coding a list. v3.1 adds `feeBps` / `profitTaxBps` / `treasuryCutBps`: these are owner-governable storage, not constants, and `null` means the indexer has not read them yet (distinct from a governed value of 0). Use them to quote a NEW transaction only — a historical fee has to be explained with the rate that was in force at that block, which you get from the pool's `FeeBpsUpdated` history in config-events.

GET/api/v1/pools/:poolId

Fetch a single pool by its bytes32 id.

NameInTypeDescription
poolId*
path
0x… (bytes32 pool id)Hex pool id from the factory's `MarketCreated` event.
GET/api/v1/pools/:poolId/rebalances

Rebalance events for a single pool, newest first.

NameInTypeDescription
poolId*
path
0x… (bytes32 pool id)Hex pool id from the factory's `MarketCreated` event.
limit
query
integer (1–1000, default 50)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).
GET/api/v1/pools/:poolId/snapshots

Periodic pool_snapshots rows for a single pool (oldest → newest by default).

Each row captures the on-chain TVL of a pool: longTotalAssets, shortTotalAssets, longTotalShares, shortTotalShares, totalAssets, plus the block number / timestamp at which the read happened. v3.1 adds `longNotional` / `shortNotional` (from `getSideNotional`): settlement moves 1% of the LOSING SIDE'S NOTIONAL, not of its collateral, so without these a snapshot cannot explain the size of any rebalance. They are `null` on rows captured before v3.1. The snapshotter inside the indexer process writes a row every POOL_SNAPSHOT_INTERVAL_SEC seconds (default 300s) and de-dupes against the previous row, so idle pools only get a new snapshot when their state actually changes — note that notional participates in that comparison, because an adjustLeverage can change notional while leaving every direction total identical.

NameInTypeDescription
poolId*
path
0x… (bytes32 pool id)Hex pool id from the factory's `MarketCreated` event.
limit
query
integer (1–500, default 100)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).
from
query
ISO timestampInclusive lower bound on the event timestamp.
e.g. 2026-01-01T00:00:00Z
to
query
ISO timestampInclusive upper bound on the event timestamp.
e.g. 2026-12-31T23:59:59Z
order
query
"asc" | "desc" (default "asc")Time ordering of returned rows.
GET/api/v1/pools/:poolId/subpool-snapshots

Per-tier TVL: subpool_snapshots rows for a single pool (one row per direction × leverage tier).

Each pool is split into `(direction × leverage tier)` sub-pools — 20 of them at v3.1's ten tiers. The indexer's pool snapshotter reads every sub-pool and writes a granular row (isLong, leverage, totalAssets, totalShares) alongside each direction-aggregated pool_snapshots row. Use this to chart TVL / share-price by leverage tier. There is deliberately no `notional` field: within a row it is exactly `totalAssets × leverage`. Default ordering is oldest → newest. Filter with `?side=long|short` and/or `?leverage=<tier>`.

NameInTypeDescription
poolId*
path
0x… (bytes32 pool id)Hex pool id from the factory's `MarketCreated` event.
side
query
"long" | "short"Restrict to a single direction.
leverage
query
integer (a tier from the pool's leverageTiers)Restrict to a single leverage tier.
limit
query
integer (1–1000, default 200)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).
from
query
ISO timestampInclusive lower bound on the event timestamp.
e.g. 2026-01-01T00:00:00Z
to
query
ISO timestampInclusive upper bound on the event timestamp.
e.g. 2026-12-31T23:59:59Z
order
query
"asc" | "desc" (default "asc")Time ordering of returned rows.
GET/api/v1/pools/:poolId/config-events

Config-change events for a pool (e.g. fees, listing toggle).

NameInTypeDescription
poolId*
path
0x… (bytes32 pool id)Hex pool id from the factory's `MarketCreated` event.
limit
query
integer (1–1000, default 50)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).

Events (per user)

User-scoped activity. All endpoints accept `?pool=<id|address>` to scope results to a single market.

GET/api/v1/deposits/:user

Deposits made by a user.

Each deposit carries `leverage` — the tier chosen for that deposit (1 = unleveraged). v3.1 changed how `fee` is derived: it is `notional × feeBps` where `notional = amount × leverage`, so a 10x deposit pays ten times the fee of a 1x deposit of the same collateral. Under v3.0 the fee was charged on collateral alone. `amount` is still the collateral the user sent.

NameInTypeDescription
user*
path
0x… (20-byte EVM address)User address (case-insensitive; lower-cased server-side).
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.
limit
query
integer (1–1000, default 50)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).
from
query
ISO timestampInclusive lower bound on the event timestamp.
e.g. 2026-01-01T00:00:00Z
to
query
ISO timestampInclusive upper bound on the event timestamp.
e.g. 2026-12-31T23:59:59Z
GET/api/v1/withdrawals/:user

Withdrawals made by a user.

`withdraw()` exits every leverage tier at once and emits exactly one event, so each row's long/short amounts are aggregated across all tiers — there is no per-tier breakdown to be had here. To value a position before withdrawing, call the pool's `previewWithdraw(user)`: it aggregates all twenty sub-pools and applies the fee and profit-tax rules, which you would otherwise have to reproduce exactly.

NameInTypeDescription
user*
path
0x… (20-byte EVM address)User address (case-insensitive; lower-cased server-side).
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.
limit
query
integer (1–1000, default 50)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).
from
query
ISO timestampInclusive lower bound on the event timestamp.
e.g. 2026-01-01T00:00:00Z
to
query
ISO timestampInclusive upper bound on the event timestamp.
e.g. 2026-12-31T23:59:59Z
GET/api/v1/switches/:user

Long↔short switches made by a user.

Sub-pool model: rows carry `fromLeverage` / `toLeverage` / `amount` / `sharesBurned` / `sharesMinted`. Legacy v2.1 columns (`longAmount` / `shortAmount` / …) are `null` on v3.0+ rows. `switchFee` is charged on the SOURCE notional; `profitTax` on realised profit.

NameInTypeDescription
user*
path
0x… (20-byte EVM address)User address (case-insensitive; lower-cased server-side).
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.
limit
query
integer (1–1000, default 50)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).
from
query
ISO timestampInclusive lower bound on the event timestamp.
e.g. 2026-01-01T00:00:00Z
to
query
ISO timestampInclusive upper bound on the event timestamp.
e.g. 2026-12-31T23:59:59Z
GET/api/v1/leverage-adjustments/:user

Same-direction leverage adjustments (adjustLeverage) made by a user.

Each row is a same-direction move between leverage tiers (no added collateral): `isLong`, `fromLeverage`, `toLeverage`, `amount`, `sharesBurned`, `sharesMinted`, `adjustFee`. Profit tax is deferred on adjust, so there is no profitTax field. v3.1 charges `adjustFee` only when leverage goes UP, on the notional delta (`feeBps × (newNotional − oldNotional)`); levering down is free. That asymmetry is deliberate — it stops a user entering at 1x and immediately levering up to dodge the deposit fee — so expect `adjustFee` to be 0 on roughly half the rows.

NameInTypeDescription
user*
path
0x… (20-byte EVM address)User address (case-insensitive; lower-cased server-side).
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.
limit
query
integer (1–1000, default 50)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).
from
query
ISO timestampInclusive lower bound on the event timestamp.
e.g. 2026-01-01T00:00:00Z
to
query
ISO timestampInclusive upper bound on the event timestamp.
e.g. 2026-12-31T23:59:59Z
GET/api/v1/deposit-records/:user

Convenience: deposit count + first 50 records for a user (and optionally a pool).

NameInTypeDescription
user*
path
0x… (20-byte EVM address)User address (case-insensitive; lower-cased server-side).
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.

User aggregations

Combined / derived per-user views.

GET/api/v1/user/history/:user

Unified, sorted feed of deposits / withdrawals / switches / leverage adjustments for a user.

The feed includes `type: "leverage"` entries (adjustLeverage), and deposit/switch entries carry their leverage tier(s) in `displayText`.

NameInTypeDescription
user*
path
0x… (20-byte EVM address)User address (case-insensitive; lower-cased server-side).
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.
limit
query
integer (1–1000, default 50)Maximum number of items to return.
GET/api/v1/user/stats/:user

Per-pool + overall totals (deposits, withdrawals, fees) for a user.

Unknown / no-activity wallets return 200 with a zeroed stats object (`overall` all "0", `byPool: []`, `createdAt`/`updatedAt` null) rather than a 404.

NameInTypeDescription
user*
path
0x… (20-byte EVM address)User address (case-insensitive; lower-cased server-side).
GET/api/v1/user/pnl-history/:user

Periodic PnL snapshots for a user, optionally per pool.

NameInTypeDescription
user*
path
0x… (20-byte EVM address)User address (case-insensitive; lower-cased server-side).
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.
limit
query
integer (default 100)Maximum number of items to return.
GET/api/v1/user/principal/:user

Per-pool principal balance derived from deposit/withdraw history.

NameInTypeDescription
user*
path
0x… (20-byte EVM address)User address (case-insensitive; lower-cased server-side).
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.

Platform-wide events

Cross-pool feeds. All endpoints accept `?pool=<id|address>` for filtering.

GET/api/v1/rebalances

All rebalances across every pool, newest first.

NameInTypeDescription
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.
limit
query
integer (1–1000, default 50)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).
from
query
ISO timestampInclusive lower bound on the event timestamp.
e.g. 2026-01-01T00:00:00Z
to
query
ISO timestampInclusive upper bound on the event timestamp.
e.g. 2026-12-31T23:59:59Z
GET/api/v1/pool/history

All pool_snapshots rows (across pools), oldest → newest by default.

NameInTypeDescription
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.
limit
query
integer (1–500, default 100)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).
from
query
ISO timestampInclusive lower bound on the event timestamp.
e.g. 2026-01-01T00:00:00Z
to
query
ISO timestampInclusive upper bound on the event timestamp.
e.g. 2026-12-31T23:59:59Z
order
query
"asc" | "desc" (default "asc")Time ordering of returned rows.
GET/api/v1/pool/snapshots/latest

Latest pool_snapshot for every pool (one row per pool).

Convenience endpoint backing the dashboard summary cards. Optionally filter to a single pool with `?pool=<id|address>`. Pools that haven't been snapshotted yet are omitted.

NameInTypeDescription
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.
GET/api/v1/platform/events

Combined factory + pool-config events stream for ops dashboards.

`data` is an object — `{ factoryEvents, configEvents }` — not an array. v3.1 routes four new event types into `configEvents`: `SettlementSkipped` (the oracle reverted, so settlement was skipped and the stale price retained — this is why a gap in `Rebalanced` is normal rather than a sign of missed logs), plus `FeeBpsUpdated` / `ProfitTaxBpsUpdated` / `TreasuryCutBpsUpdated`. Filter by `eventName` to reconstruct the fee rate in force at any block.

NameInTypeDescription
pool
query
0x… (bytes32 pool id or 20-byte pool address)Filter results to a single pool. Accepts either the bytes32 pool id or the 20-byte pool contract address.
limit
query
integer (1–1000, default 50)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).
GET/api/v1/factory/events

Raw events emitted by the v3.1 factory contract.

`MarketCreated` payloads include the pool's `leverageTiers`. Note that `MarketStatusUpdated` toggles index metadata only — it pauses nothing. An unlisted pool should be hidden from listings but is still transactable directly, and its events are still indexed.

NameInTypeDescription
limit
query
integer (1–1000, default 50)Maximum number of items to return.
offset
query
integer (default 0)Skip the first N matching items (use with limit to paginate).

Admin (internal)

Reverse-proxied to the indexer process. The /webhook endpoint is the QuickNode target. The refresh endpoint requires the X-Admin-Token header.

GET/api/admin/indexer/health

Indexer process health (proxied from 127.0.0.1:3001).

GET/api/admin/indexer/stats

Indexer stats: pool counts, byPool breakdown, last event.

POST/api/admin/indexer/refresh-registry

Re-hydrate the in-memory pool registry from the factory contract via RPC.

NameInTypeDescription
X-Admin-Token*
header
stringMust match the indexer's ADMIN_TOKEN env var.
GET/api/admin/indexer/snapshots

Snapshotter schedule + last-run summary (pool TVL + user PnL).

POST/api/admin/indexer/snapshots/run

Force a one-shot snapshot run. Body: { kind: "pool" | "pnl" | "all" } (default "pool").

NameInTypeDescription
X-Admin-Token
header
stringRequired only if ADMIN_TOKEN is set on the indexer process.
POST/webhook

QuickNode stream target. Signature-verified by the indexer; do not call manually.

Smart Contracts

One factory, one price tracker, and ten pools. All addresses are immutable — bookmark them or fetch them from the factory at runtime.

Network

  • Chain — HyperEVM Mainnet (chainId 999)
  • RPChttps://rpc.hyperliquid.xyz/evm
  • Explorer https://hyperevmscan.io/address/<ADDRESS>
  • Native asset — HYPE (18 decimals)

Core addresses

Factory v3.20xbF9B00B3CBa5FF15FEB49d355de5c86839f19072
HyperCorePriceProvider0x74e21871d003cD88F6a60cE16d5e8fEB6daCF20B
USDC (collateral, 6 decimals)0xb88339CB7199b77E23DB6E890353E22632Ba630f

Live pools (10)

Two intervals (10m and 1h) per asset — pick the cadence that fits your strategy. Every pool carries the same leverage tiers (1, 2, 3, 5, 8, 10, 15, 20, 30, 50) and the same launch parameters: feeBps=50, profitTaxBps=30, treasuryCutBps=2500. Those three are governable, so read them from the pool rather than assuming these values.

LabelTypeSymbolCollateralIntervalAddress
HYPE-Native-10m
Native
HYPENATIVE10m
HYPE-Native-1h
Native
HYPENATIVE1h
BTC-USDC-10m
Market
BTCUSDC10m
BTC-USDC-1h
Market
BTCUSDC1h
ETH-USDC-10m
Market
ETHUSDC10m
ETH-USDC-1h
Market
ETHUSDC1h
SOL-USDC-10m
Market
SOLUSDC10m
SOL-USDC-1h
Market
SOLUSDC1h
HYPE-USDC-10m
Market
HYPEUSDC10m
HYPE-USDC-1h
Market
HYPEUSDC1h

Pool actions

Every pool exposes the same four user actions. Native pools take msg.value; ERC20 pools require an approve first.

IPool.sol (essentials)
// Take a position. isLong = true → long, false → short.
function deposit(bool isLong, uint256 amount) external;        // Market
function deposit(bool isLong) external payable;                // Native

// Exit both sides at once. Always available, even if paused.
function withdraw() external;

// Move your entire balance from one side to the other.
function switchPosition(bool fromLongToShort) external;

// Permissionless settlement. Anyone can call when a rebalance is due.
function rebalance() external;

// Read your position.
function getUserPosition(address user) external view returns (
    uint256 longShares, uint256 shortShares,
    uint256 longPrincipal, uint256 shortPrincipal
);
function previewWithdraw(address user) external view returns (
    uint256 grossLong, uint256 grossShort,
    uint256 netLong,   uint256 netShort,
    uint256 totalFee,  uint256 totalProfitTax
);

Discover pools on-chain

Don't hardcode pool addresses — read them from the factory so new markets show up automatically.

discover.ts
import { createPublicClient, http } from "viem";

const client = createPublicClient({
  chain: {
    id: 999,
    name: "HyperEVM",
    nativeCurrency: { name: "HYPE", symbol: "HYPE", decimals: 18 },
    rpcUrls: { default: { http: ["https://rpc.hyperliquid.xyz/evm"] } },
    // Lets viem batch reads into one Multicall3 request — the public RPC
    // will throttle you without it.
    contracts: { multicall3: { address: "0xcA11bde05977b3631167028862bE2a173976CA11" } },
  },
  transport: http(),
  batch: { multicall: true },
});

const FACTORY_ABI = [
  { type: "function", name: "getAllMarketIds", inputs: [], outputs: [{ type: "bytes32[]" }], stateMutability: "view" },
  { type: "function", name: "getMarketInfo", inputs: [{ name: "id", type: "bytes32" }],
    outputs: [{ components: [
      { name: "market",            type: "address" },
      { name: "marketType",        type: "uint8"   },   // 0 = ERC20, 1 = NATIVE
      { name: "priceSymbol",       type: "string"  },
      { name: "collateralToken",   type: "address" },
      { name: "priceProvider",     type: "address" },
      { name: "marketOwner",       type: "address" },
      { name: "rebalanceInterval", type: "uint256" },   // 600 (10m) or 3600 (1h)
      { name: "createdAt",         type: "uint256" },
      { name: "listed",            type: "bool"    },
      { name: "exists",            type: "bool"    },
    ], type: "tuple" }],
    stateMutability: "view" },
] as const;

const ids = await client.readContract({
  address: "0xbF9B00B3CBa5FF15FEB49d355de5c86839f19072",
  abi: FACTORY_ABI,
  functionName: "getAllMarketIds",
});

const pools = await Promise.all(ids.map((id) =>
  client.readContract({ address: "0xbF9B00B3CBa5FF15FEB49d355de5c86839f19072", abi: FACTORY_ABI, functionName: "getMarketInfo", args: [id] })
));

Events

The event topics to filter for if you're building an indexer, subgraph, or alerting bot. topic0 is the keccak256 of the canonical signature. Rows tagged v3.1 did not exist in v3.0, so a handler carried over from Monad has never seen them.

Topic0 reference

ScopeNameSignatureTopic0
Pool
DepositedDeposited(address,bool,uint256,uint256,uint256,uint256,uint256)
Pool
WithdrawnWithdrawn(address,uint256,uint256,uint256,uint256,uint256,uint256,uint256)
Pool
SwitchedSwitched(address,bool,uint256,uint256,uint256,uint256,uint256,uint256,uint256)
Pool
LeverageAdjustedLeverageAdjusted(address,bool,uint256,uint256,uint256,uint256,uint256,uint256)
Pool
RebalancedRebalanced(uint256,bool,uint256)
Pool
SettlementSkipped
v3.1
SettlementSkipped(uint256,uint256)
Pool
FeeBpsUpdated
v3.1
FeeBpsUpdated(uint256)
Pool
ProfitTaxBpsUpdated
v3.1
ProfitTaxBpsUpdated(uint256)
Pool
TreasuryCutBpsUpdated
v3.1
TreasuryCutBpsUpdated(uint256)
Pool
Paused
v3.1
Paused(address)
Pool
Unpaused
v3.1
Unpaused(address)
Factory
MarketCreatedMarketCreated(bytes32,address,uint8,string,address,address,address,address,uint256,uint256[])
Factory
MarketStatusUpdatedMarketStatusUpdated(bytes32,bool)
Factory
OwnershipTransferredOwnershipTransferred(address,address)

Webhooks

We push every relevant on-chain log to your endpoint via QuickNode Streams with HMAC-signed payloads. Set it up once and you have an independent mirror of the protocol state.

How it works

  1. The provider subscribes to Block w/ Receipts on HyperEVM mainnet.
  2. Each block runs through the filter (below), which keeps only the known event topics.
  3. The payload is POST-ed with HMAC-SHA256 signature to your URL.
  4. You verify the signature, decode the events, and write to your store.

QuickNode setup

  1. Create a Stream → Network HyperEVM Mainnet, Dataset Block w/ Receipts, Start block Latest.
  2. Paste the filter below and use Test Filter to confirm logs come through.
  3. Destination → POST https://your-endpoint/webhook.
  4. Security → enable HMAC, save the secret as QUICKNODE_WEBHOOK_SECRET in your env.
  5. Retries → exponential backoff. Decode idempotently by (txHash, logIndex).
quicknode-stream-filter.js
// QuickNode Stream Filter — HyperVapor v3.2 (HyperEVM, chain 999)
// Forward every log whose topic0 matches a known event. v3.2 changed no
// signature, so every hash below carries over from v3.1 verbatim.
// Full version with the admin topics: packages/indexer/ingest/quicknode-stream-filter.js
const TOPICS = new Set([
  // Pool — user actions
  "0xa2f51034b7d5c22afc3c90a46dceef0885e40ada060439c939377f4c1eea9ff7", // Deposited
  "0x06c514d6b519b3f249f6f8b7fc10573de80de5388bfc514bfb432616ee930be7", // Withdrawn
  "0x88342168f36b10112d2ef30aa41be9b0f0066542438783575e270eafc6e63bfb", // Switched
  "0x685940063de382cbb1302282d84691a35ec89b61de21bd4e8a570d0ef14073be", // LeverageAdjusted
  // Pool — settlement
  "0x5ef5c3de2451b8e81ef437df0ed11d9c3e7fd1485531ad5dd1874f64df4d7d70", // Rebalanced
  "0xd4649f99bdcd09936ea6b0321efd28f26654dd548382383a3da5d4f27b5083eb", // SettlementSkipped  (v3.1)
  // Pool — governance
  "0x0399aa81c69e33acc2ff42d0f7a0809e6bf4c10ff3c3a7e040b12fedc14602f2", // FeeBpsUpdated       (v3.1)
  "0x7c0f28483aab8073d1840eb9c7d2e344d825048684e69ccedbdbe90bbb7389b8", // ProfitTaxBpsUpdated (v3.1)
  "0x82ff8c03d5653e8ed28fe0907512a3e819127c86f8b2be61e029e9e77ea95451", // TreasuryCutBpsUpdated (v3.1)
  // Factory
  "0x324da90b745a702fd1df222215baa38c3b03d960df9df726244d99990ac45e03", // MarketCreated
  "0x1fb3747e10a9f9d9b8368bbf1e725aa452f6c4eb3527168288f16899f453bef4", // MarketStatusUpdated
]);

function main(streamData) {
  const blocks = [];
  for (const item of streamData.data ?? []) {
    const block = item.block ?? item;
    const logs = (item.receipts ?? block.receipts ?? [])
      .flatMap((r) => r.logs ?? [])
      .filter((l) => l.topics?.[0] && TOPICS.has(l.topics[0].toLowerCase()));
    if (logs.length) blocks.push({ number: block.number, hash: block.hash, timestamp: block.timestamp, logs });
  }
  return blocks.length ? { data: blocks, metadata: streamData.metadata } : null;
}

Payload shape

json
{
  "data": [
    {
      "number":    "0x...",
      "hash":      "0x...",
      "timestamp": "0x...",      // hex unix seconds
      "logs": [
        {
          "address":          "0x5fa1585458ca8fc1bb2b9b1528d488cdcbb8643c",
          "topics":           ["0xc2cf288c...", "0x000...abc"],
          "data":             "0x...",
          "blockNumber":      "0x...",
          "transactionHash":  "0x...",
          "transactionIndex": "0x...",
          "blockHash":        "0x...",
          "logIndex":         "0x...",
          "removed":          false
        }
      ]
    }
  ],
  "metadata": { "...": "..." }
}

Verify the signature

QuickNode sets X-QN-Signature to HMAC-SHA256(secret, rawBody), hex-encoded. Verify before trusting any payload.

verify.ts
import { createHmac, timingSafeEqual } from "crypto";
import express from "express";

const app = express();

// IMPORTANT: capture the raw body BEFORE JSON.parse runs.
app.use(express.json({
  limit: "10mb",
  verify: (req, _res, buf) => { (req as any).rawBody = buf.toString(); },
}));

app.post("/webhook", (req, res) => {
  const sig = (req.header("x-qn-signature") ?? "").replace(/^sha256=/, "");
  const expected = createHmac("sha256", process.env.QUICKNODE_WEBHOOK_SECRET!)
    .update((req as any).rawBody)
    .digest("hex");

  const a = Buffer.from(sig);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  // req.body is safe to use now
  res.json({ ok: true });
});

Versioning

  • REST API — versioned via URL prefix (/api/v1/). A breaking change introduces /api/v2/ and the old version stays online for at least 90 days.
  • Smart contracts — currently v3.2 on HyperEVM. Pool and factory addresses are immutable; a new release means a new factory and a new set of pools. Retired deployments are never paused, only delisted, so positions in the v3.1 pools and in the v3.0 deployment on Monad stay withdrawable indefinitely.
  • Protocol semantics — every response carries protocolVersion. This matters more than the URL prefix here, because all three releases share the same /api/v1/ shapes while the numbers in them changed meaning twice: v3.1 moved fees from collateral to notional, and v3.2 raised the default rate from 30 to 50 bps. Assert the version rather than assuming it.
  • Event signatures — frozen for a given contract version. The topic0 hashes above are the canonical reference. v3.1 added signatures on top of v3.0 without changing existing ones, and v3.2 inherits v3.1's set unchanged.