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
| Need | Route | Important meaning |
|---|---|---|
| Lifetime counters and freshness | GET /v1/{chain}/privacy-pool/status | Public event and note-fact counters at the latest decoded cursor. |
| Daily activity | GET /v1/{chain}/privacy-pool/metric-buckets | Prepared daily deposits, withdrawals, note-use events, and viewing-key registrations. |
| Chart series | GET /v1/{chain}/privacy-pool/metrics/series | Prepared wallet-growth, token public-flow, and privacy-fee series. |
| Aggregate analysis | GET /v1/{chain}/privacy-pool/analytics | Event, visibility, token-flow, attribution, and typed unavailable-metric summaries. |
| Observed private swaps | GET /v1/{chain}/privacy-pool/private-swaps | Indexed matched-swap count and USD volume by window, venue, token pair, and day; historical source completeness is unproven. |
| Public event evidence | GET /v1/{chain}/privacy-pool/events | Cursor-paginated raw keys/data plus decoded public fields. |
| Commitment evidence | GET /v1/{chain}/privacy-pool/commitments | Public commitment facts. No owner or spend inference. |
| Nullifier evidence | GET /v1/{chain}/privacy-pool/nullifiers | Public 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_MAINSend 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:
latestRawEventCursorandlatestDecodedEventCursor;eventLagBlocks; andmaterializationFresh.
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:
| Field | Meaning |
|---|---|
commitmentCount | Public commitment facts indexed by Starkscan. |
nullifierCount | Public nullifier facts indexed by Starkscan. |
spentNoteProxyCount | Public proxy equal to the nullifier count. |
unspentNoteProxyCount | Commitments 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:
| Field | Values | Publication rule |
|---|---|---|
TVL top-level status | complete, degraded, unavailable | Publish only complete. |
TVL coverage.status | complete, partial, unavailable | Publish only complete; partial is not a zero. |
Private-swaps coverage.status | observed_only | Publish only with an explicit observed-only label; never call it a complete total. |
Event publicFields.visibility | public, partial, hidden_by_design | Use 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; andlatestBlockNumber.
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.
walletSeriescontains viewing-key wallet growth.shieldedSupplySeriescontains deposited, withdrawn, and net public-flow raw amounts for the requested token.privacyFeeSeriescontains 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; andmissingAmountEventCount === 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.
| Field | Meaning |
|---|---|
observedTotal, last24h, last7d, last30d | swapCount, volumeUsd, and unpricedSwapCount over observed rows in each span. observedTotal is not an all-time chain total. |
coverage.earliestObservedSwapBlock | Minimum block among matched observed swaps, or null; not a coverage-from boundary. |
chainFinalizedHeadBlock | Chain finalized tip, not proof that pool events were indexed through it. |
venues | Observed totals per protocol, most observed swaps first. |
topPairs | The ten sold/bought token pairs with the most observed swaps, with summed raw amounts. |
daily | The 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; orhidden_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/dashboardis a UI-composition payload. Use the dedicated routes above so a frontend layout change cannot break an integration.GET /v1/{chain}/privacy-pool/rootsis 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, nothttps://starkscan.co/v1/*. - Send
X-Starkscan-Api-Key; never place keys in URLs. - Treat every route's OpenAPI
x-starkscan-certificationandx-starkscan-stabilityas 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.