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 prediction protocol on Monad mainnet (chainId 143). Each pool tracks one price symbol and lets users take a long or short position against other LPs in the same pool. Rebalances move 1% of the losing side's assets 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 rebalances on the MON-Native-10m pool
POOL=0xa9791f163002f6a63cc07aff8142095a9a858f6458a00cae00f7acad6482059b
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.

Pools

Registry of pool contracts deployed by the v2.1 factory and their metadata / config events.

GET/api/v1/pools

List every pool registered in the indexer.

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: long_total_assets, short_total_assets, long_total_shares, short_total_shares, total_assets, plus the block number / timestamp at which the read happened. 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.

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/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.

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.

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.

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 for a user.

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.

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.

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 v2.1 factory contract.

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 — Monad Mainnet (chainId 143)
  • RPChttps://rpc.monad.xyz
  • Explorer https://monad.xyz/address/<ADDRESS>

Core addresses

Factory v2.10x63072Cdc78874DaDA5A6053f07E636eF3aec8c57
PythPriceTracker0x5a3178d52C46d0aaCF81A7159F1598e871811aFD
USDC (collateral)0x754704Bc059F8C67012fEd69BC8A327a5aafb603

Live pools (10)

Two intervals (10m and 1h) per asset — pick the cadence that fits your strategy.

LabelTypeSymbolCollateralIntervalAddress
MON-Native-10m
Native
MONNATIVE10m
MON-Native-1h
Native
MONNATIVE1h
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: 143, name: "Monad", nativeCurrency: { name: "MON", symbol: "MON", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.monad.xyz"] } } },
  transport: http(),
});

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: "0x63072Cdc78874DaDA5A6053f07E636eF3aec8c57",
  abi: FACTORY_ABI,
  functionName: "getAllMarketIds",
});

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

Events

The six event topics you need to filter for if you're building an indexer, subgraph, or alerting bot. topic0 is the keccak256 of the canonical signature.

Topic0 reference

ScopeNameSignatureTopic0
Pool
DepositedDeposited(address,bool,uint256,uint256,uint256,uint256)
Pool
WithdrawnWithdrawn(address,uint256,uint256,uint256,uint256,uint256,uint256,uint256)
Pool
SwitchedSwitched(address,bool,uint256,uint256,uint256,uint256,uint256,uint256)
Pool
RebalancedRebalanced(uint256,bool,uint256)
Factory
MarketCreatedMarketCreated(bytes32,address,uint8,string,address,address,address,address,uint256)
Factory
MarketStatusUpdatedMarketStatusUpdated(bytes32,bool)

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. QuickNode subscribes to Block w/ Receipts on Monad mainnet.
  2. Each block runs through the filter (below) which keeps only the 6 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 Monad 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
// Forward every log whose topic0 matches a known v2.1 event.
const TOPICS = new Set([
  // Pool
  "0xc2cf288cd8c138e724d1e17ce848afd04e0d32e56883d38635d5a81523dbce02", // Deposited
  "0x06c514d6b519b3f249f6f8b7fc10573de80de5388bfc514bfb432616ee930be7", // Withdrawn
  "0x0945716462655c1f308382e972112dec530f9595b0ee214386dbb14adf618dea", // Switched
  "0x5ef5c3de2451b8e81ef437df0ed11d9c3e7fd1485531ad5dd1874f64df4d7d70", // Rebalanced
  // Factory
  "0xd782b4fbb126b84aa938ca9ef083461d682456398f5d1b1f4112c40a6376ee00", // 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 v2.1. Pool and factory addresses are immutable; a new release means a new factory and a new set of pools.
  • Event signatures — frozen for a given contract version. The topic0 hashes above are the canonical reference.