SDK Quickstart
Install the UPP SDK, look up the live Sepolia deployment, and run shield / transfer / withdraw — in under 10 minutes.
Pre-audit. Sepolia only.
UPP is pre-audit and only deployed to Sepolia. Read the Known Limitations page before doing anything beyond a friendly demo — permissionlessTokens is on, the M31 token-address collision attack is unmitigated, and several infrastructure dependencies (proof gateway, ASP service) are single-operator today.
1. Install
npm install @permissionless-technologies/upp-sdk viem wagmiPeer deps: viem is required, wagmi + React are only needed for the React hooks.
2. Sepolia deployment
The SDK ships the live Sepolia addresses; resolve them by chain ID:
import { getDeploymentOrThrow } from '@permissionless-technologies/upp-sdk'
const deployment = getDeploymentOrThrow(11155111)
// {
// UniversalPrivatePool, ASPRegistryHub, UPPSwapModule,
// implementations: { … },
// verifiers: { Transfer, Merge, Withdraw, JoinSplit, MergeTransfer2x2, MergeTransfer4x2 },
// TestToken, TestToken2,
// chainId: 11155111,
// deployBlock: 10846101,
// deployTimestamp,
// }For non-TypeScript callers or anything that wants raw addresses, here's the same set (cohort deployed 2026-05-18 for the V4 octary rollout — pool proxy is stable across the V4-aware impl upgrade):
| Contract | Address |
|---|---|
UniversalPrivatePool (proxy) | 0xCDA138FFd4789670e0034aE30946b8873bb51740 |
ASPRegistryHub (proxy) | 0xB71129021E6f1C45D206aa15233d9B1DcEfdeb97 |
UPPSwapModule (proxy) | 0xa2FD8Dc1DF8aa081D72115E761271Eb999032C73 |
FeeManager | 0xb163d4e917F21C7c04E9E99D8D69B5bD4AC1ABb8 |
StarkVerifierManager | 0x5EBd061a0cea81Fd3dd28451DBd524FB297FfB22 |
StarkPoolAdapter | 0x064B87B23F0CaFE525728d3C01b49B6f89A3Ac24 |
PlonkVerifierBundle | 0x08F475830a2bf906A06C22327f3827d0e37259aC |
TestStableToken (test fee token) | 0xFE10f8742a6A25df574f4556AB4db89ce3Bc8a2e |
TestStableToken2 | 0x611a55838d7e9E85c87986E0314ebDC0d20eb3Bc |
| Anchor | Value |
|---|---|
deployBlock (scan from here) | 10846101 |
| Chain ID | 11155111 |
STARK SP1 withdraw verifiers (V1 → V4 are all live alongside; the additive-upgrade pattern keeps every prior version registered):
| Version | Withdraw verifier | Transfer verifier |
|---|---|---|
| V1 (sp1 v3) | 0xe711b6f3C2e4669940E8EA1026B815d4EA3eB5eB | 0x018fDAF95041CB456856e8E094F14Aa7838CCA7F |
| V2 (sp1 v4) | 0x2707AEa32B8E03218CdC17299795ee7DE6918Bfa | 0xe74A40C4Fe4822227D26cd2Bc8a4B59B9cBc1eDa |
| V3 (sp1 v6, Track C) | 0x0c3422412658C21FdFa96612223FD0a1f5176B66 | 0x37Bc7E52DCff6BFd1b86d061eA32129814d7657A |
| V4 (octary tree) | 0x646eD1455f9863a48F8350181922fb533D511eE3 | 0x7A6764e31A4c1F704faAd4feDE2196A652a50ef5 |
The useStarkVerifierType(poolAddress) hook lists what's currently registered — use it to drive a verifier selector in the UI rather than hard-coding any one of the above.
Why deployBlock matters
The indexer scans from deployBlock forward to find encrypted notes. Starting earlier costs RPC time; starting later misses notes. The pool proxy is the same address as before the V4 upgrade, so use 10846101 even though the V4-aware impl is newer.
3. React quickstart (recommended path)
import {
UPPAccountProvider, useUPPAccount,
useShield, usePoolTransfer, useWithdraw,
} from '@permissionless-technologies/upp-sdk/react'
import {
encodeShieldTxFromBuildData,
encodeTransferTxFromBuildData,
encodeWithdrawTxFromBuildData,
getDeploymentOrThrow,
} from '@permissionless-technologies/upp-sdk'
import {
useAccount, useChainId, usePublicClient,
useSignTypedData, useWriteContract,
} from 'wagmi'
import { parseUnits } from 'viem'
const { UniversalPrivatePool: poolAddress, UPPSwapModule: swapModuleAddress, TestToken: UPD, deployBlock } =
getDeploymentOrThrow(11155111)
function App({ children }: { children: React.ReactNode }) {
const { address } = useAccount()
const chainId = useChainId()
const publicClient = usePublicClient()
const { signTypedDataAsync } = useSignTypedData()
return (
<UPPAccountProvider
ethAddress={address}
chainId={chainId}
publicClient={publicClient}
signTypedData={signTypedDataAsync}
indexerConfig={{ contractAddress: poolAddress, swapModuleAddress, fromBlock: deployBlock }}
activeProofSystem="snark"
>
{children}
</UPPAccountProvider>
)
}
function PrivacyPanel() {
const { address } = useAccount()
const { writeContractAsync } = useWriteContract()
const { isSetup, balance, addNote, markNoteSpent, connect } = useUPPAccount()
const shield = useShield({ poolAddress, tokenAddress: UPD })
const xfer = usePoolTransfer({ poolAddress, tokenAddress: UPD, publicClient: usePublicClient() })
const wdraw = useWithdraw({ poolAddress, tokenAddress: UPD, publicClient: usePublicClient() })
if (!isSetup) return <button onClick={() => connect()}>Unlock privacy layer</button>
const onShield = async () => {
const d = await shield.build({ amount: parseUnits('100', 18), originAddress: address! })
// ERC20 approve to the pool is yours to do
await writeContractAsync({ address: poolAddress, ...encodeShieldTxFromBuildData(d) })
addNote(d.noteData.note)
}
const onTransfer = async (recipient: string) => {
const d = await xfer.build({ amount: parseUnits('10', 18), recipient })
await writeContractAsync({ address: poolAddress, ...encodeTransferTxFromBuildData(d) })
markNoteSpent(d.spentNote.commitment)
addNote(d.changeNoteData.note)
}
const onRagequit = async () => {
const d = await wdraw.build({
amount: parseUnits('50', 18),
recipient: address!,
isRagequit: true,
})
await writeContractAsync({ address: poolAddress, ...encodeWithdrawTxFromBuildData(d) })
markNoteSpent(d.spentNote.commitment)
if (d.changeNoteData) addNote(d.changeNoteData.note)
}
return (
<div>
<p>Private balance: {balance.toString()} (wei)</p>
<button onClick={onShield}>Shield 100</button>
<button onClick={() => onTransfer('0zk1…')}>Transfer 10</button>
<button onClick={onRagequit}>Ragequit 50 (no ASP)</button>
</div>
)
}That's the whole golden path. The hooks own the proof + encryption; your component owns the wallet write. See the React Hooks page for the full surface (usePrivateBalance, usePersonalASP, useCircuitCache, swap hooks, passkey login).
4. Non-React quickstart (encoders directly)
import {
encodeShieldTx,
createNote, encryptNote,
generateStealthAddress,
deriveDualKeysFromSignature,
getDeploymentOrThrow,
} from '@permissionless-technologies/upp-sdk'
const { UniversalPrivatePool: poolAddress, TestToken: token } = getDeploymentOrThrow(11155111)
// 1. Derive keys from a one-shot EIP-712 signature (same input → same keys, every time)
const signature = await walletClient.signTypedData(EIP712_DOMAIN_AND_MESSAGE)
const { snark } = await deriveDualKeysFromSignature(signature)
// 2. Build a shielded note for self
const { stealthAddress, ephemeralPubKey } = generateStealthAddress(snark.metaAddress)
const note = createNote({ amount: 100n * 10n**18n, token, origin: depositor })
const encryptedNote = await encryptNote(note, ephemeralPubKey)
// 3. Encode + submit
const tx = encodeShieldTx({
token: note.token,
amount: note.amount,
ownerHash: note.ownerHash,
blinding: note.blinding,
encryptedNote,
})
await walletClient.writeContract({ address: poolAddress, ...tx })The same shape works for transfer and withdraw — see Transaction Encoders for the full encoder catalog (SNARK / STARK V3 / STARK V4, including the V4 commit/flush pair).
5. The current demo posture
This is a Sepolia demo, not a production deployment. A few specifics worth knowing before integrating:
permissionlessTokens = true
Any ERC20 can be shielded today. Combined with the M31 token-address collision finding, that's exploitable on the STARK rail. Mitigation for a production deployment is a single tx — setPermissionlessTokens(false) + addSupportedToken(<allowlist>), both gated by ADMIN_ROLE on the pool. We've left it on for the friendly-audience demo so anyone can pull from the TestStableToken faucet and try the round-trip.
Plan B-1 commit/flush is enabled by default
V4 transfers always go through transferSTARKV4Commit + flushPendingV4Inserts(1) because the atomic transferSTARKV4 exceeds every public RPC's 16.77M gas cap. The React hooks handle this transparently — for now the user has to click "Flush" once per output commitment. A background flusher service is on the roadmap.
All fees are zero
The FeeManager constructor doesn't seed defaults and no admin call has set any non-zero amount on Sepolia. Read the live amount with feeManager.feeAmount(kind) before assuming. The Fee System page documents the FeeKind enum and the EIP-3009 / relayer payment paths.
Single-operator infrastructure
prover.upd.io— STARK Groth16 wrapping happens on one operator-run gateway. Fault stalls STARK transfers.asp-whitelist.upd.io— one ECS task, one operator key, publishes the ASP root.
Both are documented under Known Limitations §5 / §6. Roadmap is to open-source the gateway image and decentralize ASP root publishing.
6. Self-hosting the ASP service
If you don't want to depend on asp-whitelist.upd.io, the service ships as @permissionless-technologies/upc-asp-whitelist and runs against any RPC + a Subsquid event source:
import {
startASPService,
SubsquidEventSource,
SanctionsGate,
} from '@permissionless-technologies/upc-asp-whitelist'
import { parseAbiItem } from 'viem'
const shieldedEvent = parseAbiItem(
'event Shielded(address indexed token, address indexed depositor, bytes32 indexed commitment, uint256 leafIndex, bytes encryptedNote)'
)
startASPService({
rpcUrl: process.env.RPC_URL!,
registryAddress: '0xB71129021E6f1C45D206aa15233d9B1DcEfdeb97',
operatorPrivateKey: process.env.OPERATOR_PRIVATE_KEY as `0x${string}`,
chainId: 11155111,
port: 3001,
eventSource: new SubsquidEventSource({
archive: 'https://v2.archive.subsquid.io/network/ethereum-sepolia',
apiKey: process.env.SQD_KEY, // ← required since mid-2026
rpcUrl: process.env.RPC_URL!,
watchAddress: '0xCDA138FFd4789670e0034aE30946b8873bb51740',
event: shieldedEvent,
addressTopicIndex: 2,
deployBlock: 10846101n,
finalityConfirmation: 2,
}),
gate: new SanctionsGate({ blocklist: [/* OFAC etc. */] }),
})Subsquid v2 archives require an API key
Subsquid gated the v2 archive endpoints in mid-2026. Without SQD_KEY (or the SDK's own SQD_API_KEY fallback) the service can't catch up from deployBlock — it'll fall back to RPC-only and run very slowly. Get a key at app.subsquid.io and set it in the service environment.
The operatorPrivateKey becomes the on-chain signer for ASP root updates against the registry above. The SanctionsGate is a pluggable filter; swap in your own gate.allows(origin) implementation to enforce a different policy.
7. Next steps
- V4 Octary State Tree — Why STARK V4 lives in its own depth-11 octary tree
- V4 Commit/Flush Transfers — Lifecycle behind the two-tx STARK transfer
- Known Limitations — Everything you should know before non-demo usage
- Audit & Compliance — Viewing-key exports +
upp-auditCLI - Fee System — FeeManager, FeeAuth, and the two payment paths
- SDK Reference — Full API surface (encoders, React hooks, indexer)
Universal Private Pool
Privacy layer for any ERC20 token — shared anonymity pool with SNARK and post-quantum STARK proofs, stealth addresses, and compliance-ready ASP verification.
Notes and Account Model
UPP uses a UTXO-style note model instead of account balances — encrypted UTXOs that prove ownership without revealing contents.