getting started

Quick Start

End-to-end walkthrough of a private transfer

Prerequisites#

  • A Solana wallet (Phantom or Solflare) with devnet SOL
  • Node.js 18+ for the relayer
  • The Mini Veil devnet cluster running

Step-by-Step Flow#

1. Connect Wallet#

Open the Mini Veil dashboard and connect your wallet. The dashboard automatically checks your key registration status.

2. Register Keys (first time only)#

If prompted, approve the signMessage request to derive your protocol keys. The dashboard will register them on-chain via the register_keys instruction.

3. Enter Recipient#

In the PrivateSend card, enter the recipient's wallet address. The dashboard checks if the recipient has registered their keys (500ms debounced lookup).

i

Recipient Does Not Have Keys

If the recipient hasn't registered keys, the dashboard shows a warning. They need to connect to the dashboard at least once and register their keys before you can send to them.

4. Select Denomination#

Choose a pool denomination: 0.1 SOL, 1 SOL, or 10 SOL. Each denomination has a separate Merkle tree and vault.

5. Shield (if needed)#

If you don't have an unspent note in the pool, click Shield:

  • The relayer provides a counter via GET /next-counter
  • A note is computed: nullifier = Poseidon(spendingKey, domain, counter), commitment = Poseidon(nullifier, secret)
  • The shield instruction transfers denomination lamports to the vault PDA
  • The relayer detects the DepositEvent via onLogs subscription and inserts the commitment into its Merkle tree

6. Wait for Relayer#

The frontend polls GET /proof?commitment=0x...&denomination=...:

  • Blocks up to 30 seconds waiting for the relayer to process the deposit
  • Returns 408 on timeout — the frontend retries up to 30 times with 1-second intervals
  • Returns { root, pathElements, pathIndices, leafIndex } when ready

7. Generate ZK Proof#

snarkjs generates a Groth16 proof in the browser:

public signals: merkle_root, nullifier_hash, recipient, denomination
private signals: spending_key, counter, secret, pathElements[20], pathIndices[20]

This takes approximately 2-5 seconds.

8. Unshield + Announce#

A bundled transaction is submitted:

  1. ComputeBudgetProgram.setComputeUnitLimit({ units: 700_000 })
  2. unshield with packed proof (G1: 64 bytes, G2: 128 bytes, 4 public inputs)
  3. announceStealth with ephemeral key, view tag, encrypted key

The on-chain program:

  • Verifies the Groth16 proof via alt_bn128 pairing
  • Creates a nullifier PDA to prevent double-spending
  • Transfers denomination from vault to the recipient's stealth address

9. Recipient Claims#

On the recipient's dashboard, ScanClaimCard:

  • Fetches StealthAnnouncement events from recent transactions
  • Filters by view tag (fast rejection)
  • Decrypts the ephemeral key using their viewing key
  • Derives the stealth private key
  • Builds a transaction signed by the stealth address's ScalarSigner
  • Sends the claim transaction, transferring funds to their main wallet

Costs#

| Operation | Cost | |---|---| | Key registration | ~0.005 SOL | | Shield | denomination + transaction fee | | Unshield | denomination + ~0.00089 SOL (nullifier PDA rent) + transaction fee | | Sweep fee | 10,000 lamports deducted from claim amount |

Complete

That's the full flow. You've now sent a private, unlinkable transfer on Solana. The entire process fits in ~9 steps and takes about 30 seconds end-to-end.