How it fits together
The SDK communicates with contracts on an EVM chain and three backend services that index and relay transactions. None of them holds keys or can move funds.
This page shows which component talks to which, what each request contains, and which requests the SDK never makes.
The whole system
- Proving is local. The prover runs inside the SDK. Secrets —
nsk, note values, the notes being spent — never leave the process. - Two parties write to the chain. Your signer broadcasts deposits and cancellations. The relayer broadcasts all other transactions.
- Reads and writes use different services.
fmd-webserveris read-only and never receives transactions. The relayer does not serve note data.
Who the SDK talks to
| Party | SDK entry point | Requests |
|---|---|---|
| fmd-webserver | FmdClient, NoteSource | encrypted notes, commitment chunks, nullifier chunks, sync positions |
| relayer | RelayerClient, Submitter, DepositStream | chain registry, fee estimates, swap wrapper address, spend, swap and generic-execution submission, deposit flush events |
| metaquoter | quoteSwap, fetchSwapQuote | swap route and minOut |
| EVM chain | ChainAdapter | asset registry, fee rates, deposit broadcast, escrow state, handle records |
The deployment also runs an explorer indexer, a risk-screening API, and a price feed. The SDK does not contact them.
Requests the SDK never makes
These requests would identify the wallet's notes to the server, so the SDK performs the work locally:
| Request not made | Reason | Local alternative |
|---|---|---|
"Is nullifier N spent?" | a nullifier identifies a note the wallet owns | download the full spent set and check locally |
"Merkle path for leaf i" | the leaf index identifies the note being spent | rebuild the tree from the append-only chunk feed |
| "Which notes are mine?" | reveals the wallet's notes | download all encrypted notes and trial-decrypt locally |
The third can be delegated: the matches sync strategy sends a detection key to the server to reduce bandwidth. The delegation is opt-in and cannot be revoked.
Deposit flow
A deposit is broadcast by your signer. After it is mined, the funds are in escrow until the relayer adds the note to the tree.
A new note is spendable only after steps 4 and 5. awaitDeposit waits for both; DepositStream observes step 4 directly.
The relayer's fee for step 4 is part of the deposit. Every deposit creates two leaves, both bound by the signature in step 1:
- the depositor's note;
- a fee note addressed to the relayer.
The fee note can use a different registered asset (feeAsset). The request records it as feeAssetId, and the pool transfers both tokens from the payer in the same transaction. The protocol fee is always in the deposited asset. If the relayer charges nothing, the fee leaf is a zero-value note sealed to an address drawn for it, which no one holds, and feeAssetId is 0.
Spend flow
Transfers, withdrawals, and swaps are broadcast by the relayer, never by your signer. The spend proof binds the relayer's address, so the pool rejects the transaction from any other sender.
The relayer receives the proof, its public inputs, and its fee note. The spent notes, payee, and amount are private circuit inputs and are not revealed. The relayer does see the submitter's IP address and the submission time.
The relayer and the FMD server are defaults, not requirements. Submitter and NoteSource are interfaces: you can use several relayers, run your own, submit directly from your own account, or use a different indexer. See Pluggable interfaces.
Visibility by party
| Party | Can see | Cannot see |
|---|---|---|
| fmd-webserver | that a client fetched a page of the public feed | which notes belong to the wallet (with the default full strategy) |
| relayer | IP address, submission time, its fee | spent notes, payee, amount |
| metaquoter | the token pair and size of a requested route | the requester's identity, and whether the swap is executed |
| the chain | deposit payer and amount; withdrawal recipient and amount; a handle and the address published under it | the contents of shielded transfers, other than that they occurred; who registered a handle |
| your RPC provider | which handle a client looks up, and when | which payment, if any, follows a lookup, unless it also serves the relayer |
Deposits, withdrawals and handle registrations are publicly visible. Shielded transfers are not.
What each service can and cannot do
No service holds a key, so none can spend. What each can still do, and what the SDK offers against it:
| Service | Cannot | Can | Check |
|---|---|---|---|
| relayer | change the recipient, the amount or any output, or the label, address or controller of a handle registration: the proof binds them | quote any fee; refuse to relay; see a registration's label before it lands | maxFee on a spend, acceptRelayerFee on the wallet — Fees |
| fmd-webserver | make a spend land: a proof against a tree the pool does not hold is rejected on chain | withhold a note; serve a well-formed note the pool never inserted | confirmCommitment before crediting a payment — Watch-only wallets |
| artifact host | — | serve another proving key, whose proofs can leak the witness | digest pinning — Browser usage |
A wallet that only displays a balance needs none of these. One that pays unattended, or releases goods on a payment, should use the first two.
Next
- Concepts
- Syncing — the read path
- Deposit — the deposit path
- Architecture — the SDK's module structure