Starkscan

Migrate To Wallet State

Replace retired indexed token holdings with separate discovery and block-pinned wallet-state calls.

Migrate to wallet state

GET /v1/{chain}/address/{address}/token-holdings is retired. It returns HTTP 410 Gone, no holdings data, successor links, and this guide. POST /v1/{chain}/address/{address}/portfolio-live was removed.

The old routes mixed two different questions:

  1. Which token contracts might matter for this wallet?
  2. What is the token contract's authoritative balance now?

An index can answer the first question within a declared evidence scope. It cannot certify the second. Transfer-derived balances can retain phantom assets, miss non-standard changes, or look internally consistent while omitting tokens. Wallet integrations must keep discovery coverage separate from balance correctness.

New two-step contract

Use discovery when you do not already have a token list:

GET /v1/{chain}/address/{address}/assets/discovery?scope=discovered_plus_registry&limit=25

Discovery returns token candidates, evidence, a version-pinned cursor, and coverage. It returns no balance. Continue while hasMore=true. coverage.completeWithinScope describes the declared standard-fungible evidence scope only; coverage.globallyComplete remains false because non-standard or unregistered assets may be undiscoverable.

Then verify a bounded candidate page:

POST /v1/{chain}/query/wallet-state
Content-Type: application/json

{
  "ownerAddress": "0x...",
  "mode": "require_complete",
  "scope": "discovered_plus_registry",
  "limit": 25,
  "blockPreference": "latest_accepted_l2",
  "include": {
    "nonce": true,
    "classHash": true
  }
}

wallet-state resolves one immutable block hash, then reads every balance and optional account field from the dedicated RPC serving pool at that hash. Starkscan never substitutes an indexed balance and never interprets an error as zero.

The same response is the supported USD envelope for wallet integrations; there is no separate public batch-price route. Historical-main attempts a governed current-price refresh every five minutes, outside the request path, for 25 exact SN_MAIN contracts in two independent cohorts. The release covers STRK, ETH, native and bridged USDC, USDT, WBTC, both reviewed DAI contracts, canonical wstETH, xSTRK, xWBTC, BROTHER, LBTC, tBTC, strkBTC, eBTC, mRe7BTC, LINK, LORDS, EKUBO, NSTR, CASH, UNO, USN, and DOG. Contract address—not ticker—selects the mapping. Trust a valuation only when its price status is priced. Provider failures and assets outside the exact cohort remain explicit as price_stale, token_unmapped, or another typed unavailable reason—never as a zero-dollar balance. rETH, legacy wstETH, SolvBTC, xstrkBTC, and vSTRK remain unpriced until an address-specific conversion or identity proof is available.

xtBTC keeps its exact mapping for causal transaction-time history, but it is not in the recurring current-price cohort because its provider timestamp does not currently satisfy the 5,400-second freshness contract. Do not treat xtBTC as covered by the recurring current release; an unavailable current response remains explicit rather than weakening that bound.

Choose a scope

ScopeUse whenCoverage meaning
explicityour wallet already owns a reviewed token listcomplete only for the supplied list
discoveredyou want transfer-evidence candidatesbounded standard-fungible evidence
discovered_plus_registryyou want evidence plus reviewed known assetsbounded evidence plus the checked-in registry

For explicit, send tokenAddresses and do not send a cursor. For discovery scopes, omit tokenAddresses and pass the returned cursor when continuing.

Choose a correctness mode

  • require_complete is the wallet-home-screen default. If any requested value cannot be verified at the selected block, the request fails with HTTP 503 and Retry-After; it does not return a partial success body.
  • verified_partial returns successful values plus typed timeout, unsupported, or error statuses. It always sets partial=true and walletSafe=false when a value failed.

Only status="ok" with a non-null balanceRaw is a verified balance. Verified zeros are omitted from items and counted in verification.verifiedZeroHidden. An absent row caused by an error is never equivalent to zero.

walletSafe=true requires all three conditions:

  • every selected value was verified at block.blockHash;
  • the candidate page has no continuation;
  • discovery is complete within its declared scope.

It is not a claim of globally complete asset discovery.

Block preference

  • latest_accepted_l2 resolves the latest accepted Starknet block, then pins its hash.
  • l1_accepted resolves Starkscan's current L1-accepted Starknet watermark, then pins its hash.
  • explicit requires exactly one blockHash or non-negative blockNumber.

The response always echoes block.blockNumber, block.blockHash, and block.finalityStatus. Compare snapshots only when their block identity matches.

Pricing

Balances and prices have separate status. A non-zero asset with unavailable pricing remains visible. Its price.valueUsd is null, it increments valuation.unpricedNonZeroAssetCount, and it is excluded from valuation.totalUsd. It is never counted as zero dollars.

Raw wallet-state valuation is deliberately page-scoped. Every response says valuation.scope="page". Its total and counters describe only that response's items; valuation.singlePageComplete=true only for a discovered or discovered-plus-registry walk whose first page is also terminal. Caller-supplied scope="explicit" token subsets, responses with hasMore=true, and every continuation response set singlePageComplete=false. The raw completeForWallet field is a deprecated compatibility alias for singlePageComplete; it is not a wallet-wide guarantee. Stop a raw walk only when hasMore=false or nextCursor=null. Do not add page subtotals from snapshots at different blocks.

In particular, when hasMore=true, completeForWallet=false: continue with the opaque cursor or use walletStateComplete() rather than presenting that page's valuation as the wallet total.

For a complete portfolio within the selected discovery scope, use starkscan.walletStateComplete(...). The SDK starts at page one, pins every continuation to the first response's block hash, rejects cursor, coverage, or token-identity drift, and returns valuation.scope="wallet" only after the cursor is exhausted. The resulting excludedUnpricedAssets is safe to interpret for that complete selected-scope wallet snapshot.

Discovery coverage uses three different boundaries. coveredThroughBlock and targetThroughBlock describe the completed historical seed. tailObservedThroughBlock describes only the bounded standard ERC-20 transfer tail. It does not cover nonstandard token patterns, so globallyComplete=false with unsupported_nonstandard remains expected even when the tail matches the current finalized block.

Current wallet pricing and transaction-time pricing are separate planes. Wallet-state uses fresh coingecko_simple_price facts. Transaction and transfer views use causal hourly coingecko_market_chart_range facts at or before the transfer timestamp and never substitute today's quote. New transfers target the next five-minute materializer cycle; older 365-day coverage continues to fill in the background and remains typed pending or unavailable until a causal fact can be certified.

Capacity and billing

Phase 1 accepts one owner and at most 25 candidates per request. The 26-50 band remains unavailable until dedicated-pool and clean-window capacity certification plus a coordinated schema and client release. The partner is billed one bounded wallet-state operation. Starkscan still accounts for actual internal RPC work and enforces concurrency, timeout, and response-size limits.

SDK and CLI

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',
});

const completeState = await starkscan.walletStateComplete({
  ownerAddress: '0x...',
  mode: 'require_complete',
  scope: 'discovered_plus_registry',
  limit: 25,
});

if (completeState.valuation.excludedUnpricedAssets) {
  // totalUsd excludes one or more non-zero holdings across the complete walk.
}
starkscan wallet-asset-discovery 0xWALLET --limit 25
starkscan wallet-state 0xWALLET --mode require_complete --limit 25

The MCP replacements are wallet_asset_discovery and wallet_state. Installed clients should update to the coordinated SDK, CLI, and MCP release before the retirement deploy. If package-registry propagation is delayed, this page and the HTTP examples above remain the authoritative migration path.

On this page