TypeScript SDK
Use the first-party Starkscan TypeScript client when you want typed explorer reads in application code.
TypeScript SDK
Use the TypeScript SDK when you want typed Starkscan reads in application code without rebuilding the HTTP contract yourself.
Target label: stable. The npm latest tag resolves to 0.2.0, wraps
certified and beta REST routes, and has passed the public-client smoke against
https://api.starkscan.co. Use the same STARKSCAN_API_KEY and hosted API base
that REST, CLI, and MCP launcher flows use.
For npm package provenance, Socket links, and exact-version pinning rules, use Package trust.
Use this surface for
- frontend or backend TypeScript integrations
- typed access to the same public contract used by the explorer
- application code that should not hand-build routes, headers, or selector calldata
Try in app before you wire code
Use the live explorer when you want to see the same entities first:
- Contracts for deployment metadata, holders, and activity
- Transactions for detail pages and action labeling
- Watchlist for saved addresses and repeat analysis
Install from a package manager
npm install @starkscan/sdk
pnpm add @starkscan/sdk
bun add @starkscan/sdkExact pin for unattended services:
npm install @starkscan/[email protected]Release channels:
latest: default public0.2.0releasebeta: prerelease channel for explicit tests onlyalpha: historical prerelease channel; use only when directed during rollback
Fallback artifact install
npm install ./starkscan-sdk-<version>.tgzUse the tarball flow only when you need controlled distribution or release verification. Public npm publishing uses the same @starkscan/sdk package name and API surface, so you can switch install channels without changing your application code.
The SDK defaults to https://api.starkscan.co. Set STARKSCAN_BASE_URL only
when targeting preview or a self-hosted Starkscan host. The SDK keeps using the
normal /v1/* route paths under that configured base.
First successful client
import { createStarkscanClient } from "@starkscan/sdk";
const starkscan = createStarkscanClient({
apiKey: process.env.STARKSCAN_API_KEY!,
chainId: "SN_MAIN",
});
const status = await starkscan.status();
const block = await starkscan.block(1234);
const totalSupply = await starkscan.tokenTotalSupply("0xtoken");
const balance = await starkscan.tokenBalanceOf("0xtoken", "0xowner");Privacy Pool routes
The current SDK convenience methods cover the finalized public-flow snapshot
and prepared hourly points through privacyPoolTvl() and
privacyPoolTvlHourly(). The public OpenAPI also publishes beta status,
analytics, metric-bucket, metric-series, event, commitment, and nullifier
operations. Until dedicated convenience methods are added, use an
OpenAPI-generated client or direct authenticated fetch for those routes.
See Privacy Pool metrics and note evidence for copyable requests, freshness and availability gates, cursor handling, and the privacy boundary. Raw token amounts are strings; website USD values are presentation estimates rather than an API pricing contract.
Errors, retries, and response validation
The SDK validates Starkscan responses at the network boundary before returning typed objects. If the API returns invalid JSON, an empty body, a wrong envelope shape, or a malformed high-value payload, the client throws ResponseFormatError instead of blind-casting the response.
import {
AuthError,
RateLimitError,
RedirectError,
ResponseFormatError,
ResponseSizeError,
ServerError,
ValidationError,
createStarkscanClient,
} from "@starkscan/sdk";
const starkscan = createStarkscanClient({
apiKey: process.env.STARKSCAN_API_KEY!,
chainId: "SN_MAIN",
timeoutMs: 10_000,
maxResponseBytes: 8 * 1024 * 1024,
});
try {
await starkscan.addressActivity("0xwallet", undefined, 50);
} catch (error) {
if (error instanceof RateLimitError) {
console.log("retry after ms", error.retryAfterMs);
} else if (error instanceof AuthError) {
console.log("fix the API key, tier, or auth scope");
} else if (error instanceof ValidationError) {
console.log("fix the request shape");
} else if (error instanceof ServerError) {
console.log("transient server failure");
} else if (error instanceof RedirectError) {
console.log("unexpected redirect rejected before credentials moved");
} else if (
error instanceof ResponseFormatError ||
error instanceof ResponseSizeError
) {
console.log("bad or oversized API response");
}
}Retry behavior is intentionally narrow:
- retryable reads are attempted up to 3 times with bounded jitter
429,502,503, and504are retryable;Retry-Afteris honored on429- network failures are retryable when the request is retryable
- non-idempotent writes are not retried; the bounded
txDetailspreview POST is the explicit retryable POST - redirects are rejected with
RedirectErrorso credentials are not replayed across origins - caller aborts stay as aborts, while SDK timeouts become
HttpTimeoutError
The high-level client inherits timeoutMs, maxResponseBytes, custom fetchFn, and request-id settings from createStarkscanClient(...). The exported lower-level HTTP client also accepts per-call AbortSignal and timeout options for applications that need request-level cancellation.
Client-side bounds
The SDK clamps or rejects inputs before they reach the API:
- paginated reads default to
25items and clamplimitto1..100 - live feed
block_limitandtx_limitdefault to10and clamp to1..25 - block detail transaction previews clamp
tx_limitto1..200 - token transfer and address-transfer filters accept at most
128addresses - contract calldata arrays accept at most
1024felt items txDetailsaccepts at most128transaction hashes per batch
For cursor pagination, pass nextCursor back unchanged. nextCursor: null means the page is complete.
Complete token holder walks
The current high-level SDK does not expose a tokenHolders() convenience
method. For the partner holder route, use an OpenAPI-generated client from the
public API contract, or the exported low-level
createHttpClient() with a response validator generated from that contract.
Start GET /v1/{chain}/token/{token}/holders without a cursor and follow
nextCursor to null. Across every page, pin chainId, tokenAddress,
holderCount, holderBalanceTotalRaw, snapshot.generationId,
snapshot.asOfBlock, snapshot.asOfBlockHash, and snapshot.rowDigest.
Preserve the walk-specific snapshot.expiresAt while paging. The durable
generation identity is generationId, asOfBlockHash, and rowDigest.
The first page derives expiresAt from request time and the six-hour retention;
continuation pages reuse that deadline from the cursor.
An HTTP 400 invalid_request for an invalid or expired cursor means discard
the partial union and restart without a cursor. Normal page coverage leaves
completeness.truncated=false; require
certification.status === "certified" and completeness.exact === true for an
exact generation claim. starknet_rpc_balanceOf is bounded sample evidence,
not RPC enumeration of the holder population.
Block reads
Use block reads when your application starts from a block number or block hash and needs canonical block contents.
const block = await starkscan.block(8279910, 5);
const txs = await starkscan.blockTransactions(8279910, undefined, 25);
for (const item of txs.items) {
console.log(item.txIndex, item.txHash, item.finalityStatus);
}starkscan.block accepts a block number or block hash. blockTransactions requires a concrete block number; resolve a block hash with starkscan.block(...) first, then pass the returned blockNumber with nextCursor. The hosted SDK does not expose per-block event or receipt pages; use transaction detail reads after resolving the block transaction list.
Wallet monitoring workflow
const wallets = ["0xwalletA", "0xwalletB"];
const activity = await Promise.all(
wallets.map((wallet) => starkscan.addressActivity(wallet, undefined, 50)),
);
const transactions = await Promise.all(
wallets.map((wallet) => starkscan.addressTransactions(wallet, undefined, 50)),
);
const assetDiscovery = await Promise.all(
wallets.map((wallet) =>
starkscan.walletAssetDiscovery(wallet, {
scope: 'discovered_plus_registry',
limit: 25,
}),
),
);
const walletStates = await Promise.all(
wallets.map((wallet) =>
starkscan.walletState({
ownerAddress: wallet,
scope: 'discovered_plus_registry',
mode: 'require_complete',
limit: 25,
blockPreference: 'latest_accepted_l2',
}),
),
);
const flows = await starkscan.tokenTransfers("0xtoken", {
addresses: wallets,
limit: 100,
});Use that pattern when you are monitoring a handful of wallets and need recent activity, recent transactions, indexed asset discovery, current wallet state, and token-scoped inflow/outflow reads from the same client. addressTokenHoldings remains in the SDK only as a deprecated source-compatibility method; the hosted route returns 410 Gone and no holdings data. Use walletAssetDiscovery and walletState for every new and migrated wallet integration.
For the full external starter, including the shared env contract and the matching REST and CLI flows, use Monitor 10 wallets.
Partner address classification
Use the batch helpers when you already have a backend list of addresses and need indexed classification without issuing one request per address. These helpers call advanced-utility routes; use a utility or batch-scoped API key, and expect 403 Forbidden from standard keys.
const addresses = ["0xwalletA", "0xcontractB"];
const summaries = await starkscan.addressSummaries(addresses);
const intelligence = await starkscan.addressIntelligence(addresses);
for (const item of intelligence.items) {
console.log(
item.address,
item.label,
item.typeLabel,
item.isDeployed,
item.classHash,
item.classLabel,
item.hasReceivedFunds,
item.latestActivityBlock,
);
}addressSummaries returns indexed aggregate address facts such as activity counts, latest activity, account hint, class hash, and deployment metadata when available. addressIntelligence adds utility classification fields such as readable label/protocol, account-vs-contract typeLabel, reviewed class-family classLabel, isDeployed, and hasReceivedFunds. label is curated/token attribution; typeLabel is generic indexed account-kind evidence and classLabel describes a reviewed class family, so neither is a unique address name tag. These helpers are optimized for bounded batch classification: they do not run raw activity scans, deployment repair, or RPC calls on the request path, so treat activityCountExact=false as an inexact/unknown signal. Do not use the deprecated /api/v0/contracts-by-address route for name_tag lookups; use /v1/{chain}/address/{address}/attribution for one-off attribution and these batch helpers for bulk classification. Batches are capped at 128 unique addresses and preserve the request order after validation.
Standard token reads
const totalSupply = await starkscan.tokenTotalSupply("0x0123...");
const pendingSupply = await starkscan.tokenTotalSupply("0x0123...", "pending");
const balance = await starkscan.tokenBalanceOf("0x0123...", "0x0456...");
const transfers = await starkscan.tokenTransfers("0x0123...", {
addresses: ["0x0456...", "0x0789..."],
fromBlock: 7_800_000,
toBlock: 7_802_500,
});Transfer exports and incremental reads
const transfers = await starkscan.tokenTransfers("0x0123...", {
addresses: ["0xwalletA", "0xwalletB"],
fromBlock: 7_800_000,
toBlock: 7_801_000,
limit: 100,
});
for (const item of transfers.items) {
console.log(item.timestampIso, item.txHash, item.rawValue);
}Contract event indexers
const events = await starkscan.contractEvents("0xcontract", {
topics: [
["0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9"],
undefined,
undefined,
undefined,
["0xabc", "0xdef"],
],
fromBlock: 7_800_000,
toBlock: 7_800_500,
limit: 100,
});
for (const item of events.items) {
console.log(item.decodingStatus, item.eventName ?? item.topic0, item.blockNumber, item.txHash, item.logIndex, item.keys);
}Use contractEvents when you need the canonical paginated event stream for one contract before applying protocol-specific decoding. The topics matrix covers key positions zero through fifteen: values within one row are OR, populated rows are AND, and empty rows are wildcards. Any row after zero requires a non-empty topic0 row plus explicit numeric fromBlock and toBlock. Ordinary keys may span at most 10,000 blocks. A Wallet workspace may request a larger range only when /v1/meta/capabilities advertises ready coverage for the selected topic0 and anchor position. Then paginate with nextCursor.
Voyager /events migrations should replace p/lastPage loops with cursor/nextCursor loops:
async function* fetchContractEventWindow(address: string) {
let cursor: string | undefined;
do {
const page = await starkscan.contractEvents(address, {
cursor,
fromBlock: 7_800_000,
toBlock: 7_800_500,
limit: 100,
});
for (const item of page.items) {
yield {
name: item.eventName ?? null,
keys: item.keys,
data: item.data,
timestamp: Math.floor(Date.parse(item.timestampIso) / 1000),
blockNumber: item.blockNumber,
transactionHash: item.txHash,
transactionNumber: item.txIndex,
number: item.logIndex,
};
}
cursor = page.nextCursor ?? undefined;
} while (cursor);
}decodingStatus is server-certified: decoded has exact-schema decodedFields; name_only has an attributed name but no certified field layout; unknown has no attribution at the event's execution class. eventDecodingDegraded reports an operational attribution-lookup failure for the page. Use complete keys[] and data[] as authoritative raw evidence; topic0..topic3 are compatibility aliases. The SDK does not invoke request-time RPC, Voyager, ABI, or trace fallback.
For cross-contract selector scans, use globalEvents with repeated address/topic filters and cursor pagination:
const page = await starkscan.globalEvents({
addresses: ["0xcontractA", "0xcontractB"],
topics: [["0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9"]],
limit: 100,
});
for (const item of page.items) {
console.log(item.address, item.blockNumber, item.txHash, item.topic0);
}For topic0-only retained-history workflows, keep a selective filter and continue with nextCursor; do not use the SDK to simulate broad RPC block-window scans. Wallet workspaces may request larger later-position ranges only for ready selector coverage advertised by /v1/meta/capabilities. Starkscan does not support unfiltered whole-chain event exports, arbitrary event-data substring scans, or keys filters on this route.
Batch transaction hydration
const batch = await starkscan.txDetails(["0xabc...", "0xdef..."], {
logLimitPerTx: 32,
});
for (const tx of batch.items) {
console.log(
tx.txHash,
tx.blockNumber,
tx.logs.length,
tx.tokenTransfers.length,
);
}Use txDetails when you already have an ordered tx hash list and want bounded Starkscan transaction previews in one batch. Check logsTruncated and tokenTransfersTruncated before treating child arrays as exhaustive.
Prepared staking reads
Prepared staking is available through the exported createExplorerApi client:
import { createExplorerApi } from "@starkscan/sdk";
const explorer = createExplorerApi({
baseUrl: "https://api.starkscan.co",
apiKey: process.env.STARKSCAN_API_KEY!,
});
const summary = await explorer.getStakingSummary("SN_MAIN");
if (summary.coverage.status !== "prepared") {
throw new Error(`staking coverage: ${summary.coverage.reasonCode}`);
}
const validators = await explorer.getStakingValidators("SN_MAIN", undefined, 25);The same client exposes getStakingValidator, getStakingDelegators, getStakingActivity, and getStakingAddress. It validates the prepared source, typed coverage, decimal raw-amount strings, and call-path availability states. Read Prepared staking API before interpreting unavailable metrics, gaps, or truncated arrays.
More client methods
The client exposes the full public read surface. Beyond the examples above:
| Method | Returns | Use |
|---|---|---|
transaction(txHash) | TransactionDetailView | one transaction's full detail (receipt, logs, inline transfers) |
transactionTrace(txHash) | ContractTransactionTraceView | the execution trace |
addressSummary(address) | AddressSummaryView | per-address aggregate (activity counts, first/last seen) |
addressSummaries(addresses) | AddressSummaryBatchView | advanced-utility ordered aggregate facts for up to 128 known addresses |
addressIntelligence(addresses) | AddressIntelligenceBatchView | advanced-utility ordered deployment, attribution, and inbound-funds facts for wallet/paymaster workflows |
addressTransfers(address, request?) | GlobalTransferPage | address-scoped transfer pager (direction / token filters) |
globalEvents(request?) | GlobalEventPage | indexed cross-contract event search by address and positional key filters |
contractMetadata(address) | ContractMetadataView | indexed contract metadata (classHash, deploy info) without a live RPC call |
contractEntrypoints(address) | ContractEntrypointsView | callable entrypoints — the companion to readContract |
contractVerification(address) | ContractVerificationView | verification status / source metadata |
search(query) | SearchView | resolve a transaction hash, address, or block by query |
Generic contract reads
// discover callable selectors first, then read
const strkToken =
"0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d";
const entrypoints = await starkscan.contractEntrypoints(strkToken);
const nameEntrypoint = entrypoints.external.find((entry) => entry.name === "name");
if (!nameEntrypoint) {
throw new Error("name entrypoint not indexed for STRK");
}
const result = await starkscan.readContract(
strkToken,
nameEntrypoint.selector,
[], // calldata (felts)
"latest", // optional block tag
);Multiple chains from one client
const sepolia = starkscan.withChain("SN_SEPOLIA");
const status = await sepolia.status();withChain returns a new client bound to another chain; the original client is unchanged.
Why this is the recommended app path
It keeps:
- auth handling centralized
- route construction consistent with the live API
- typed responses aligned with explorer semantics
- request IDs available when you need to correlate app issues with backend logs