Starkscan
Getting started

Pagination and cursors

Treat cursors as opaque and follow nextCursor — never build or parse a cursor by hand.

Pagination and cursors

List endpoints return an items array and, when more rows exist, a nextCursor.

The one rule

Treat nextCursor as opaque. Pass the value back unchanged to fetch the next page — but URL-encode it when placing it in the query string, since cursors can contain reserved characters (for example :). Never parse, construct, or mutate a cursor: the internal format differs across endpoints and can change without notice. Stop when nextCursor is absent or null.

# first page
curl -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/address/<address>/transactions?limit=25"

# next page: take nextCursor from the previous response and URL-encode it
# (it is opaque and may contain reserved characters such as : or +)
cursor="$(printf '%s' "$NEXT_CURSOR" | jq -sRr @uri)"
curl -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/address/<address>/transactions?limit=25&cursor=$cursor"

limit

limit caps how many rows a single response returns. Keep it modest on hosted APIs and page with nextCursor rather than requesting very large pages.

Coverage on wallet-asset discovery

assets/discovery uses a version-pinned cursor so a full walk never silently mixes two candidate universes. Keep the same snapshotId and scope on every page and follow nextCursor until it is null. If the discovery generation or reviewed registry changes during the walk, discard the partial result and restart from the first page. The authenticated walk expires five minutes after its first page so a stale snapshot cannot accumulate unbounded tail work. Finish the walk within that window; after a 400 invalid_cursor, restart without a cursor.

Discovery coverage is not balance correctness. coverage.completeWithinScope describes only the selected evidence scope, while globallyComplete=false and mayMissNonstandardUnregisteredAssets=true preserve the wider limitation. Call query/wallet-state to verify a bounded candidate page at one immutable block hash; do not treat discovery order or evidence as a balance.

Within one snapshot, curated known tokens appear before unknown tokens. Ordering is deterministic, and token-provided names and symbols do not confer known-token rank.

Immutable token-holder walks

GET /v1/{chain}/token/{token}/holders uses a retained immutable finalized generation so transfers during a long walk cannot move rows across page boundaries. Start without a cursor, pass each nextCursor back unchanged, and stop only at nextCursor=null.

Pin the complete response identity across every page: chainId, tokenAddress, holderCount, holderBalanceTotalRaw, snapshot.generationId, snapshot.asOfBlock, snapshot.asOfBlockHash, snapshot.rowDigest, and the walk-specific snapshot.expiresAt. The durable generation identity is generationId, asOfBlockHash, and rowDigest. expiresAt is the deadline for this cursor walk, not part of that identity. The terminal union is complete only when addresses are unique, ranks are contiguous from 1, and the number of rows equals holderCount. Rows are ordered by descending raw balance, then ascending canonical holder address as the deterministic tie-breaker.

Use the cursor before expiresAt. The first page derives that deadline from the request time and the six-hour default retention; operators can configure a different retention period, and each generation persists its selected value. Continuation pages reuse the deadline encoded in the cursor. The returned timestamp is authoritative for that walk. An unchanged active generation remains available. Malformed, old-version, cross-scope, mismatched, revoked, or expired cursors return HTTP 400 with an invalid_request error and restart guidance. Discard the partial union and restart from page one; the server never silently continues against a newer generation. A warming or temporarily unavailable first page returns retryable HTTP 503 with Retry-After; retry that same no-cursor request. A supplied continuation cursor whose generation is unavailable, revoked, or expired returns HTTP 400 with invalid_request: discard the partial union and restart from page one.

For this route, nextCursor describes page coverage and normal pagination does not set completeness.truncated=true. completeness.exact instead describes certification of the full generation. RPC balanceOf can validate bounded samples at asOfBlockHash, but Starknet RPC cannot enumerate the holder population.

On this page