Skip to main content

Integration architecture

This guide describes how to assemble the pieces of a Privacy Pools v2 integration: what runs in the client, what you persist, which external services you depend on, and the sequencing that keeps flows from failing mid-way. The patterns are the ones the production frontend uses, generalized so they apply to any app.

Client-side proving

Proving happens client-side, always. Note secrets, derived keys, and proof witnesses must never leave the user's device, because anyone who sees them can deanonymize (or in the nullifying key's case, spend) the user's funds. Everything else in this guide follows from that rule: circuit artifacts move to the client rather than witnesses moving to a server, key derivation happens in the user's wallet session, and the only things that cross the network are public reads, encrypted payloads, and finished proofs with their public signals.

Sessions and keys

Sessions. Build one session per wallet-and-chain pair, hold it in memory, and rebuild it whenever the wallet or chain changes. The session itself is never persisted: it is cheap to reconstruct, and everything durable lives behind it.

Keys. Same principle: derive them from the wallet's EIP-712 signature when the user first needs them, keep them in memory for the session, and never write a derived private key to storage. The same wallet signing the same message always reproduces the same keys, so a page reload costs only one signature. Two rules from production use:

  • Ask for the derivation signature only in response to a user action, with an explainer of what they're signing, rather than reflexively on wallet connect.
  • Build the EIP-712 payload byte-for-byte to the SDK's reference, since any deviation derives a different account (see derive keys).

Keystore registration. A spending prerequisite, not a deposit one: the protocol checks keystore membership only for transfers, withdrawals, and ragequit, never for a deposit. The v2 frontend still routes users through registration before their first deposit as a product-sequencing choice, so a freshly deposited note is spendable as soon as it attests rather than stalling on registration later.

In your own app, check isKeystoreRegistered() at that entry point and route through registration when needed. Handle the case where another tab won the registration race by treating the contract's already-set error as success.

What to persist

Persist (per owner and chain)Why
Note state and sync cursor (exportAccount() output)Avoids a full chain rescan on every load; local note state is a cache, reconcilable from the chain at any time
Payment-request stateCarries each request's matching state; supply a persistent storage adapter, because the default in-memory one loses open requests on refresh
Receipts you issue or importThey contain the noteSecret; once handed over they cannot be regenerated by the recipient

Never persist derived private keys or the session object. Treat everything you do persist as sensitive to the degree it contains note material: an exported note state includes every note's secret.

Circuit artifacts in production

Artifacts are large and the client needs them before any proof. The full set runs to roughly a gigabyte, dominated by the transact proving keys, which range from under 20 MB for the smallest input/output shape to nearly 70 MB for the largest; the deposit circuit's artifacts are a few megabytes. Two consequences:

  • Serve them as static files from your own origin or a CDN and fetch only the circuit shape a given proof needs, which the SDK's artifact loaders already do. Hosting options are covered in circuit artifacts.
  • Run witness preparation and proving off the main thread (web workers in a browser), run proofs one at a time rather than in parallel, and show staged progress. Proof generation and witness preparation can take up to several seconds depending on the shape.

External dependencies and degraded behavior

An integration depends on three external services. Decide what your app does when each is slow or down, because all three will be at some point.

DependencyProvidesWhen it degrades
RPC endpointChain reads and self-submitted transactionsChunk historical eth_getLogs scans to the provider's block-range cap, pause background polling while a transaction is in flight, and prefer the ASP's bulk endpoints over raw log scans
ASP serviceEvent snapshots, note-event stream, attestation status, entrypoint resolutionDiscovery falls back from the ASP fast path to chunked RPC log scans; cache the last good responses and tolerate staleness for reads
RelayerFee quotes and gasless submissionConfigure more than one relayer where available, and fall back to self-relay, which keeps the transfer private but exposes your wallet as the submitter

On chains that host more than one pool, resolve the entrypoint at runtime from the ASP's /global/public/entrypoints feed by matching your configured pool vault, rather than hardcoding it.

Sequencing that prevents mid-flow failures

Order matters in spend flows, because fee quotes are short-lived and proofs are expensive:

  1. Register before quoting. If the account still needs keystore registration, clear it before fetching a quote rather than between quote and submit. It is a separate transaction gated by a wallet confirmation, and folding that into the short quote-to-submit window risks the quote expiring after you have already paid the proving cost.
  2. Spendability checks before proving. Verify each candidate input on-chain (nullifier unspent, commitment present, label attested) before paying the proving cost; local state can lag after a failed flow, and a proof built on a stale input reverts.
  3. Quote, then prove, then submit, with the expiry shown to the user and re-checked at submit time. On expiry, re-quote and rebuild rather than retrying the stale payload.
  4. Recipient checks before review. Look up the recipient's viewing-key registration before building anything, and do it against a bulk-synced local registry rather than per-recipient queries, so the lookup itself doesn't leak who your user is about to pay.

Production checklist

  • Proving and witness preparation run client-side, with no secret material in any network request.
  • One session per wallet and chain, rebuilt on change; keys re-derived, never stored.
  • Note state, sync cursor, and payment-request state persisted per owner and chain through a storage adapter.
  • Artifacts hosted statically, fetched per circuit shape, with multi-second proving reflected in the UX.
  • Registration handled the first time it's required, and the spend flow ordered register, quote, prove, submit as above.

For the failure catalog behind these patterns, see Failure modes.