# Verify a note's spent status on-chain

> Confirm on-chain that a note is still unspent before picking it as an input.

## What this lets you do

Ask the chain directly whether a specific note has been spent, bypassing the local [`NoteManager`](/sdk/note-manager) entirely. The pool stores each spent note's [nullifier](/concepts/commitments-and-nullifiers) in [`PoolVault`](/protocol/contracts/pool-vault)`.spentNullifiers`, and a non-zero return means "spent."

## When to use it

-   **Before picking inputs to spend.** Local state can drift after failed or partial flows. Always cross-check.
-   **To diagnose `PoolVault_NullifierAlreadySpent` reverts.** If a relay failed with selector `0x66f527f1`, this confirms the nullifier was already on-chain.
-   **For receipt verification.** Spent vs. unspent is part of payment-status answers, even though the canonical [receipt](/operations/receipts-and-viewing-keys) only proves "the note exists on-chain."

## Constraints & limits

-   You need the note's **nullifier**, not its commitment. Derive: `nullifier = Poseidon(privateNullifyingKey, commitment)`.
-   One `eth_call` per note. Batching depends on your RPC's multicall support.
-   This is a chain read: free, but it counts against your RPC's rate limit. Rather than running it for every note on every render, use it at decision points.

## What it unlocks next

-   Input picking that survives state drift: filter candidate notes by their on-chain spent status before selecting one, so a stale local note never gets picked.
-   Key-free payment confirmation: a service can confirm a note was spent from just its nullifier, since this is a public read that needs no private keys.

## How to use it

```ts

// Minimal ABI for the spentNullifiers read (not exported from the SDK entry point).
const POOL_VAULT_ABI = [
    {
        type: "function",
        name: "spentNullifiers",
        inputs: [{ name: "_nullifierHash", type: "uint256" }],
        outputs: [{ name: "when_", type: "uint256" }],
        stateMutability: "view",
    },
] as const;

const hash = await PoseidonHashService.create();

// For each note you're considering spending:
async function isNoteSpent(note: Note, privateNullifyingKey: Secret) {
    const nullifier = hash.hash([privateNullifyingKey, note.commitment]);
    const result = await publicClient.readContract({
        address: poolAddress,
        abi: POOL_VAULT_ABI,
        functionName: "spentNullifiers",
        args: [BigInt(nullifier)],
    });
    return result !== 0n;
}

// Chain-verified input picker.
async function pickUnspentNoteForSpend(candidates: Note[], privateNullifyingKey: Secret, amount: bigint) {
    for (const candidate of candidates) {
        if (BigInt(candidate.value) < amount) continue;
        if (await isNoteSpent(candidate, privateNullifyingKey)) continue;
        return candidate;
    }
    return null;
}
```

**Check the chain before spending.** Local note state can lag after a partial flow. Check the selected note's nullifier on-chain before using it as an input, and skip the note if the nullifier is already spent.

## Behind the scenes

Contract method `PoolVault.spentNullifiers(uint256 nullifier)` (returns `uint256`)
Nullifier formula `Poseidon(privateNullifyingKey, commitment)`
SDK helper No direct one-liner in the SDK. You build it from `PoseidonHashService` and `publicClient.readContract`.
