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.3.0

Published

TypeScript SDK for the Starkscan Starknet block explorer API.

Readme

@starkscan/sdk

TypeScript SDK for the Starkscan Starknet explorer API.

Release target: 0.3.0. This minor release makes address-intelligence activity counts nullable so unknown activity can no longer masquerade as zero. Use the untagged install only after the live latest verifier passes; pin 0.3.0 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.3.0 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. MCP uses a separate transport: POST https://api.starkscan.co/mcp on the API domain, or POST {appBaseUrl}/api/mcp on app-origin deployments. 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 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.

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 current high-level client does not expose a tokenHolders() convenience method. Use an OpenAPI-generated client from starkscan-openapi.yaml, or the exported low-level HTTP client with an application validator, so immutable snapshot fields are checked at the network boundary:

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

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

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

do {
  const page = await http.getJson(
    '/v1/SN_MAIN/token/0xtoken/holders',
    { cursor, limit: 100 },
    validateTokenHolderPage,
  );
  const identity = JSON.stringify([
    page.chainId,
    page.tokenAddress,
    page.snapshot.generationId,
    page.snapshot.asOfBlock,
    page.snapshot.asOfBlockHash,
    page.snapshot.rowDigest,
    page.holderCount,
    page.holderBalanceTotalRaw,
  ]);
  snapshotIdentity ??= identity;
  if (identity !== snapshotIdentity) throw new Error('holder generation changed');
  if (
    page.completeness.populationComplete !== true ||
    page.completeness.populationReasonCode !==
      'complete_canonical_transfer_coverage' ||
    page.certification.status !== 'certified' ||
    page.completeness.exact !== true
  ) {
    throw new Error('holder generation is not an exact complete population');
  }
  holders.push(...page.items);
  cursor = page.nextCursor ?? undefined;
} while (cursor);

validateTokenHolderPage should be generated from the public OpenAPI or enforce the same schema locally. 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. Normal pagination keeps truncated=false; first require completeness.populationComplete === true, then require certification.status === 'certified' and completeness.exact === true for an exact population claim. 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.

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.

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);

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.3.0 before relying on the new address-activity truth 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.