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:
Read pools, positions, history. No auth.
Factory + 10 pools. Addresses & ABIs.
Real-time event delivery via QuickNode.
Quickstart
Hit the API directly — no key, no signup.
# 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" | jqREST 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) andoffset. - Time format. ISO-8601 UTC.
from/tofilters 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.
/api/v1/healthApp 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.
/api/v1/poolsList 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.
/api/v1/pools/:poolIdFetch a single pool by its bytes32 id.
| Name | In | Type | Description |
|---|---|---|---|
| poolId* | path | 0x… (bytes32 pool id) | Hex pool id from the factory's `MarketCreated` event. |
/api/v1/pools/:poolId/rebalancesRebalance events for a single pool, newest first.
| Name | In | Type | Description |
|---|---|---|---|
| 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). |
/api/v1/pools/:poolId/snapshotsPeriodic 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.
| Name | In | Type | Description |
|---|---|---|---|
| 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 timestamp | Inclusive lower bound on the event timestamp. e.g. 2026-01-01T00:00:00Z |
| to | query | ISO timestamp | Inclusive upper bound on the event timestamp. e.g. 2026-12-31T23:59:59Z |
| order | query | "asc" | "desc" (default "asc") | Time ordering of returned rows. |
/api/v1/pools/:poolId/subpool-snapshotsPer-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>`.
| Name | In | Type | Description |
|---|---|---|---|
| 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 timestamp | Inclusive lower bound on the event timestamp. e.g. 2026-01-01T00:00:00Z |
| to | query | ISO timestamp | Inclusive upper bound on the event timestamp. e.g. 2026-12-31T23:59:59Z |
| order | query | "asc" | "desc" (default "asc") | Time ordering of returned rows. |
/api/v1/pools/:poolId/config-eventsConfig-change events for a pool (e.g. fees, listing toggle).
| Name | In | Type | Description |
|---|---|---|---|
| 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.
/api/v1/deposits/:userDeposits 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.
| Name | In | Type | Description |
|---|---|---|---|
| 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 timestamp | Inclusive lower bound on the event timestamp. e.g. 2026-01-01T00:00:00Z |
| to | query | ISO timestamp | Inclusive upper bound on the event timestamp. e.g. 2026-12-31T23:59:59Z |
/api/v1/withdrawals/:userWithdrawals 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.
| Name | In | Type | Description |
|---|---|---|---|
| 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 timestamp | Inclusive lower bound on the event timestamp. e.g. 2026-01-01T00:00:00Z |
| to | query | ISO timestamp | Inclusive upper bound on the event timestamp. e.g. 2026-12-31T23:59:59Z |
/api/v1/switches/:userLong↔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.
| Name | In | Type | Description |
|---|---|---|---|
| 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 timestamp | Inclusive lower bound on the event timestamp. e.g. 2026-01-01T00:00:00Z |
| to | query | ISO timestamp | Inclusive upper bound on the event timestamp. e.g. 2026-12-31T23:59:59Z |
/api/v1/leverage-adjustments/:userSame-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.
| Name | In | Type | Description |
|---|---|---|---|
| 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 timestamp | Inclusive lower bound on the event timestamp. e.g. 2026-01-01T00:00:00Z |
| to | query | ISO timestamp | Inclusive upper bound on the event timestamp. e.g. 2026-12-31T23:59:59Z |
/api/v1/deposit-records/:userConvenience: deposit count + first 50 records for a user (and optionally a pool).
| Name | In | Type | Description |
|---|---|---|---|
| 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.
/api/v1/user/history/:userUnified, 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`.
| Name | In | Type | Description |
|---|---|---|---|
| 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. |
/api/v1/user/stats/:userPer-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.
| Name | In | Type | Description |
|---|---|---|---|
| user* | path | 0x… (20-byte EVM address) | User address (case-insensitive; lower-cased server-side). |
/api/v1/user/pnl-history/:userPeriodic PnL snapshots for a user, optionally per pool.
| Name | In | Type | Description |
|---|---|---|---|
| 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. |
/api/v1/user/principal/:userPer-pool principal balance derived from deposit/withdraw history.
| Name | In | Type | Description |
|---|---|---|---|
| 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.
/api/v1/rebalancesAll rebalances across every pool, newest first.
| Name | In | Type | Description |
|---|---|---|---|
| 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 timestamp | Inclusive lower bound on the event timestamp. e.g. 2026-01-01T00:00:00Z |
| to | query | ISO timestamp | Inclusive upper bound on the event timestamp. e.g. 2026-12-31T23:59:59Z |
/api/v1/pool/historyAll pool_snapshots rows (across pools), oldest → newest by default.
| Name | In | Type | Description |
|---|---|---|---|
| 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 timestamp | Inclusive lower bound on the event timestamp. e.g. 2026-01-01T00:00:00Z |
| to | query | ISO timestamp | Inclusive upper bound on the event timestamp. e.g. 2026-12-31T23:59:59Z |
| order | query | "asc" | "desc" (default "asc") | Time ordering of returned rows. |
/api/v1/pool/snapshots/latestLatest 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.
| Name | In | Type | Description |
|---|---|---|---|
| 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. |
/api/v1/platform/eventsCombined 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.
| Name | In | Type | Description |
|---|---|---|---|
| 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). |
/api/v1/factory/eventsRaw 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.
| Name | In | Type | Description |
|---|---|---|---|
| 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.
/api/admin/indexer/healthIndexer process health (proxied from 127.0.0.1:3001).
/api/admin/indexer/statsIndexer stats: pool counts, byPool breakdown, last event.
/api/admin/indexer/refresh-registryRe-hydrate the in-memory pool registry from the factory contract via RPC.
| Name | In | Type | Description |
|---|---|---|---|
| X-Admin-Token* | header | string | Must match the indexer's ADMIN_TOKEN env var. |
/api/admin/indexer/snapshotsSnapshotter schedule + last-run summary (pool TVL + user PnL).
/api/admin/indexer/snapshots/runForce a one-shot snapshot run. Body: { kind: "pool" | "pnl" | "all" } (default "pool").
| Name | In | Type | Description |
|---|---|---|---|
| X-Admin-Token | header | string | Required only if ADMIN_TOKEN is set on the indexer process. |
/webhookQuickNode 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) - RPC —
https://rpc.hyperliquid.xyz/evm - Explorer —
https://hyperevmscan.io/address/<ADDRESS> - Native asset — HYPE (18 decimals)
Core addresses
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.
| Label | Type | Symbol | Collateral | Interval | Address |
|---|---|---|---|---|---|
| HYPE-Native-10m | Native | HYPE | NATIVE | 10m | |
| HYPE-Native-1h | Native | HYPE | NATIVE | 1h | |
| BTC-USDC-10m | Market | BTC | USDC | 10m | |
| BTC-USDC-1h | Market | BTC | USDC | 1h | |
| ETH-USDC-10m | Market | ETH | USDC | 10m | |
| ETH-USDC-1h | Market | ETH | USDC | 1h | |
| SOL-USDC-10m | Market | SOL | USDC | 10m | |
| SOL-USDC-1h | Market | SOL | USDC | 1h | |
| HYPE-USDC-10m | Market | HYPE | USDC | 10m | |
| HYPE-USDC-1h | Market | HYPE | USDC | 1h |
Pool actions
Every pool exposes the same four user actions. Native pools take msg.value; ERC20 pools require an approve first.
// 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.
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
| Scope | Name | Signature | Topic0 |
|---|---|---|---|
Pool | Deposited | Deposited(address,bool,uint256,uint256,uint256,uint256,uint256) | |
Pool | Withdrawn | Withdrawn(address,uint256,uint256,uint256,uint256,uint256,uint256,uint256) | |
Pool | Switched | Switched(address,bool,uint256,uint256,uint256,uint256,uint256,uint256,uint256) | |
Pool | LeverageAdjusted | LeverageAdjusted(address,bool,uint256,uint256,uint256,uint256,uint256,uint256) | |
Pool | Rebalanced | Rebalanced(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 | MarketCreated | MarketCreated(bytes32,address,uint8,string,address,address,address,address,uint256,uint256[]) | |
Factory | MarketStatusUpdated | MarketStatusUpdated(bytes32,bool) | |
Factory | OwnershipTransferred | OwnershipTransferred(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
- The provider subscribes to Block w/ Receipts on HyperEVM mainnet.
- Each block runs through the filter (below), which keeps only the known event topics.
- The payload is
POST-ed with HMAC-SHA256 signature to your URL. - You verify the signature, decode the events, and write to your store.
QuickNode setup
- Create a Stream → Network
HyperEVM Mainnet, DatasetBlock w/ Receipts, Start blockLatest. - Paste the filter below and use Test Filter to confirm logs come through.
- Destination →
POST https://your-endpoint/webhook. - Security → enable HMAC, save the secret as
QUICKNODE_WEBHOOK_SECRETin your env. - Retries → exponential backoff. Decode idempotently by
(txHash, logIndex).
// 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
{
"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.
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.2on 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.