Skip to content
Launch App >

Withdrawing sends tokens from the privacy pool to any address without revealing which deposit is being spent. The relayer submits the transaction, preserving your privacy.

  1. Load Your Deposit Secrets

    Retrieve the secrets you stored during deposit:

    interface DepositSecrets {
    nullifier: Uint8Array;
    secret: Uint8Array;
    leafIndex: number;
    mint: string;
    amount: number;
    }
    function loadDeposit(txSignature: string): DepositSecrets {
    const encrypted = localStorage.getItem(`deposit_${txSignature}`);
    const decrypted = decrypt(encrypted);
    const data = JSON.parse(decrypted);
    return {
    nullifier: Uint8Array.from(data.nullifier),
    secret: Uint8Array.from(data.secret),
    leafIndex: data.leafIndex,
    mint: data.mint,
    amount: data.amount,
    };
    }
  2. Get Merkle Proof from Indexer

    Fetch the proof path for your deposit:

    async function getMerkleProof(
    mint: string,
    leafIndex: number,
    network: string = 'mainnet'
    ): Promise<MerkleProof> {
    const response = await fetch(
    `${API_BASE}/get_proof/${mint}/32/${leafIndex}/${network}`,
    { method: 'POST' }
    );
    if (!response.ok) {
    throw new Error(`Failed to get proof: ${response.status}`);
    }
    return response.json();
    }
  3. Get Current Merkle Root

    async function getMerkleRoot(
    mint: string,
    network: string = 'mainnet'
    ): Promise<Uint8Array> {
    const response = await fetch(
    `${API_BASE}/get_merkle/${mint}/32/${network}`,
    { method: 'POST' }
    );
    const data = await response.json();
    return Uint8Array.from(data.json.root);
    }
  4. Generate ZK Proof

    Generate the withdrawal proof client-side:

    import { poseidon } from '[PLACEHOLDER: POSEIDON_NPM_PACKAGE]';
    async function generateWithdrawProof(
    nullifier: Uint8Array,
    secret: Uint8Array,
    merkleProof: Uint8Array[],
    merkleRoot: Uint8Array,
    recipient: string,
    leafIndex: number
    ): Promise<{ proof: Uint8Array; nullifierHash: Uint8Array }> {
    // Compute nullifier hash
    const nullifierHash = poseidon([nullifier]);
    // Compute commitment
    const commitment = poseidon([nullifier, secret]);
    // Generate SNARK proof
    // This requires the circuit and proving key
    const proof = await generateProof({
    nullifier,
    secret,
    commitment,
    merkleProof,
    merkleRoot,
    nullifierHash,
    recipient,
    leafIndex,
    });
    return { proof, nullifierHash };
    }
  5. Check OFAC Compliance

    Get an OFAC signature before withdrawing:

    async function getOfacSignature(
    authToken: string,
    network: string = 'mainnet'
    ): Promise<string> {
    const response = await fetch(
    `${API_BASE}/get_ofac_signature/${network}/${authToken}`,
    { method: 'POST' }
    );
    if (!response.ok) {
    throw new Error('OFAC check failed');
    }
    return response.json();
    }
  6. Call Relayer

    Submit the withdrawal to the relayer:

    interface RelayParams {
    nullifierHash: number[];
    proof: number[];
    root: number[];
    recipient: string;
    depth: number;
    depositSize: number;
    mint: string;
    network?: string;
    }
    interface RelayResponse {
    status: number;
    signature?: string;
    error?: string;
    }
    async function relay(params: RelayParams): Promise<RelayResponse> {
    const response = await fetch(`${API_BASE}/relay`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(params),
    });
    return response.json();
    }
async function withdraw(
deposit: DepositSecrets,
recipient: string,
authToken: string,
network: string = 'mainnet'
): Promise<string> {
// 1. Get Merkle proof
const proofPath = await getMerkleProof(deposit.mint, deposit.leafIndex, network);
const merkleRoot = await getMerkleRoot(deposit.mint, network);
// 2. Generate ZK proof
const { proof, nullifierHash } = await generateWithdrawProof(
deposit.nullifier,
deposit.secret,
proofPath,
merkleRoot,
recipient,
deposit.leafIndex
);
// 3. Check OFAC compliance
await getOfacSignature(authToken, network);
// 4. Call relayer
const result = await relay({
nullifierHash: Array.from(nullifierHash),
proof: Array.from(proof),
root: Array.from(merkleRoot),
recipient,
depth: 32,
depositSize: deposit.amount,
mint: deposit.mint,
network,
});
if (result.status !== 200 || !result.signature) {
throw new Error(result.error || 'Withdrawal failed');
}
// 5. Mark deposit as spent locally
markDepositSpent(deposit);
return result.signature;
}

A withdrawal fee is deducted from the withdrawn amount:

  • Relay Fee: [PLACEHOLDER: FEE_WITHDRAW_PCT]

The recipient receives: depositAmount - relayFee

ErrorCauseSolution
NullifierAlreadySpentAlready withdrawnCheck local records
InvalidProofProof verification failedRegenerate proof
InvalidRootMerkle root too oldRefetch current root
OFACCheckFailedAddress is sanctionedCannot proceed

Before attempting withdrawal, check if the nullifier was already spent:

async function isNullifierSpent(
mint: string,
nullifierHash: Uint8Array,
network: string = 'mainnet'
): Promise<boolean> {
const response = await fetch(
`${API_BASE}/get_nullifier/${mint}/32/${network}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
nullifierHash: Array.from(nullifierHash),
}),
}
);
const data = await response.json();
return data.json !== null; // If account exists, nullifier is spent
}
PracticeReason
Wait before withdrawingLarger anonymity set
Use a fresh recipient addressNo link to your identity
Don’t withdraw immediately after depositTiming analysis protection
Withdraw full amountPartial withdrawals may reduce privacy