# Failure modes

> Errors you will hit, what they actually mean, and how to recover.

Concrete errors you will see, mapped to actual cause and the recovery path. Curated from real PoC integration sessions. Not exhaustive, so add as you find new ones.

## Builder & config

### `InvalidBuilderConfig: Invalid input: expected string, received undefined`

-   **Cause:** a required string field in [`PoolSessionBuilder`](/sdk/pool-session-builder)`.fromConfig` is undefined. The message prefixes the failing field as `path: message` from the first Zod issue.
-   **Fix:** read the path in the message; for the full issue list, wrap your call site and dump the config before passing it in. Common culprits: empty `ASP_PUBLIC_KEY` env var, undefined `RELAYER_ADDRESS`, `protocolKeys.viewingPublicKey` empty when keys haven't been derived yet.

## Transfer + relay

### `RecipientViewingKeyUnregistered`

-   **Cause:** the recipient's address has no viewing key registered on the on-chain [Keystore](/protocol/contracts/keystore). An unregistered recipient throws by default.
-   **Fix:** pass `recipientDiscoveryData: {evmAddress, allowOutOfBandFallback: true}` to opt into out-of-band note delivery. The receipt captures the noteSecret either way, so OoB is fine for most flows.

### `RelayerRejected: PoolVault_InvalidTransactProof()`

-   **Cause:** your local proving keys (zkeys) don't match the on-chain verifying key. Usually happens when the SDK, circuits, and on-chain verifier are from different deployment sets.
-   **Fix:** align all three to the same deployment set. See [Sepolia V9 addresses](../deployments/sepolia#contract-addresses-sepolia-v9).

### `PoolVault_NullifierAlreadySpent()` (selector `0x66f527f1`)

-   **Cause:** the chosen input note's nullifier is already on-chain. Local note state can lag after a failed or partial relay flow.
-   **Fix:** before picking inputs, verify each candidate's nullifier against [`PoolVault`](/protocol/contracts/pool-vault)`.spentNullifiers(nullifier)` via `eth_call`. Nullifier formula: `Poseidon(privateNullifyingKey, commitment)`.

### `PoolVault_InvalidProcessor()`

-   **Cause:** `transactParams.processor` does not match the address that actually submitted `PoolVault.transact`.
-   **Fix:** bind the proof to the right processor: the one your relayer designates (`RelayerInfo.processorAddress`; the deployed Sepolia relayer uses the `PrivacyPoolRelay` contract for both transfers and withdrawals), or your own wallet for self-relay.

### `PoolVault_ProofContextMismatch()`

-   **Cause:** the proof's context hash no longer matches the encoded `TransactParams` and `NoteData`.
-   **Fix:** do not mutate the prepared payload after proving. If processor, note data, fee data, or payout routing changes, rebuild the proof.

### `Too many values for input signal keystoreSiblings`

-   **Cause:** circuits were compiled with a different `keystoreMaxDepth` than the SDK's witness generator expects. The SDK and circuits are from different deployment sets with different tree depths.
-   **Fix:** align the SDK and circuits to the same deployment set.

## Discovery

### `NoteNotFoundError: Note not found: 0x…`

-   **Cause:** the cycle / spend code picked an input commitment that the SDK's local [NoteManager](/sdk/note-manager) doesn't have. Usually state drift between a server-side preflight (reading the `indexed_notes` cache) and the client-side NoteManager.
-   **Fix:** call `session.discoverNotes()` right before picking inputs to reconcile local state.

### Discovery hangs / never completes

-   **Cause:** SDK paginates `eth_getLogs` at 10k blocks/page from the `fromBlock` passed to `discoverNotes()` (defaulting to the NoteManager sync cursor) to head. On a fresh NoteManager without `fromBlock`, scans start from genesis: thousands of RPC calls, hits free-tier rate limits.
-   **Fix:** pass `fromBlock` as the deploy block of the current pool: `discoverNotes({ fromBlock: deployBlockHex })`. Or configure `aspUrl` so the SDK's HttpASPDataProvider pulls the encrypted Note stream from `/{chainId}/public/note-events` (gap-filling the tail over RPC) instead of chunking the log scan itself.

## Persistence

### Treasury balance refuses to decrease across page reload

-   **Cause:** a `SPENT` note in the in-memory NoteManager was not persisted before reload, typically after an interrupted write. The next `discoverNotes` then resurrects the spent note as `ACTIVE`.
-   **Fix:** call `await session.discoverNotes()` to reconcile. It flushes note updates internally through the public session API, so you do not need to reach the lower-level note-manager directly. The transact path also flushes on success, so this only surfaces after an interrupted write. If the local index is corrupt rather than stale, reset it by clearing the configured persistent storage and re-running discovery.

## Build / install

### Next.js `ENOENT` referencing an old SDK tarball hash

-   **Cause:** after swapping the vendored SDK tarball, Next.js's `.next/` cache holds compiled references to the old `.pnpm/` identity, but pnpm reshuffled and the path no longer exists. This is specific to the Next.js-based Payroll PoC, not the current v2 frontend.
-   **Fix:** in the Payroll PoC, kill the dev server, `rm -rf .next`, and restart after every SDK swap. The current v2 frontend is an Expo app, so the equivalent reset there is clearing the Metro bundler cache with `expo start -c`.
