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. Package availability is registry-authoritative. The
0.4.0 exact-verification target is usable only when npm latest resolves to
it and exact-main deployment, registry publication, clean install, import, and
hosted public API smoke are complete. Use the same STARKSCAN_API_KEY and
hosted API base that REST and CLI 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 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/sdkFor unattended services, resolve latest immediately before pinning the
version:
SDK_VERSION="$(npm view @starkscan/sdk@latest version --registry https://registry.npmjs.org)"
npm install "@starkscan/sdk@${SDK_VERSION}"Release channels:
latest: registry-authoritative stable channel; resolve it before pinning and require the release evidence before relying on0.4.0verification methodsbeta: 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, paginate } 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.
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)),
);
// One block range, every page: paginate passes each nextCursor back
// unchanged; the range goes with every page.
for await (const tx of paginate((cursor) =>
starkscan.addressTransactions("0xwalletA", cursor, 100, {
fromBlock: 1_200_000,
toBlock: 1_250_000,
}),
)) {
console.log(tx.blockNumber, tx.txHash);
}
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 portfolio = await starkscan.walletPortfolio({
ownerAddress: wallets[0],
scope: 'discovered',
mode: 'require_complete',
limit: 25,
nftLimit: 25,
});
const completeWalletStates = await Promise.all(
wallets.map((wallet) =>
starkscan.walletStateComplete({
ownerAddress: wallet,
scope: 'discovered_plus_registry',
mode: 'require_complete',
limit: 25,
}),
),
);
const flows = await starkscan.tokenTransfers("0xtoken", {
addresses: wallets,
limit: 100,
});Use walletState for one bounded page. Its valuation has scope: 'page'; singlePageComplete says only that the walk fit in that first page. Raw completeForWallet is a deprecated compatibility alias and is not a portfolio-wide signal. Stop a raw walk at hasMore: false or nextCursor: null. Use walletStateComplete when you need one selected-scope portfolio valuation: it pins all continuation pages to the first block hash, rejects drift and duplicates, and returns scope: 'wallet' with completeForWallet: true only after exhausting the cursor. addressTokenHoldings remains in the SDK only as a deprecated source-compatibility method; the hosted route returns 410 Gone and no holdings data.
walletPortfolio keeps this to one network request while adding supported
Vesu, Ekubo, and Troves positions plus one indexed ERC-721 page. Read
defiPositions.coverage and nftPositions.coverage independently;
walletSafe applies only to the fungible page and NFT global completeness is
always false. Follow the fungible and NFT cursors independently.
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. When activityCoverage.reasonCode is success_only_total_not_materialized, use latestActivityBlock as the activity-exists signal and keep totalActivityCount hidden because it is null and activityCountExact=false. Never coerce unknown activity to zero. 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. Use the matching scope's servableFromBlock and maxServableToBlock, set certifiedFullRange: true, then verify rangeCoverage on each successful wide page. The flag bypasses only the SDK's ordinary 10,000-block guard; it does not replace Wallet-plan authorization or ready selector coverage. 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, request?) | SearchView | resolve a transaction hash, address, or block by query; { mode: 'typed' } also finds tokens by name or symbol in results |
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
When to choose another surface
- Choose the REST API when you need raw wire visibility.
- Choose the CLI when you need local shell workflows or exports.
- Choose the CLI when an agent works from a shell.
- Stay in the explorer when you are still verifying product behavior visually.
API key usage
Call await client.apiKeyUsage() for the authenticated workspace key's last 24
hours of recorded HTTP requests, failures, and rate-limit observations. Requires
read or prove scope. This account-level snapshot is independent of chainId; telemetry
is best effort, and the last observed rate limit is not a live quota balance.
The same response includes proofUsage for this key's mainnet proofs today
(UTC). Use proofUsage.succeededToday for successful completions, including
jobs submitted before midnight. failedToday, unavailableToday, and
unknownDeliveryToday are separate terminal outcomes; queued and running
show current unfinished work. These counters come from the durable job ledger.
admittedToday is the daily quota usage: every admitted job consumes one unit,
including failed jobs. Polling and idempotent replays do not consume another
unit. dailyLimit reflects the per-key override or relay default, and
remainingToday is the nonnegative difference. Both are null when relay
configuration is unavailable on the API instance. Remaining budget does not
guarantee admission: proving scope, relay availability, and concurrency limits
still apply. windowEnd is the next UTC midnight; observedAt is the snapshot
time. This daily window is separate from the HTTP telemetry's rolling 24 hours.