npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@starkscan/sdk

v0.4.2

Published

TypeScript SDK for the Starkscan Starknet block explorer API.

Readme

@starkscan/sdk

TypeScript SDK for the Starkscan Starknet explorer API.

Release target: 0.4.2. This release adds the typed one-call wallet portfolio contract for fungible balances, Vesu/Ekubo/Troves positions, and indexed ERC-721 ownership while retaining the exact-verification methods. Use the untagged install only after the live latest verifier passes; pin 0.4.2 for reproducible installs after that promotion.

Install

npm install @starkscan/sdk

Exact pin for unattended services:

npm install @starkscan/[email protected]

Prerelease tags such as @beta are maintainer-directed test channels only. Normal users and agents should use the default package or the exact 0.4.2 pin above.

First request

import { createStarkscanClient } from '@starkscan/sdk';

const starkscan = createStarkscanClient({
  apiKey: process.env.STARKSCAN_API_KEY!,
  chainId: 'SN_MAIN',
});

const status = await starkscan.status();
console.log(status.chainId, status.headBlockNumber ?? status.latestIndexedBlockNumber);

The SDK defaults to https://api.starkscan.co, sends X-Starkscan-Api-Key, and uses the same REST contract as the CLI. Set STARKSCAN_BASE_URL only for preview or self-hosted Starkscan hosts, and map STARKSCAN_CHAIN to the SDK chainId option when you want env-driven clients.

The package is ESM-only and supports Node.js 18 or newer. CommonJS consumers should use dynamic import('@starkscan/sdk').

Why use it

  • Typed helpers over Starkscan REST routes.
  • Centralized API-key and request metadata handling.
  • Runtime response validation on high-level client calls.
  • Data-honesty helpers for pagination, exactness, lag, and truncation flags.
  • Chain-bound clients with withChain('SN_SEPOLIA').

Useful reads

const block = await starkscan.block(8279910, 5);
const txs = await starkscan.blockTransactions(8279910, undefined, 25);
const tx = await starkscan.transaction('0x...');
const activity = await starkscan.addressActivity('0x...');
const discovery = await starkscan.walletAssetDiscovery('0x...', {
  scope: 'discovered_plus_registry',
  limit: 25,
});
const walletState = await starkscan.walletState({
  ownerAddress: '0x...',
  mode: 'require_complete',
  scope: 'discovered_plus_registry',
  limit: 25,
  blockPreference: 'latest_accepted_l2',
});
const portfolio = await starkscan.walletPortfolio({
  ownerAddress: '0x...',
  mode: 'require_complete',
  scope: 'discovered',
  limit: 25,
  nftLimit: 25,
});
const privacyPool = await starkscan.privacyPoolTvl();
const transfers = await starkscan.tokenTransfers('0xtoken', {
  addresses: ['0xwalletA', '0xwalletB'],
  limit: 100,
});

For wallet-state responses, value a holding only when its balance status is ok, price.status === 'priced', price.reasonCode === 'fresh_exact_cached_price', and both USD fields are non-null. A partial diagnostic may retain a unit price for an unverified balance, but never a wallet value. The SDK rejects unknown or empty pricing reason codes. metadata.spamStatus === 'not_assessed' means no spam classification was performed; it is not a safety claim. See the Wallets guide for pagination, retirement, full-range event certification, and partner access.

walletPortfolio() requests fungible balances, supported DeFi positions, and one indexed ERC-721 page in one round trip. The fungible and NFT cursors are independent. walletSafe applies only to the selected fungible balance page; read defiPositions.coverage and nftPositions.coverage separately, and never treat nftPositions.coverage.globallyComplete as true.

privacyPoolTvl() returns finalized public deposit-minus-withdrawal accounting. Use exact decimal-string protectedAmountRaw values as the source of truth. valuation.totalUsd is deliberately null unless every asset has complete amount decoding, known decimals, and a fresh exact cached quote; the SDK does not fetch or guess missing prices. Require coverage.decodedMaterializationFresh === true for a certified partner total; false reports raw finalized flow events ahead of decoding and null means the deployment has no raw comparison filter configured.

Event decoding

contractEvents, globalEvents, transaction(...).logs, and block-detail event payloads expose the same server-certified event contract. Use complete keys and data as the authoritative raw payload (topic0 through topic3 are compatibility aliases) and render decodingStatus as one of decoded, name_only, or unknown. Typed decodedFields exists only for decoded; provenance and unavailable reason fields explain the other states. Page/detail eventDecodingDegraded signals an operational attribution-lookup failure, not an individual unknown event. The SDK validates this contract and does not call RPC, Voyager, ABI, or trace fallbacks on the request path.

const page = await starkscan.contractEvents('0xcontract', { limit: 100 });
for (const event of page.items) {
  console.log(event.decodingStatus, event.eventName, event.keys, event.data);
}

Data honesty helpers

Starkscan responses expose freshness, exactness, pagination, and truncation signals directly. Use helper functions instead of guessing whether a response is complete:

import { getLagBlocks, hasMorePages } from '@starkscan/sdk';

const discovery = await starkscan.walletAssetDiscovery('0x...', {
  scope: 'discovered_plus_registry',
  limit: 25,
});
const state = await starkscan.walletState({
  ownerAddress: '0x...',
  mode: 'require_complete',
  scope: 'discovered_plus_registry',
  limit: 25,
  blockPreference: 'latest_accepted_l2',
});
if (!state.walletSafe) throw new Error('wallet snapshot is not complete within scope');

if (hasMorePages(await starkscan.addressActivity('0x...'))) {
  console.log('Continue with nextCursor before summarizing totals');
}

const allCandidates = [];
let cursor: string | undefined;
let snapshotId: string | null | undefined;
do {
  const page = await starkscan.walletAssetDiscovery('0x...', {
    scope: 'discovered_plus_registry',
    cursor,
    limit: 25,
  });
  snapshotId ??= page.snapshotId;
  if (page.snapshotId !== snapshotId) throw new Error('discovery snapshot changed');
  allCandidates.push(...page.items);
  cursor = page.nextCursor ?? undefined;
} while (cursor);

const status = await starkscan.status();
console.log('index lag', getLagBlocks(status));

Complete token holder walks

The exported client validates holder responses at the network boundary, including the requested chain and canonical token address on every page. Responses for a different chain or token raise a non-retryable ResponseFormatError; their holder rows are not retained in its payload. Supported named/felt chain aliases resolve to canonical SN_MAIN or SN_SEPOLIA in holder requests and responses. Holder rows exclude the zero transfer endpoint. Zero-count pages cannot carry a positive total; a complete empty population has total "0", while a null unavailable total is not population proof. Initial requests start at rank 1 and each page rejects duplicate addresses. SDK cursor walkers must also retain generation identity, contiguous ranks, unique addresses and balance/address ordering across page boundaries; the browser enforces these checks before retaining a continuation.

Use getTokenHolderScreening for the bounded policy Top-N used by whale-screening workflows; use getTokenHolders for holder population pages, and make a complete-population claim only after the exact predicate below holds for the entire cursor walk.

Holder eligibility is independently typed. getTokenHolders serves only tokens in the explicit address-keyed producer policy registry and never builds a count or generation on the request path. For that enumeration method, unknown or terminal non-ERC20 targets return 404, while positive indexed fungible evidence without a current policy throws UnqualifiedTokenPolicyError for API 422 unqualified_token_policy. getTokenHolderScreening gates policy membership directly, so every unregistered identity throws the same 422 regardless of indexed discovery evidence. Its apiCode is unqualified_token_policy and retryable is false; do not retry it as a generation build. A token without a current certificate returns its rows as a non-exact 200 page. A token without a sealed generation returns the canonical unavailable 200 tuple; transient prepared-serving failures retain the retryable 503 lifecycle case.

The API's /v1/meta/capabilities policy registry exposes display name/symbol, netWorthEligible, holderScreeningEnabled, priorityTier, adapterQualificationRevision and sourceProvenance beside each canonical address. These are selection and scheduling facts, not a price guarantee or holder certificate. Use the returned holder generation's completeness and certification fields for those decisions. The SDK's holder methods do not silently fetch capabilities, RPC or explorer data on the request path.

import { createExplorerApi } from '@starkscan/sdk';

const explorer = createExplorerApi({
  baseUrl: 'https://api.starkscan.co',
  apiKey: process.env.STARKSCAN_API_KEY!,
});

function requireExactCompleteHolderSnapshot(
  page: Awaited<ReturnType<typeof explorer.getTokenHolders>>,
) {
  const { certification, completeness, snapshot } = page;
  if (
    snapshot.source !== 'sealed_finalized_holder_generation' ||
    snapshot.freshness !== 'finalized_generation'
  ) {
    throw new Error('holder walk is not bound to an immutable generation');
  }

  const now = Date.now();
  const maximumFutureSkewMs = 5 * 60_000;
  const updatedAt = Date.parse(snapshot.updatedAt);
  const expiresAt = Date.parse(snapshot.expiresAt);
  const checkedAt = certification.checkedAt ? Date.parse(certification.checkedAt) : Number.NaN;
  if (
    completeness.populationComplete !== true ||
    completeness.populationReasonCode !== 'complete_canonical_transfer_coverage' ||
    completeness.exact !== true ||
    completeness.truncated !== false ||
    completeness.reasonCode !== 'materialized_snapshot' ||
    certification.status !== 'certified' ||
    certification.validatedAgainst !== 'starknet_rpc_balanceOf' ||
    certification.reasonCode !== 'materialized_snapshot' ||
    !Number.isSafeInteger(snapshot.generationId) ||
    snapshot.generationId <= 0 ||
    !Number.isSafeInteger(snapshot.asOfBlock) ||
    snapshot.asOfBlock < 0 ||
    !/^0x[0-9a-f]+$/i.test(snapshot.asOfBlockHash) ||
    !/^sha256:[0-9a-f]{64}$/i.test(snapshot.rowDigest) ||
    !Number.isFinite(updatedAt) ||
    !Number.isFinite(expiresAt) ||
    !Number.isFinite(checkedAt) ||
    updatedAt > now + maximumFutureSkewMs ||
    checkedAt < updatedAt ||
    checkedAt > now + maximumFutureSkewMs ||
    expiresAt <= now
  ) {
    throw new Error('holder generation is not an exact complete population');
  }
  return snapshot;
}

let screeningCursor: string | undefined;
do {
  const page = await explorer.getTokenHolderScreening(
    'SN_MAIN',
    '0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d',
    screeningCursor,
    100,
  );
  // Even when nextCursor is null, this is a bounded Top-N projection.
  if (page.screening.populationComplete || page.screening.exact) {
    throw new Error('invalid screening response');
  }
  screeningCursor = page.nextCursor ?? undefined;
} while (screeningCursor);

const holders = [];
let cursor: string | undefined;
let snapshotIdentity: string | undefined;

do {
  const page = await explorer.getTokenHolders(
    'SN_MAIN',
    '0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d',
    cursor,
    100,
  );
  const snapshot = requireExactCompleteHolderSnapshot(page);
  const identity = JSON.stringify([
    page.chainId,
    page.tokenAddress,
    snapshot.generationId,
    snapshot.asOfBlock,
    snapshot.asOfBlockHash,
    snapshot.rowDigest,
    page.holderCount,
    page.holderBalanceTotalRaw,
  ]);
  snapshotIdentity ??= identity;
  if (identity !== snapshotIdentity) throw new Error('holder generation changed');
  holders.push(...page.items);
  cursor = page.nextCursor ?? undefined;
} while (cursor);

validateTokenHolderPage and validateTokenHolderScreeningPage are also exported for callers using the low-level HTTP client. Immutable generations expose generationId, asOfBlockHash, rowDigest, and expiresAt together. The default retention is six hours; the returned expiresAt is authoritative for the current cursor walk but is not part of the durable generation identity. Treat cursor HTTP 400 invalid_request as a restart from page one. An exact population claim requires the sealed finalized source with freshness='finalized_generation' and immutable IDs above, populationComplete=true with populationReasonCode='complete_canonical_transfer_coverage', truncated=false, both reason codes equal to materialized_snapshot, certification.status='certified' with validatedAgainst='starknet_rpc_balanceOf', a certification timestamp no earlier than publication, and an unexpired expiresAt. The SDK rejects an exact=true response that violates that predicate. A full cursor walk with populationComplete=false is only a complete walk of the published subset. RPC balanceOf certifies bounded samples only and cannot enumerate every holder.

The canonical cold unavailable holder 200 is the full tuple with snapshot.source=materialized_fungible_balances, snapshot.asOfBlock=null, holderCount=0, holderBalanceTotalRaw=null, no rows, nextCursor=null, and Cache-Control: no-store. It also carries certification.status=unavailable, but that status alone does not mean a generation is absent: a sealed generation may carry unavailable certification while still serving immutable rows with exact=false. It is not an empty population; do not cache it or turn it into a zero holder total. Trust a served generation as a complete population only when completeness.exact is true.

Historical USD coverage

Transfer historicalUsd values are transaction-time facts, not spot-price fallbacks. Lightweight list and preview endpoints can intentionally omit this enrichment, so historicalUsd can be null. When it is present, branch on the paired coverage fields:

  • priced / priced: a materialized transaction-time USD valuation.
  • unpriced / a typed reason: outside_history_window is a terminal provider policy result, while price_missing is an in-horizon repairable gap. Neither is an audit-grade transaction-time valuation.
  • pending / materialization_pending: an eligible transfer that has not yet produced a materialization fact.

Price fields can be null for unpriced and pending outcomes. Do not replace them with a current market quote when presenting an audit surface.

Public exact contract verification

Every active workspace API key includes the independent verify scope. The SDK exposes the complete asynchronous lifecycle; deterministic package creation and offline receipt authentication remain in the starkscan verify CLI.

import { createStarkscanClient } from '@starkscan/sdk';
import { readFile } from 'node:fs/promises';

const client = createStarkscanClient({
  apiKey: process.env.STARKSCAN_API_KEY,
  chainId: 'SN_MAIN',
});
const metadata = JSON.parse(await readFile('./contract.package.json', 'utf8'));
const archive = new Uint8Array(await readFile('./contract.source.tar.gz'));

const created = await client.createVerificationJob(
  metadata,
  'my-contract-release-2026-09-07',
);
await client.uploadVerificationSource(created.jobRef, archive);
const current = await client.verificationJob(created.jobRef);
if (current.state === 'succeeded') {
  const receipt = await client.verificationReceipt(created.jobRef);
  // Persist receipt, then authenticate it with `starkscan verify receipt-check`.
}

Create, upload, and cancel are never retried by the SDK. Reuse the same Idempotency-Key only to resolve an ambiguous create response. Uploads are capped at 64 MiB and must be the exact gzip bytes committed by Package V1. The policy-bound source portion remains capped at 10 MiB; the additional allowance is only for lockfile-bound offline dependency material. See the complete verification guide for target selectors, retention, terminal states, privacy, and proof limits.

Low-level HTTP client

Prefer createStarkscanClient() for application code. If you use HttpClient directly, generic calls such as getJson<T>(), postJson<T>(), or putJson<T>() return unknown unless you pass a JsonResponseValidator<T>. That keeps network-boundary validation explicit.

await http.getJson<MyEnvelope>(path, undefined, validateMyEnvelope);
await http.postJson<MyEnvelope>(path, body, validateMyEnvelope);
await http.putJson<MyEnvelope>(path, body, validateMyEnvelope);

putBytes() is available for bounded binary uploads and is deliberately never retried. A custom HttpClient must implement it before using uploadVerificationSource().

Trust and safety

Socket is an external package-risk signal, not a Starkscan security certificate. The public trust source is Starkscan docs because the canonical engineering repository is private. Package promotion also requires checked release scripts, packed-tarball smoke, npm Trusted Publishing/OIDC for CI publishes, and live API smoke proof.

When a release makes a runtime validator require a newly added response member, deploy the exact-main API first and prove that the member is always present on the live response, using null where the value is unknown. Publish the SDK only after that backend-first compatibility proof. This prevents a new SDK from rejecting responses from a supported Starkscan deployment during rollout.

Release channels

  • latest: stable channel; verify that it resolves to 0.4.2 before relying on the one-call wallet portfolio contract.
  • beta: prerelease channel for explicit tests only.
  • alpha: historical prerelease channel; use only when directed during rollback.

Server-side key tiers control route access. The package stays the same; entitlements do not.