Skip to main content

Quickstart

This walkthrough takes a wallet that holds some Sepolia ETH from install to a first private transfer. Everything runs client-side against the Sepolia testnet.

1. Install

SDK access: The v2 SDK (@privacy-pools-v2/sdk) is not yet publicly available. Partners with early access are granted access to the v2-monorepo and consume the SDK from it as a workspace dependency. Reach out via X to request access: @0xbowio.

2. Connect a wallet

The snippets below assume a viem wallet client and an HTTPS JSON-RPC endpoint. Any funded Sepolia account works:

import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

const rpcUrl = process.env.RPC_URL;
if (!rpcUrl) throw new Error("set RPC_URL to an HTTPS JSON-RPC endpoint");

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({ account, chain: sepolia, transport: http(rpcUrl) });
const address = account.address;

In a browser app, walletClient comes from the user's connected wallet instead.

3. Derive your protocol keys

Privacy Pools v2 derives three private keys and one public viewing key from a single EIP-712 signature. You sign once per session; signing the same message again always re-derives the same keys.

import { CryptoService } from "@privacy-pools-v2/sdk";
import { buildSecretDerivationPayload } from "./your/derivation/helper";

const payload = buildSecretDerivationPayload(address);
const signature = await walletClient.signTypedData(payload);

const crypto = new CryptoService();
const keys = crypto.deriveKeysFromSignature({
signature,
signerAddress: address,
addressHash: payload.message.addressHash,
revocableKeyIndex: "0x0",
});

Key derivation helper: copy buildSecretDerivationPayload from v2-monorepo/apps/sample/src/keystore/secretDerivationPayload.ts, or build the same EIP-712 payload in your app. Fresh accounts use revocableKeyIndex: "0x0". After key rotation, reuse the returned index.

4. Build a PoolSession

import { PoolSessionBuilder } from "@privacy-pools-v2/sdk";
import { FetchCircuitArtifacts } from "./FetchCircuitArtifacts";

const session = await PoolSessionBuilder.fromConfig({
chainId: 11155111, // Sepolia
rpcUrl,
ownerAddress: address,
protocolKeys: { ...keys, revocableKeyIndex: "0x0" },
aspUrl: "https://api-dev.0xbow.io",
relayers: [
{
url: "https://relayer-v2-staging-149184580131.us-east1.run.app",
name: "0xBow Relayer",
chainId: 11155111,
chainType: "evm",
status: "active",
address: "0x4Ba5fF376865b370790A56276C63e7984DCFf1f7",
processorAddress: "0x762665Dc7aAeeA25DC1759AEBef1F61730497f6e",
},
],
walletInteractor: { type: "viem", walletClient },
})
.withCircuitArtifacts(new FetchCircuitArtifacts("/circuits"))
.create();

Setup notes:

  1. Proving runs client-side, so the session loads circuit artifacts over HTTP with FetchCircuitArtifacts pointed at wherever your app serves them (here /circuits). It is a sample-app helper rather than an SDK export, so copy v2-monorepo/apps/sample-web/src/FetchCircuitArtifacts.ts or supply your own ICircuitArtifacts loader (see circuit artifacts). The pre-compiled artifact set ships in v2-monorepo/packages/circuits/build/, so copy the circuits you serve from there.
  2. address is the relayer's signing wallet, while processorAddress is the PrivacyPoolRelay contract the deployed relayer submits through. Proofs bind to the processor, so this value must match what the relayer actually uses.

5. Register on the keystore

Register before you spend. You can deposit first, but transfer, withdraw, and ragequit all prove keystore membership, so a deposit made by an unregistered wallet can be neither spent nor recovered until its owner registers (see Accounts & keys).

if (!(await session.isKeystoreRegistered())) {
await session.registerKeystore(); // two transactions, paid in Sepolia ETH
}

6. Deposit

const NATIVE_ETH = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
const receipt = await session.deposit({
tokenId: NATIVE_ETH,
value: "0x2386f26fc10000", // 0.01 ETH in wei
});
console.log("deposit landed:", receipt.txHash);

// Wait for ASP approval (often within an hour, up to 7 days).
// Then sync your local note state from chain:
await session.discoverNotes();
const active = (await session.exportAccount()).notes
.filter((n) => n.status === "ACTIVE");
console.log(`${active.length} spendable note(s)`);

When the deposit is approved, the second log prints 1 spendable note(s). Until then it prints 0: the note exists but is still PENDING.

7. Send a private transfer

The recipient is any address with a registered viewing key; for a first run, a second account of your own works.

const recipientAddress = "0x..."; // a registered recipient
const myNotes = (await session.exportAccount()).notes
.filter((n) => n.status === "ACTIVE");
const noteToSpend = myNotes[0];

const prepared = await session.prepareTransfer({
inputCommitments: [noteToSpend.commitment],
amount: "0x11c37937e08000", // 0.005 ETH in wei
tokenId: NATIVE_ETH,
recipientDiscoveryData: { evmAddress: recipientAddress },
});

const result = await session.relayTransfer(prepared.relayOptions[0]);
console.log("transfer landed:", result.txReceipt.txHash);

A transaction hash on the final log means the transfer is on-chain.

The transfer is complete. The recipient's note is now in the pool. They can call discoverNotes() to find it (discoverable mode), or you can hand them the secret from prepared.executeOptions.recipientPendingNotes[0].noteSecret (out-of-band mode).

Next steps