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

Export viewing keys for specific notes to give auditors selective read access:

const auditExport = await upp.exportViewingKeys({
  notes: [note1, note2], // Only these notes become visible
})
// Send auditExport to auditor — they can decrypt these notes only

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
  }
}

Next Steps

On this page