vike

Page through large reads

The four ways the /v1 routes page, a cursor, a cursor in an envelope, an offset and a moving time window, each walked in one script with its stop condition, and the on-chain routes that do not page at all.

There is no single paging scheme. Which one a route uses is on its endpoint page, and using the wrong one either repeats the first page or silently stops short. The recipe walks one route of each kind for three pages:

StyleExample routeMove to the next pageStop when
Cursor/liquidationspass nextCursor back as cursor, every other parameter unchangednextCursor is null, or a page comes back empty
Cursor in an envelope/settlements/bulkpass next_cursor back as cursor, every other parameter unchangedhas_more is false
Offset/archive/datasetsadd the rows received to offsetoffset + len(datasets) >= total
Time window/ohlcvrestart start one bar after the last candle you holda page shorter than limit

The cursor style is Hyperliquid's and HL Cohorts'; the envelope is Polymarket's, on /events as well as /settlements/bulk. The on-chain routes have no paging at all.

Run
export VIKE_API_KEY=...
python paging-large-reads.py             # real run: needs the key
python paging-large-reads.py --dry-run   # prints the first request of each walk, calls nothing

MAX_PAGES keeps the script to three pages per walk. Remove it, or raise it, for a real pull.

paging-large-reads.py
"""The four ways /v1 pages, each walked for a few pages: a cursor, a cursor in an envelope, an offset and a
moving time window.

Run:  VIKE_API_KEY=... python paging-large-reads.py    (--dry-run lists the requests and makes none)
A real walk drops MAX_PAGES and runs to the end.
"""
import os
import sys
from urllib.parse import urlencode

NAME = "paging-large-reads"
BASE = "https://data.vike.io"
DRY = "--dry-run" in sys.argv
MAX_PAGES = 3  # keeps the demo short; a real walk loops until the stop condition alone ends it
DAY_START = 1786924800000  # 2026-08-17T00:00:00Z in epoch ms
MINUTE_MS = 60_000


def fail(why):
    sys.exit(f"{NAME}: {why}")


def call(path, method="GET", **params):
    url = BASE + path + ("?" + urlencode(params) if params else "")
    if DRY:
        print(method, url)
        return None
    key = os.environ.get("VIKE_API_KEY") or fail("set VIKE_API_KEY")
    import requests

    r = requests.request(method, url, headers={"X-API-Key": key}, timeout=60)
    if r.status_code != 200:
        fail(f"{method} {path} answered HTTP {r.status_code}: {r.text[:200]}")
    return r.json()


def by_cursor():
    """HL liquidations: pass nextCursor back verbatim, with every other parameter unchanged, until it is null."""
    rows, cursor = 0, ""
    for _ in range(MAX_PAGES):
        params = dict(symbol="BTC", start="2026-08-17T00:00:00Z", end="2026-08-18T00:00:00Z", limit=500)
        if cursor:
            params["cursor"] = cursor
        page = call("/v1/hyperliquid/liquidations", **params)
        if page is None:
            return 0
        rows += len(page["liquidations"])
        cursor = page["nextCursor"]
        # A full last page still carries a cursor; the page after it is empty.
        if not cursor or not page["liquidations"]:
            break
    return rows


def by_envelope():
    """Polymarket settlements: pass next_cursor back as cursor while has_more is true."""
    rows, cursor = 0, ""
    for _ in range(MAX_PAGES):
        params = dict(slug_prefix="btc-updown-5m-", limit=1000)
        if cursor:
            params["cursor"] = cursor
        page = call("/v1/polymarket/settlements/bulk", **params)
        if page is None:
            return 0
        rows += len(page["settlements"])
        # has_more is exact: the last page says false and carries next_cursor null, with no empty page after it.
        if not page["has_more"]:
            break
        cursor = page["next_cursor"]
    return rows


def by_offset():
    """Polymarket archive index: offset += rows received, until offset reaches total."""
    rows, offset = 0, 0
    for _ in range(MAX_PAGES):
        page = call("/v1/polymarket/archive/datasets", stream="trades", limit=100, offset=offset)
        if page is None:
            return 0
        rows += len(page["datasets"])
        offset += len(page["datasets"])
        # total is the filtered count before limit and offset, the only field that says when to stop.
        if not page["datasets"] or offset >= page["total"]:
            break
    return rows


def by_window():
    """HL candles have no cursor: restart one bar after the last candle you hold."""
    rows, start = 0, DAY_START
    for _ in range(MAX_PAGES):
        page = call("/v1/hyperliquid/ohlcv", symbol="BTC", interval="1m", start=start,
                    end=DAY_START + 6 * 3_600_000, limit=100)
        if page is None:
            return 0
        candles = page["candles"]
        rows += len(candles)
        if len(candles) < 100:  # a short page is the last one
            break
        start = candles[-1]["ts"] + MINUTE_MS
    return rows


def main():
    counts = {"cursor": by_cursor(), "envelope": by_envelope(), "offset": by_offset(), "window": by_window()}
    if DRY:
        return
    for style, n in counts.items():
        if not n:
            fail(f"the {style} walk returned no rows")
        print(f"{style:8} paging: {n} rows over at most {MAX_PAGES} pages")


if __name__ == "__main__":
    try:
        main()
    except Exception as e:  # a shape the recipe does not expect fails by name, not by traceback
        fail(f"{type(e).__name__}: {e}")

Details that decide whether the walk is complete

  • A full last page still carries a cursor. /liquidations sets nextCursor whenever the page holds limit rows, so the last page of a result whose size is an exact multiple is followed by an empty one. Stop on either.
  • The envelope says when to stop. /settlements/bulk and /events answer {…, has_more, next_cursor}, and next_cursor is a string only while has_more is true: the last page says so itself, with no empty page after it. The cursor is a position (resolved_ts_ms:condition_id on /settlements/bulk, local_ts:seq:token_id on /events); the walk resumes strictly after it. /settlements/bulk takes up to 5000 rows a page, and a page takes a few seconds whatever its size, so a real walk asks for full pages.
  • total is the filtered count before paging. On the archive index it follows your filters, not the page, and it is the only field that says when to stop. limit defaults to 200 and is capped at 1000.
  • /ohlcv has no cursor. It returns one page of at most 5000 bars, oldest first, and start accepts epoch milliseconds, so the next start is the last candle's ts plus one bar.
  • Not every list route pages. /traders stops at 20 rows with no cursor, and /markets at 100 with no offset; for those, narrow the filter or use the richer route named on the page.
  • /leaderboard on Polymarket pages by offset, up to 1000, over a board rebuilt every hour, so a walk that spans a rebuild can repeat or skip a wallet.

A cursor records where to resume, not what you asked for

/liquidations builds its cursor from the last row's timestamp and wallet alone. The filters come from the request, so send the same symbol, start, end and limit with every page; the script does, and a page with different ones answers a different question from the position the cursor names.

On-chain: one page and a silent cap

No /v1/onchain route takes a cursor or an offset. Each returns one page, and nothing in the answer says it was cut. Two numbers bound it:

  • limit is checked against one ceiling and applied under another. Several routes accept a limit far above what they return: /wallets/{address}/transactions accepts up to 1000 and returns at most 500, and /wallets/{address}/counterparties accepts up to 1000 and returns at most 50. Each parameter list names both, as the route default and the applied cap.
  • A full page at the cap is not the whole answer. When the row count equals the applied cap, assume there is more. There is no second page to ask for: narrow the question instead, with a shorter window or fewer days, a higher min_usd, a direction, or one token_address, and make several smaller calls.

On this page