Starkscan

API

How to call Starkscan REST—base URL, auth, lists, errors. Paths live in the API reference.

API

The explorer, SDK, and CLI share one REST contract. For every path and field, open the API reference or starkscan-openapi.yaml. This page is how to use the API: URL shape, keys, paging, mistakes we see in the wild.

Base URL

Hosted external lane:

  • Production base = https://api.starkscan.co
  • Optional override: STARKSCAN_BASE_URL=https://<custom-host>
  • Resources = /v1/... → example: https://api.starkscan.co/v1/SN_MAIN/status
  • Do not use the same-origin /v1/... explorer lane for external integrations; it is reserved for app traffic on Starkscan-hosted pages.

Explorer

Same objects as JSON: Dashboard · Transactions · Contracts · Watchlist

OpenAPI tags (map only)

The sidebar under API reference is the full list. This table orients you:

TagCovers
AccountSession-authenticated self-serve API-key lifecycle and usage
AddressesActivity, txs, holdings, aggregate views
BlocksBlock metadata and lists
ContractsClass, verification, reads
ReferenceGenerated OpenAPI artifacts and reference surfaces
SearchExplorer search
StatusChain status
TokensMetadata, supply, balances, transfers
TransactionsDetail, previews, related reads
UtilitiesHelpers; key tier may vary (Advanced utilities)

Protocol-domain surfaces are part of the public contract only when Starkscan documents them explicitly.

Tiers

TierMeaning
Official public APIDefault contract in /docs and Scalar
Advanced utilitiesSupported, often needs a broader key
PartnerPartner-only indexed analytics such as token-holder census

Start on the official tier. Add utilities only for batch jobs that truly need them.

Certification states

The OpenAPI reference includes x-starkscan-certification on each operation.

StateUse it how
certifiedSafe default for production client workflows. Current launch set: status, block read, timestamp-to-block, tx read, token total supply, and token balance-of.
betaUsable with named clients and explicit limits. Treat indexed lists, holder census, and protocol routes as beta until their reconciliation gates are complete.
experimentalPartner preview only. Do not depend on schema stability.
unsupportedNot for client use.

Correctness is the hard launch gate. For token reads, use block_tag=<block_number> or block_tag=<block_hash> when you need reproducible exactness; latest and pending are live moving state. If a route is slow but certified, use backoff, caching, or a lower quota. If a route is fast but not yet reconciled, keep it out of unattended production paths.

Self-serve account routes

/v1/me/* is the personal-workspace control plane behind the signed-in /api-key experience.

  • Auth with a Better Auth session, not X-Starkscan-Api-Key.
  • GET / HEAD / OPTIONS may use the hosted browser session cookie or a bearer session token.
  • POST / DELETE must use Authorization: Bearer <session_token>.
  • GET /v1/me/api-keys lists metadata only.
  • POST /v1/me/api-keys issues or rotates the default live read + batch + write key. Write execution remains separately gated; see Self-serve account routes.
  • DELETE /v1/me/api-keys/{public_id} revokes one key.
  • GET /v1/me/usage returns recent request activity, failures, and per-key aggregates.

See Self-serve account routes for the exact auth shape and examples.

First calls

If you do not have a key yet, create one from the hosted API keys page. One server-side integration may use its scoped key across RPC, REST, the TypeScript SDK, the CLI, and MCP; browser-direct integrations need a dedicated client key and must never share it with a server-side service. Do not use the same-origin explorer /v1/* lane for external integrations. If your software asks for a Starknet node URL, start with Starkscan RPC.

curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/status"
curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/token/0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d/total-supply?block_tag=latest"
curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/token/0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d/balance-of/0x259fec57cd26d27385cd8948d3693bbf26bed68ad54d7bdd1fdb901774ff0e8?block_tag=latest"

For deterministic checks, resolve a block first and pass it as block_tag:

curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/token/0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d/balance-of/0x259fec57cd26d27385cd8948d3693bbf26bed68ad54d7bdd1fdb901774ff0e8?block_tag=10000000"

Block reads

The safe block reads in the public spec come with concrete examples and defaults:

curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/blocks?limit=25"
curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/block/8717378?tx_limit=50"
curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/block/8717378/txs?limit=25"

Wallet loop

curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/address/0x259fec57cd26d27385cd8948d3693bbf26bed68ad54d7bdd1fdb901774ff0e8/activity?limit=50"
curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/address/0x259fec57cd26d27385cd8948d3693bbf26bed68ad54d7bdd1fdb901774ff0e8/transactions?limit=50"
curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/address/0x259fec57cd26d27385cd8948d3693bbf26bed68ad54d7bdd1fdb901774ff0e8/token-holdings"

Filter transfers to several wallets—repeat address=:

curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/token/0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d/transfers?address=0x259fec57cd26d27385cd8948d3693bbf26bed68ad54d7bdd1fdb901774ff0e8&address=0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a&limit=100"

One POST for many summaries (utility key tier):

curl -X POST \
  -H "Content-Type: application/json" \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  -d '{"addresses":["0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a","0x259fec57cd26d27385cd8948d3693bbf26bed68ad54d7bdd1fdb901774ff0e8"]}' \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/address/summaries"

Readable attribution for one address:

curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/address/0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a/attribution"

Batch deployment, attribution, and inbound-funds flags:

curl -X POST \
  -H "Content-Type: application/json" \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  -d '{"addresses":["0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a","0x259fec57cd26d27385cd8948d3693bbf26bed68ad54d7bdd1fdb901774ff0e8"]}' \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/address/intelligence"

Full scripted loop: Monitor 10 wallets.

Agent pitfalls

  • POST …/tx/previews → body {"hashes":[...]}
  • POST …/address/summaries → body {"addresses":[...]}
  • POST …/address/intelligence → body {"addresses":[...]}
  • GET …/address/{address}/attribution → readable alias and indexed deployment/token metadata for one address
  • GET …/contract/.../read → Starknet selector, not a Cairo name
  • GET …/verification 404 → no record yet, not a missing route
  • 403 on a documented utility → key scope, not a typo in X-Starkscan-Api-Key

Token reads

  • balance-of — you know the contract address.
  • token-holdings — you care about the wallet; symbols like USDC can point at more than one live contract.
  • Big screens: avoid N×balance-of; use holdings.
  • Gate on holdings only when completeness.complete=true; legacy clients should also require exact=true, truncated=false, and completeness.reasonCode="complete".
  • GET /v1/{chain}/token/{token}/holders is a partner-tier indexed holder census for token-contract-first analytics. It is not a substitute for transfer-history replay and is intentionally outside the default read-key lane.

Partner holder example:

curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/token/0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d/holders?limit=50"

Cross-layer messages, events, and token facts

Beyond the core reads, these public (read-tier) routes are easy to miss — full request/response schemas are in the API reference:

Cross-layer messages (L1 ↔ L2)

  • GET /v1/{chain}/messages — paginated canonical cross-layer messages
  • GET /v1/{chain}/contract/{address}/messages — canonical messages for one contract
  • GET /v1/{chain}/contract/{address}/bridge-signals — L2 bridge-signal activity for one contract

Indexed events

  • GET /v1/{chain}/events — paginated, API-decoded events with optional address, positional topic0..topic15, and block filters
  • GET /v1/{chain}/contract/{address}/events — contract-scoped event search with the same positional topic0..topic15 contract

Copy-paste global event search:

curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/events?selector=0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9&address=0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d&from_block=10630000&to_block=10630325&limit=100"

Copy-paste contract-scoped event search:

curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/contract/0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d/events?selector=0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9&topic1=0x5983efa05a23ecc4eb29d8717f86b34412964ea152d6a499ea447fcf5f5ee39&topic2=0x1176a1bd84444c89232ec27754698e5d2e7e1a7f1539f12027f28b23ec9f3d8&from_block=10630000&to_block=10630325&limit=100"

Event filters are exact-position matches on both routes. topic0..topic15 map to key positions zero through fifteen; repeated values at one position are OR, populated positions are AND, and omitted positions are wildcards. selector aliases topic0. The contract route also keeps key/keys as sequential singleton compatibility inputs; the global route rejects those aliases because they do not identify a position. Each position is capped at 128 distinct felts, each request at 256 total, pages at 100. Any topic1..topic15 filter requires topic0 plus explicit numeric from_block and to_block bounds spanning at most 10,000 blocks inclusive. An address alone is not a topic0 anchor. Topic0-only searches may page the retained history; full-history later-position search is not yet provided.

Voyager /events migration

Voyager-style indexers usually call GET /events?contract={market}&p={page}&ps=100 and stop at lastPage. Starkscan uses the indexed contract-events route instead:

curl \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/contract/$MARKET_ADDRESS/events?from_block=$FROM_BLOCK&to_block=$TO_BLOCK&limit=100"

Keep calling the same route until nextCursor is null. Treat nextCursor as opaque and URL-encode it before putting it in the query string:

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/contract/$MARKET_ADDRESS/events?from_block=$FROM_BLOCK&to_block=$TO_BLOCK&limit=100&cursor=$cursor"

Response shape is { items, nextCursor, eventDecodingDegraded }, not { items, lastPage }.

Field mapping for Voyager adapters:

Voyager fieldStarkscan field
itemsitems
lastPagestop when nextCursor is null
keyskeys (the complete canonical key array; topic0..topic3 are compatibility aliases)
nameeventName when attributed, otherwise null
selectortopic0
transactionHashtxHash
transactionNumbertxIndex
numberlogIndex
timestampUnix seconds from timestampIso
datadata

Event decoding contract

Contract, global, block-detail, and transaction-detail event views use one server-certified contract. data[] is authoritative and keys[] is the canonical indexed key array; legacy rows without payload.keys reconstruct only topic0..topic3, so their keys[] may be incomplete. decodedFields is emitted only when an exact materialized ABI schema or a reviewed standard selector-and-arity schema consumes the full payload; its source/reason fields carry provenance.

  • decoded: typed decodedFields are certified for the payload.
  • name_only: the event name is attributed, but typed fields are not certified.
  • unknown: no attribution is available at the event's execution class.

eventDecodingDegraded: true means only that the optional attribution lookup failed for this response, not that an individual event is unknown. Starkscan does not call RPC, Voyager, class-ABI, or trace sources at request time to fill event fields. The web UI consumes the API result directly.

Fully decoded response:

{"items":[{"blockNumber":10630025,"timestampIso":"2026-07-15T12:00:00Z","txHash":"0xabc","txIndex":4,"logIndex":1,"address":"0xcontract","keys":["0xtransfer","0xfrom","0xto"],"topic0":"0xtransfer","topic1":"0xfrom","topic2":"0xto","topic3":null,"data":["0x1","0x0"],"decodingStatus":"decoded","eventName":"Transfer","eventNameSource":"class_abi","decodedFields":[{"label":"amount","type":"core::integer::u256","source":"data","kind":"u256","status":"decoded","rawValues":["0x1","0x0"],"originIndexes":[0,1],"displayValue":"1","addressValue":null,"numericValue":"1","textValue":null,"boolValue":null}],"decodedFieldsSource":"class_abi"}],"nextCursor":null,"eventDecodingDegraded":false}

Name-only response:

{"items":[{"blockNumber":10630024,"timestampIso":"2026-07-15T11:59:00Z","txHash":"0xdef","txIndex":3,"logIndex":0,"address":"0xcontract","keys":["0xapproval","0xowner"],"topic0":"0xapproval","topic1":"0xowner","topic2":null,"topic3":null,"data":["0x10","0x0"],"decodingStatus":"name_only","eventName":"Approval","eventNameSource":"selector_unique","decodedFieldsUnavailableReason":"selector_only_attribution"}],"nextCursor":null,"eventDecodingDegraded":false}

Unknown response:

{"items":[{"blockNumber":10630023,"timestampIso":"2026-07-15T11:58:00Z","txHash":"0x123","txIndex":2,"logIndex":7,"address":"0xcontract","keys":["0xunmatched","0x7"],"topic0":"0xunmatched","topic1":"0x7","topic2":null,"topic3":null,"data":["0xbeef"],"decodingStatus":"unknown"}],"nextCursor":null,"eventDecodingDegraded":false}

Token facts

  • GET /v1/{chain}/token/{token}/controls — indexed token control facts

GET /v1/{chain}/token/{token}/markets/pools (DEX pool facts) and GET /v1/{chain}/token/{token}/holders/analytics (holder-concentration analytics) also exist but require a partner-tier key — read-tier keys receive 403.

Privacy Pool public data

The beta external surface includes status, analytics, metric-buckets, metrics/series, events, commitments, and nullifiers. See Privacy Pool metrics and note evidence for copyable requests, response gates, cursor handling, and intentional exclusions.

GET /v1/{chain}/privacy-pool/tvl is the current finalized snapshot. GET /v1/{chain}/privacy-pool/tvl/series returns prepared hourly points, not a certified continuous history. Both are beta and can be called directly over REST or through the current Starkscan SDK.

Server-to-server clients normally omit Origin. Browser clients may call external api.starkscan.co/v1 or app-host /api/v1 REST routes from arbitrary origins by sending the API key in X-Starkscan-Api-Key or X-Api-Key; responses use wildcard, non-cookie CORS (Access-Control-Allow-Origin: * without Access-Control-Allow-Credentials). Both preflights and actual requests carrying bearer, cookie, internal, or legacy credentials are excluded from that wildcard lane and remain subject to Starkscan's strict origin allowlist. The public edge also strips caller-supplied internal keys as credential sanitization; stripping does not move those requests into the wildcard lane. MCP, OAuth, WSS, and self-serve session requests remain restricted to Starkscan-configured origins. Never embed a shared production key in a public frontend bundle; use a client-specific key with bounded scope and rate limits.

curl --fail-with-body --silent --show-error \
  -D /tmp/starkscan-tvl.headers \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/privacy-pool/tvl" \
  -o /tmp/starkscan-tvl.json

Keep the returned weak ETag and revalidate it. If-None-Match takes precedence over If-Modified-Since; a matching request returns 304 with no body:

etag="$(awk 'BEGIN{IGNORECASE=1} /^etag:/ {sub(/^[^:]+:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit}' /tmp/starkscan-tvl.headers)"
curl --fail-with-body --silent --show-error \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  -H "If-None-Match: $etag" \
  "${STARKSCAN_BASE_URL:-https://api.starkscan.co}/v1/SN_MAIN/privacy-pool/tvl" \
  -o /dev/null -w '%{http_code}\n'

Raw decimal-string token amounts are authoritative. coverage.latestEventCursor is the exact block:tx:log public-flow watermark for current-snapshot same-block reconciliation. coverage.finalizedOnly=true means starkscan_indexed_finalized_tier, not L1 settlement; call the snapshot L1-accepted only when coverage.asOfL1Accepted=true. The L1 fields are null when indexed acceptance evidence is unavailable. The request path never calls RPC, another explorer, or a price provider. Schema-v1 valuation fields are legacy compatibility fields, not a pricing contract for new integrations.

Hourly pages require inclusive, UTC-hour-aligned from and to RFC3339 timestamps plus granularity=hour. They return at most 24 oldest-first points; pass nextCursor unchanged until it is null. An empty items[] page or sparse UTC-hour sequence is unavailable history, not a zero-value point or complete-range assertion; fail closed. The current page shape has no page-level coverage or freshness watermark, so do not synthesize missing hours from RPC, another explorer, or request-time pricing. Hourly assets include nonzero public-flow amounts and amount-incomplete degraded entries; known zero amounts are omitted until reactivated.

Keyed api.starkscan.co responses remain private. The trusted same-origin /v1 lane may use public caching but is reserved for Starkscan app traffic. ETag is authoritative. On the current snapshot, Last-Modified is the current process cache-generation time, so a cold process may conservatively return 200; on an hourly page, it is the newest materialization timestamp in that page.

Every request

  • For data-plane routes under /v1/{chain}/*, send X-Starkscan-Api-Key; browser-direct REST clients may use X-Api-Key.
  • The explorer's same-origin /v1 lane is trusted and distinct from the external API-host /v1 and app-host /api/v1 lanes.
  • Never paste full API keys into chats, tickets, screenshots, or PR comments. Redact them like mzk_REDACTED_KEY.
  • For workspace-control routes under /v1/me/*, use Better Auth sessions for your personal Starkscan workspace: cookie auth for safe reads, bearer token required for POST / DELETE.
  • Log X-Request-Id when you open a support thread.
  • Expect client/server failures as JSON with code, message, docSlug, and requestId. The X-Request-Id header is canonical.
  • Log X-Starkscan-Route-Class when present so agents can back off by class.
  • Treat 503 as retryable when it indicates temporary unavailability, and honor Retry-After when present.
  • Sanity-check a host with npx -y @starkscan/cli doctor (base URL, auth, reachability, hosted MCP), plus a status call and a deliberately bad key to confirm 401.

503 example

HTTP/1.1 503 Service Unavailable
Retry-After: 5
X-Request-Id: mzk-...
Content-Type: application/json; charset=utf-8

{"code":"service_unavailable","message":"Temporarily unavailable","docSlug":"api/retry","requestId":"mzk-..."}

Honor Retry-After. Do not spin in a tight loop.

401 / 403 examples

HTTP/1.1 401 Unauthorized
X-Request-Id: mzk-...
WWW-Authenticate: Bearer realm="starkscan", error="invalid_token"
Content-Type: application/json; charset=utf-8

{"code":"unauthorized","message":"Unauthorized","docSlug":"api/auth","requestId":"mzk-..."}
HTTP/1.1 403 Forbidden
X-Request-Id: mzk-...
WWW-Authenticate: Bearer realm="starkscan", error="insufficient_scope", scope="batch"
Content-Type: application/json; charset=utf-8

{"code":"forbidden","message":"Forbidden","docSlug":"api/auth","requestId":"mzk-..."}

Not REST?

NeedDoc
Typed TSSDK
ShellCLI
MCPMCP quickstart
Starknet JSON-RPC providerStarkscan RPC
Click-onlyExplorer

Documented elsewhere on purpose

On this page