Starkscan

Privacy Pool metrics and note evidence

Integrate public activity counters, prepared series, events, commitments, and nullifiers without inferring private state.

Privacy Pool metrics and note evidence

Use these beta routes when an application needs the public metrics shown on Starkscan's Privacy Pool page. The routes read indexed or prepared serving tables. They do not call Starknet RPC, reconstruct history, fetch external prices, or link private notes on the request path. The private-swaps route does read indexed swap-time USD values for volumeUsd and reports observed swaps without those indexed values in unpricedSwapCount.

For finalized per-token public-flow amounts and prepared hourly amount points, see Privacy Pool data API.

Route map

NeedRouteImportant meaning
Lifetime counters and freshnessGET /v1/{chain}/privacy-pool/statusPublic event and note-fact counters at the latest decoded cursor.
Daily activityGET /v1/{chain}/privacy-pool/metric-bucketsPrepared daily deposits, withdrawals, note-use events, and viewing-key registrations.
Chart seriesGET /v1/{chain}/privacy-pool/metrics/seriesPrepared wallet-growth, token public-flow, and privacy-fee series.
Aggregate analysisGET /v1/{chain}/privacy-pool/analyticsEvent, visibility, token-flow, attribution, and typed unavailable-metric summaries.
Observed private swapsGET /v1/{chain}/privacy-pool/private-swapsIndexed matched-swap count and USD volume by window, venue, token pair, and day; historical source completeness is unproven.
Public event evidenceGET /v1/{chain}/privacy-pool/eventsCursor-paginated raw keys/data plus decoded public fields.
Commitment evidenceGET /v1/{chain}/privacy-pool/commitmentsPublic commitment facts. No owner or spend inference.
Nullifier evidenceGET /v1/{chain}/privacy-pool/nullifiersPublic nullifier facts. No commitment linkage.

All eight operations are published in the interactive API reference and starkscan-openapi.yaml with x-starkscan-certification: beta. Generated clients can use the operation IDs getPrivacyPoolStatus, listPrivacyPoolMetricBuckets, getPrivacyPoolMetricSeries, getPrivacyPoolAnalytics, getPrivacyPoolPrivateSwaps, listPrivacyPoolEvents, listPrivacyPoolCommitments, and listPrivacyPoolNullifiers.

Set the API host

External integrations use the API host, not the explorer's same-origin /v1/* proxy.

export STARKSCAN_BASE_URL=https://api.starkscan.co
export STARKSCAN_API_KEY=<your-read-key>
export STARKSCAN_CHAIN=SN_MAIN

Send X-Starkscan-Api-Key on every request. Log X-Request-Id when a request fails, respect Retry-After, and treat compatible additions to these beta response objects as possible.

What is public and what remains private

Deposits and withdrawals are public on-chain transfers. When the event shape exposes them, /events can return the public depositor in actorAddress, the public withdrawal recipient in toAddress, the token, and amountRaw. Do not describe those public fields as hidden.

The privacy boundary is unlinkability. Starkscan does not link a deposit to a withdrawal, link a spent note to the note that created it, reveal a note's private balance or owner, compute an anonymity set, or turn opaque commitment and nullifier hashes into a private relationship.

Read lifetime counters safely

curl --fail-with-body --silent --show-error \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "$STARKSCAN_BASE_URL/v1/$STARKSCAN_CHAIN/privacy-pool/status"

The response includes lifetime public-event counters such as depositCount, withdrawalCount, openNoteCreatedCount, encNoteCreatedCount, noteUsedCount, and viewingKeySetCount. noteState provides public commitment/nullifier proxies.

viewingKeySetCount is the number of public viewing_key_set event rows. registeredUserCount is the distinct count of public addresses that registered a viewing key. They answer different questions and can differ.

Before publishing a current total, inspect:

  • latestRawEventCursor and latestDecodedEventCursor;
  • eventLagBlocks; and
  • materializationFresh.

When materializationFresh is not true, describe the data as indexing or unavailable. Do not silently present a lagging counter as current.

The note-state fields are deliberately narrow:

FieldMeaning
commitmentCountPublic commitment facts indexed by Starkscan.
nullifierCountPublic nullifier facts indexed by Starkscan.
spentNoteProxyCountPublic proxy equal to the nullifier count.
unspentNoteProxyCountCommitments minus nullifiers, floored at zero.

These fields are not private balances, unique-user counts, ownership claims, or an exact anonymity set. One transaction can emit more than one note-related event.

Availability vocabulary

Different routes expose the smallest status vocabulary their data can prove:

FieldValuesPublication rule
TVL top-level statuscomplete, degraded, unavailablePublish only complete.
TVL coverage.statuscomplete, partial, unavailablePublish only complete; partial is not a zero.
Private-swaps coverage.statusobserved_onlyPublish only with an explicit observed-only label; never call it a complete total.
Event publicFields.visibilitypublic, partial, hidden_by_designUse only the fields actually present.

The metric-bucket and metric-series routes are prepared chart reads and do not carry an independent continuity certificate. Check /status freshness, retain each item's latestBlockNumber, and do not infer missing buckets.

Build daily activity cards

Use prepared buckets instead of paging the event history.

curl --fail-with-body --silent --show-error \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "$STARKSCAN_BASE_URL/v1/$STARKSCAN_CHAIN/privacy-pool/metric-buckets?limit=30"

The response is chronological and contains at most 90 daily buckets. Each item includes:

  • newViewingKeyWallets;
  • cumulativeViewingKeyWallets;
  • visibleDeposits;
  • visibleWithdrawals;
  • noteUsedEvents; and
  • latestBlockNumber.

These are public product-activity counts, not token amounts or accounting history. Today's bucket is still moving; use latestBlockNumber when recording a snapshot.

Build prepared charts

TOKEN_ADDRESS=0x...

curl --fail-with-body --silent --show-error --get \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  --data-urlencode 'range=30d' \
  --data-urlencode 'granularity=day' \
  --data-urlencode "token=$TOKEN_ADDRESS" \
  "$STARKSCAN_BASE_URL/v1/$STARKSCAN_CHAIN/privacy-pool/metrics/series"

Supported ranges are 24h, 7d, 30d, and all. Supported granularities are hour, day, and week. The response stays bounded at 720 points per series.

  • walletSeries contains viewing-key wallet growth.
  • shieldedSupplySeries contains deposited, withdrawn, and net public-flow raw amounts for the requested token.
  • privacyFeeSeries contains public fee-transfer amounts and counts.

Pass a token address whenever token identity matters. If token is omitted, the route uses Starkscan's tracked strkBTC token. Raw amounts are decimal strings; do not parse them as JavaScript number values.

Read analytics without inventing unavailable metrics

curl --fail-with-body --silent --show-error \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "$STARKSCAN_BASE_URL/v1/$STARKSCAN_CHAIN/privacy-pool/analytics"

Useful fields include totalEvents, uniqueTransactionCount, eventBreakdown, visibilityBreakdown, tokenFlows, missingAmountEventCount, and unavailableMetrics.

Treat token address as identity. tokenFlows[] metadata such as symbol, name, or decimals can be null; do not join or group flows by symbol. Use the TVL token metadata or your own address-keyed registry when display metadata is required.

For token-flow publication, require:

  • tokenFlowsTruncated === false;
  • tokenFlowSourceEventsTruncated === false; and
  • missingAmountEventCount === 0.

unavailableMetrics is part of the contract. It explains why values such as USD TVL or the exact anonymity set size are not currently publishable.

Count private swaps

curl --fail-with-body --silent --show-error \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "$STARKSCAN_BASE_URL/v1/$STARKSCAN_CHAIN/privacy-pool/private-swaps"

A private swap is one finalized Privacy Pool transaction in which a registered swap helper receives token A through a pool withdrawal and deposits a different token B into an open note. The registered helpers are AVNU's PrivacySwapHelper and Ekubo's swap anonymizers; each venue lists its helper addresses in venues[].helperAddresses. Other pool apps that change tokens, such as Endur staking or Vesu lending, are not swaps, and relay fees paid to the AVNU executor are not swaps either.

This observed-only aggregate is configured for SN_MAIN only. On SN_SEPOLIA, where no verified helper registry is configured, the route returns 503 private_swap_helper_registry_unavailable with Retry-After: 3600 rather than zero totals.

The route has no authoritative certificate that privacy_pool_event_finalized contains every pool event from the mainnet deployment block through the chain tip. The cutover can start with new events only, and a targeted repair can be deferred. Every returned count and volume is therefore over matched indexed rows only, including the trailing windows, venues, pairs, and daily buckets. coverage.status is always observed_only with reasonCode: historical_source_coverage_unproven. Do not publish these as complete chain-history or complete time-window totals. A zero means no matching indexed rows, not proof that no swaps occurred.

FieldMeaning
observedTotal, last24h, last7d, last30dswapCount, volumeUsd, and unpricedSwapCount over observed rows in each span. observedTotal is not an all-time chain total.
coverage.earliestObservedSwapBlockMinimum block among matched observed swaps, or null; not a coverage-from boundary.
chainFinalizedHeadBlockChain finalized tip, not proof that pool events were indexed through it.
venuesObserved totals per protocol, most observed swaps first.
topPairsThe ten sold/bought token pairs with the most observed swaps, with summed raw amounts.
dailyThe last 30 UTC days, oldest first; a zero bucket is not certified absence.

volumeUsd is an exact decimal sum of each observed priced swap's swap-time USD value: the sold token's value, or the bought token's when the sold token has no price. unpricedSwapCount counts swaps included in swapCount but missing from volumeUsd, so show it next to any volume figure. Open-note amounts are public by protocol design, and this route returns aggregates only: no per-user or per-swap rows.

The request path checks at most 10,000 finalized open-note candidates. If the observed candidate set exceeds that bound, the route returns 503 private_swap_candidate_bound_exceeded with Retry-After: 3600 rather than silently truncating the observed set. That is the trigger to move the same rule into a maintained read model. If a matched swap lacks a finalized block timestamp or has a future timestamp, the route instead returns 503 private_swap_time_coverage_unavailable with Retry-After: 60. It never publishes observed totals alongside silently inconsistent time windows or daily buckets. None of these errors returns a helper identity or per-swap row. Candidate-bound and time-coverage failures use a process-local, chain-scoped 60-second failure-marker cache; after a repair, availability can lag by at most that cooldown.

Page public event evidence

curl --fail-with-body --silent --show-error --get \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  --data-urlencode 'event=deposit' \
  --data-urlencode 'limit=100' \
  "$STARKSCAN_BASE_URL/v1/$STARKSCAN_CHAIN/privacy-pool/events"

The supported server-side filter is event. Pass its indexed snake_case value, such as deposit, withdrawal, or note_used. Contract, transaction, and block filters shown in the explorer are client-side filters over loaded rows and are not accepted by this endpoint.

Each row retains raw keys and data as audit evidence. Decoded publicFields.visibility can be:

  • public: the supported public fields were decoded;
  • partial: only part of the public shape is known; or
  • hidden_by_design: the protocol does not expose the field publicly.

Treat absent decoded fields as unavailable. Do not reconstruct them from unrelated events.

Page commitments and nullifiers

curl --fail-with-body --silent --show-error \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "$STARKSCAN_BASE_URL/v1/$STARKSCAN_CHAIN/privacy-pool/commitments?limit=100"

curl --fail-with-body --silent --show-error \
  -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
  "$STARKSCAN_BASE_URL/v1/$STARKSCAN_CHAIN/privacy-pool/nullifiers?limit=100"

Both endpoints accept an optional pool contract address. Preserve nextCursor exactly and send it with the same query to fetch the next page. Do not derive a cursor from a block number.

A commitment row and a nullifier row are independent public facts. The API does not identify the owner of a commitment and does not say which commitment a nullifier spent.

Cursor loop

const baseUrl = "https://api.starkscan.co";
const chain = "SN_MAIN";
const apiKey = process.env.STARKSCAN_API_KEY;

let cursor;
do {
  const url = new URL(`/v1/${chain}/privacy-pool/commitments`, baseUrl);
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);

  const response = await fetch(url, {
    headers: { "X-Starkscan-Api-Key": apiKey },
  });
  if (!response.ok) {
    throw new Error(`Starkscan ${response.status}: ${await response.text()}`);
  }

  const page = await response.json();
  for (const commitment of page.items) {
    console.log(commitment.blockNumber, commitment.txHash, commitment.commitment);
  }
  cursor = page.nextCursor ?? undefined;
} while (cursor);

Amounts and USD presentation

Use GET /v1/{chain}/privacy-pool/tvl for the current finalized per-token public-flow ledger. Use GET /v1/{chain}/privacy-pool/tvl/series only for prepared hourly points that actually exist. An empty or sparse hourly response is unavailable history, not a zero or a continuity claim.

Raw token amounts, token addresses, and decimals are the integration inputs. USD values displayed by Starkscan's website are presentation estimates, not a Privacy Pool API pricing contract. Apply an address-keyed price source in the consumer when a USD presentation is required.

Routes intentionally excluded

Two explorer routes remain outside the public integration contract:

  • GET /v1/{chain}/privacy-pool/dashboard is a UI-composition payload. Use the dedicated routes above so a frontend layout change cannot break an integration.
  • GET /v1/{chain}/privacy-pool/roots is not published while root facts are not materially available. An empty root page is not an anonymity-set claim.

Production checklist

  • Use https://api.starkscan.co, not https://starkscan.co/v1/*.
  • Send X-Starkscan-Api-Key; never place keys in URLs.
  • Treat every route's OpenAPI x-starkscan-certification and x-starkscan-stability as authoritative.
  • Preserve cursor strings exactly and keep the original filter set.
  • Keep felt addresses and raw integer amounts as strings.
  • Gate current counters on materializationFresh === true.
  • Do not publish unavailable, truncated, partial, empty, or sparse data as complete. Private swaps are explicitly observed-only: retain that label and never turn a zero into an absence claim.
  • Do not infer any hidden recipient beyond the public event fields, private ownership, note linkage, balances, or exact anonymity-set size.

On this page