UPP — Universal Private Pool

SDK Quickstart

Install the UPP SDK, shield tokens, transfer privately, and withdraw — in under 5 minutes.

Private Preview

The UPP SDK is in private preview. Contact us for early access.

Installation

npm install @permissionless-technologies/upp-sdk

Setup

import { createUPPClient } from '@permissionless-technologies/upp-sdk'
import { createWalletClient, createPublicClient, http } from 'viem'
import { sepolia } from 'viem/chains'

const publicClient = createPublicClient({ chain: sepolia, transport: http() })
const walletClient = createWalletClient({ chain: sepolia, transport: http() })

// Initialize UPP client — keys derive from wallet signature (no extra seed phrases)
const upp = await createUPPClient({ publicClient, walletClient })

Core Operations

Shield (Deposit)

Move public tokens into the private pool:

import { parseUnits } from 'viem'

// Approve UPP to spend your tokens
await upp.approve({ token: UPD_TOKEN_ADDRESS, amount: parseUnits('1000', 18) })

// Shield tokens — creates an encrypted note in the shared pool
const { commitment } = await upp.shield({
  token: UPD_TOKEN_ADDRESS,
  amount: parseUnits('1000', 18),
})

console.log('Shielded! Note commitment:', commitment)

Transfer (Private Send)

Send to another user within the pool:

// Get recipient's stealth meta-address (they share this publicly)
const recipientAddress = '0zk1sepolia1...'

// Transfer — proves you own the note without revealing which one
const { recipientCommitment, changeCommitment } = await upp.transfer({
  amount: parseUnits('500', 18),
  to: recipientAddress,
})

// Recipient gets 500 UPD, you get a change note

Check Balance

Scan for your notes and compute private balance:

// Scans encrypted notes, decrypts yours using your viewing key
const notes = await upp.scan()

const balance = notes
  .filter(n => n.token === UPD_TOKEN_ADDRESS && !n.spent)
  .reduce((sum, n) => sum + n.amount, 0n)

console.log('Private balance:', balance)

Unshield (Withdraw)

Move tokens back to a public address:

const { txHash } = await upp.unshield({
  amount: parseUnits('200', 18),
  to: '0xRecipientAddress',
  aspId: 1n, // Which ASP to use for the compliance proof
})

React Integration

import { UPPAccountProvider, useUPPAccount } from '@permissionless-technologies/upp-sdk/react'

function App() {
  return (
    <UPPAccountProvider>
      <PrivateBalance />
    </UPPAccountProvider>
  )
}

function PrivateBalance() {
  const { notes, isScanning, shield, transfer, unshield } = useUPPAccount()

  const balance = notes
    ?.filter(n => !n.spent)
    .reduce((sum, n) => sum + n.amount, 0n) ?? 0n

  if (isScanning) return <div>Scanning notes...</div>

  return (
    <div>
      <p>Private balance: {balance.toString()}</p>
      <button onClick={() => shield({ token: UPD_TOKEN_ADDRESS, amount: parseUnits('100', 18) })}>
        Shield 100 UPD
      </button>
    </div>
  )
}

Key Concepts

Stealth Addresses

Recipients share a stealth meta-address (0zk1...). Senders derive unique one-time addresses per payment — even the recipient's public address never appears on-chain.

// Generate your stealth meta-address to share publicly
const myStealthAddress = upp.getStealthMetaAddress()

Viewing Keys & Audit

UPP has a 4-tier key hierarchy for graduated disclosure. Export viewing keys to give auditors selective access:

// Share specific notes only (Tier 1 — minimal disclosure)
const auditExport = await upp.exportViewingKeys({
  notes: [note1, note2],
})

// The export includes a nonce chain for completeness verification
// and an EIP-712 signed note count — auditors can verify nothing is hidden

The export JSON is consumable by the upp-audit CLI for automated compliance verification. See Viewing Keys and Audit & Compliance for details.

Notes

Each shield or transfer creates notes (encrypted UTXOs). The SDK tracks these automatically:

const notes = await upp.getNotes()
// Each note: { amount, token, commitment, spent, leafIndex }

Error Handling

try {
  await upp.transfer({ amount, to })
} catch (e) {
  if (e.code === 'INSUFFICIENT_BALANCE') {
    // Not enough private balance
  } else if (e.code === 'ASP_PROOF_FAILED') {
    // Origin not in any ASP allowlist — may need to ragequit
  } else if (e.code === 'PROOF_GENERATION_FAILED') {
    // ZK proof failed — check inputs
  }
}

Fees

UPP charges flat fees in UPD (pegged to 1)peroperatione.g.,1) per operation — e.g., 1 per SNARK transfer, 5perSTARKtransfer.FeesarepaidviaEIP3009signedauthorization(nostandingapprovals).Shieldandunshieldfeesareconfigurableanddefaultto5 per STARK transfer. Fees are paid via EIP-3009 signed authorization (no standing approvals). Shield and unshield fees are configurable and default to 0. Ragequit is always free.

See the Fee System page for details.

Next Steps

On this page