@starkscan/sdk
v0.3.0
Published
TypeScript SDK for the Starkscan Starknet block explorer API.
Maintainers
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/sdkExact 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_windowis a terminal provider policy result, whileprice_missingis 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
- npm package: https://www.npmjs.com/package/@starkscan/sdk
- SDK docs: https://starkscan.co/docs/sdk/typescript
- API docs: https://starkscan.co/docs/api
- Package trust: https://starkscan.co/docs/build/package-trust
- Machine-readable launch matrix: https://starkscan.co/public-client-surface-matrix.json
- Socket signal: https://socket.dev/npm/package/@starkscan/sdk
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 to0.3.0before 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.
