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.1.2, 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.1.2releasebeta: 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");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)),
);
const holdings = await Promise.all(
wallets.map((wallet) => starkscan.addressTokenHoldings(wallet)),
);
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, current holdings, and token-scoped inflow/outflow reads from the same client.
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.isDeployed,
item.classHash,
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, isDeployed, and hasReceivedFunds. 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", {
selector: "0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9",
fromBlock: 7_800_000,
toBlock: 7_800_500,
limit: 100,
});
for (const item of events.items) {
const keys = [item.topic0, item.topic1, item.topic2, item.topic3].filter(Boolean);
console.log(item.eventName ?? item.topic0, item.blockNumber, item.txHash, item.logIndex, keys);
}Use contractEvents when you need the canonical paginated event stream for one contract before applying protocol-specific decoding or position logic. Keep filters server-side first: contract address, exact topic0..topic3, block range, then cursor pagination 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.topic0, item.topic1, item.topic2, item.topic3].filter(Boolean),
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);
}eventName is attribution for display and routing. Keep protocol-critical decoding on raw topic* and data[] unless your protocol-specific decoded-events contract explicitly says otherwise.
For cross-contract selector scans, use globalEvents with repeated address/topic filters and cursor pagination:
const page = await starkscan.globalEvents({
addresses: ["0xcontractA", "0xcontractB"],
selectors: ["0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9"],
limit: 100,
});
for (const item of page.items) {
console.log(item.address, item.blockNumber, item.txHash, item.topic0);
}For full indexed-history workflows, keep a selective filter and continue with nextCursor; do not use the SDK to simulate broad RPC block-window scans. 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.
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/topic0 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