Starkscan

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/sdk

Exact pin for unattended services:

npm install @starkscan/[email protected]

Release channels:

  • latest: default public 0.1.2 release
  • beta: prerelease channel for explicit tests only
  • alpha: historical prerelease channel; use only when directed during rollback

Fallback artifact install

npm install ./starkscan-sdk-<version>.tgz

Use 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, and 504 are retryable; Retry-After is honored on 429
  • network failures are retryable when the request is retryable
  • non-idempotent writes are not retried; the bounded txDetails preview POST is the explicit retryable POST
  • redirects are rejected with RedirectError so 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 25 items and clamp limit to 1..100
  • live feed block_limit and tx_limit default to 10 and clamp to 1..25
  • block detail transaction previews clamp tx_limit to 1..200
  • token transfer and address-transfer filters accept at most 128 addresses
  • contract calldata arrays accept at most 1024 felt items
  • txDetails accepts at most 128 transaction 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:

MethodReturnsUse
transaction(txHash)TransactionDetailViewone transaction's full detail (receipt, logs, inline transfers)
transactionTrace(txHash)ContractTransactionTraceViewthe execution trace
addressSummary(address)AddressSummaryViewper-address aggregate (activity counts, first/last seen)
addressSummaries(addresses)AddressSummaryBatchViewadvanced-utility ordered aggregate facts for up to 128 known addresses
addressIntelligence(addresses)AddressIntelligenceBatchViewadvanced-utility ordered deployment, attribution, and inbound-funds facts for wallet/paymaster workflows
addressTransfers(address, request?)GlobalTransferPageaddress-scoped transfer pager (direction / token filters)
globalEvents(request?)GlobalEventPageindexed cross-contract event search by address/topic0 filters
contractMetadata(address)ContractMetadataViewindexed contract metadata (classHash, deploy info) without a live RPC call
contractEntrypoints(address)ContractEntrypointsViewcallable entrypoints — the companion to readContract
contractVerification(address)ContractVerificationViewverification status / source metadata
search(query)SearchViewresolve 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.

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 MCP when an agent needs tool-calling access.
  • Stay in the explorer when you are still verifying product behavior visually.

On this page