Quickstart

The shortest accurate path from zero to a blind transfer, using the real API and the real flow the dApp uses. Read How it works for the mental model and Concepts for why each step exists. The helpers referenced here (executePlan, spendableNotes, session keys) are in Recipes.

Early access. Protocol15 runs on devnet only, and the @p15/* packages are not published to npm yet. Today you consume them from local checkouts with file: paths. The import lines below are the published names so the code matches the future release. The API does not change.

1. Install

Shell
npm install @p15/stealth @p15/blind-mode @p15/compliance @solana/kit

2. Derive keys

Every key derives from one ed25519 signature. No seed phrases. The spendScalar is spend authority and never leaves the client.

TypeScript
import { deriveKeys, keyDerivationMessage, encodeMetaAddress } from "@p15/stealth";
 
const epoch = 0;
const keys = deriveKeys(await wallet.signMessage(keyDerivationMessage(walletAddress, epoch)), epoch);
 
const myMetaAddress = encodeMetaAddress({
  scanPublic: keys.scanPublic,
  spendPublic: keys.spendPublic,
});

3. Build the backend

TypeScript
import { getBackend, RemoteProver } from "@p15/blind-mode";
 
const backend = getBackend("noir-v1", { prover: new RemoteProver(process.env.PROVER_URL!) });
const rentFor = (space: bigint) => rpc.getMinimumBalanceForRentExemption(space).send();

4. Make funds private

The one explicit funding step, and the one public moment on the way in. For SOL you wrap into a wSOL ATA and deposit in the same transaction, so make-private stays a single wallet prompt.

TypeScript
import { getTransferSolInstruction } from "@solana-program/system";
import { getCreateAssociatedTokenIdempotentInstruction, getSyncNativeInstruction } from "@solana-program/token";
import { lamports } from "@solana/kit";
 
const plan = await backend.planFund({
  owner: walletSigner,
  feePayer: walletSigner,
  mint,                       // wSOL mint for SOL
  tokenProgram,
  amount: 1_000_000_000n,     // base units (lamports for SOL)
  scanSecret: keys.scanSecret,
  spendScalar: keys.spendScalar,
  rentFor,
});
 
// Wrap SOL, then deposit, in one wallet-signed transaction:
const ixs = [
  getCreateAssociatedTokenIdempotentInstruction({ payer: walletSigner, ata: wsolAta, owner: walletAddress, mint }),
  getTransferSolInstruction({ source: walletSigner, destination: wsolAta, amount: lamports(1_000_000_000n) }),
  getSyncNativeInstruction({ account: wsolAta }),
  ...plan.transactions[0].instructions,
];
await sendWithWallet(ixs);

5. Send a blind transfer

Two wallet prompts total: one already spent funding the session key (step 4 of Recipes), one is implicit in the session signing. Pick an input note from your scanned, marker-checked notes.

TypeScript
const notes = await spendableNotes(keys);            // Recipes
const inputNote = pickInputNote(notes, 250_000_000n); // Recipes
 
const plan = await backend.planSend(
  {
    feePayer: sessionSigner,
    senderScanSecret: keys.scanSecret,
    senderSpendScalar: keys.spendScalar,
    senderScanPublic: keys.scanPublic,
    mint,
    amount: 250_000_000n,
    recipientMetaAddress: theirMetaAddress,
    rentFor,
  },
  inputNote,
);
 
await executePlan(plan, { rpc, rpcSubscriptions, sessionSigner, walletSigner });

There is no claim step: an incoming payment is already a spendable note for the recipient.

6. Receive

The recipient scans one fixed anchor and re-derives their notes. spendableNotes already does this; the underlying call is:

TypeScript
import { scanAnnouncements } from "@p15/stealth";
const found = await scanAnnouncements(rpc, keys.scanSecret, { indexerUrl });
// direction 0 = incoming, 1 = your own self-receipt

7. Withdraw (optional)

Move a private note back to public SOL. Three transactions (the range proof is a solo tx), and a public moment.

TypeScript
const plan = await backend.planWithdraw(
  { feePayer: sessionSigner, owner: walletSigner, scanSecret: keys.scanSecret,
    spendScalar: keys.spendScalar, mint, tokenProgram, amount, destination, rentFor },
  inputNote,
);
await executePlan(plan, { rpc, rpcSubscriptions, sessionSigner, walletSigner });

Next