Private payments
for web3
Peek-a-boo is privacy infrastructure for web3 payments and AI agents. Shielded tokens, stealth addresses, zero-knowledge proofs, note encryption, viewing keys, and compliance proofs — across Bittensor, Base, Ethereum, and more.
01 Overview
When transactions happen on public blockchains, every payment, transfer, and routing decision is visible. Anyone can track who paid whom, how much, and when — whether it's a human wallet or an AI agent.
Peek-a-boo solves this with a privacy layer that works across chains. Users deposit tokens into a shielded pool, generate ZK proofs to withdraw without linkability, and use stealth addresses so recipients receive payment without revealing a persistent identity. The protocol is live on Bittensor EVM and supports Ethereum, BSC, Polygon, and Arbitrum via the Railgun adapter.
No observer can link a deposit to a withdrawal, or determine who paid whom. The ZK proof proves you had funds in the pool—without revealing which deposit was yours.
02 Quickstart
Peek-a-boo is a monorepo built with npm workspaces and Turborepo. All packages build to ESM + CJS + DTS via tsup.
| Tool | Version | Purpose |
|---|---|---|
| Node.js | v24.11.1 | Runtime |
| npm | 11.6.2 | Package manager |
| Turborepo | latest | Monorepo build orchestration |
| tsup | latest | Library bundler (ESM + CJS + DTS) |
| Vitest | latest | Test runner |
| Hardhat | latest | Smart contract toolchain |
| Circom | 2.1.9 | ZK circuit compiler |
| snarkjs | latest | Groth16 proving system |
03 Architecture
The protocol is organized as a layered stack. Types at the base, core privacy primitives in the middle, chain adapters on top, and an SDK that composes everything into a unified API.
Storage Abstraction
Every core module uses constructor-injected storage adapters. In-memory implementations ship as defaults. SQLite adapters are opt-in via the @peekaboopay/storage-sqlite package—core has zero SQLite dependency.
Cryptography
All cryptographic operations use @noble/curves and @noble/hashes—pure JavaScript with no native bindings. The library stays fully platform-agnostic: no node:crypto, no WASM, no system dependencies.
04 Policy Engine
The policy engine enforces spend constraints on agent transactions. Rules are evaluated in priority order—the first matching rule determines the outcome.
| Rule Type | Description |
|---|---|
| Spend Limit | Per-transaction and cumulative limits with configurable time windows |
| Time Bounds | Restrict transactions to specific time ranges with timezone support |
| Whitelist | Allow only specific destination addresses |
| Denylist | Block specific destination addresses |
| Chain Restriction | Limit which chains an agent can transact on |
Rules are composable and priority-ordered. An agent session might have a per-tx limit of 1 TAO, a daily cumulative limit of 10 TAO, and a whitelist restricting payments to verified miners only.
05 Session Manager
Sessions scope an agent's access to the privacy layer. Each session has a lifecycle:
Sessions auto-expire on access check if their TTL has elapsed. A background cleanup sweep removes stale sessions. Policy rules are attached per-session, so different agents (or different tasks within one agent) can have different spending constraints.
06 Shielded Treasury
The treasury manages a UTXO note set representing shielded balances. Each note is a commitment that can only be spent once—double-spend prevention uses nullifiers.
Each deposit creates a note (commitment = Poseidon(nullifier, secret)). Spending a note reveals its nullifierHash but not the original commitment—breaking the link between deposits and withdrawals.
The treasury supports multi-token balances, greedy UTXO selection for optimal transaction construction, and filtered transaction history with timestamps.
07 Stealth Addresses
Stealth addresses let miners receive payments without revealing a persistent public address. The protocol implements ERC-5564 using ECDH on secp256k1.
How it works
The sender generates a one-time address from the miner's meta-address. The miner scans on-chain announcements using their viewing key to detect payments—a viewTag enables fast filtering so miners only need to attempt full derivation on ~1/256 of all announcements.
08 Smart Contracts
Five Solidity contracts handle the on-chain privacy operations. All contracts are ready for deployment on any EVM-compatible chain.
| Contract | Purpose |
|---|---|
| ShieldedPool | Privacy pool with Poseidon Merkle tree, Groth16 ZK verification for withdrawals |
| StealthAnnouncer | ERC-5564 announcement registry for stealth address discovery |
| IncrementalMerkleTree | Gas-efficient Merkle tree library with Poseidon hashing |
| PoseidonHasher | On-chain Poseidon hash wrapper (matches circomlib) |
| Groth16Verifier | Auto-generated ZK proof verifier from snarkjs |
Deposit & Withdraw Flow
Deposits insert a commitment into the Poseidon Merkle tree. Withdrawals require a Groth16 proof demonstrating knowledge of the pre-image without revealing which deposit is being spent. A 30-root history buffer prevents race conditions between concurrent deposits and withdrawals.
Deploy Order
09 ZK Circuits
The withdrawal circuit is written in Circom and compiled to a Groth16 proving system. It follows the Tornado Cash pattern, upgraded from keccak256 to Poseidon hashing for ZK efficiency.
Circuit Design
The recipient and amount are bound to the proof via a square constraint trick. A front-runner cannot change the destination or amount without invalidating the proof.
10 Bittensor EVM
Bittensor's Subtensor EVM is the primary deployment target. The adapter supports both mainnet and testnet.
| Network | Chain ID | RPC | Currency |
|---|---|---|---|
| Mainnet | 964 | lite.chain.opentensor.ai | TAO (18 decimals) |
| Testnet | 945 | test.chain.opentensor.ai | TAO (18 decimals) |
The BittensorAdapter implements the PrivacyBackend interface—the same interface used by the Railgun adapter. Config widening via BittensorAdapterConfig extends BackendConfig allows chain-specific options without breaking the shared interface.
Deployed Contracts (Chain 964)
| Contract | Address |
|---|---|
| PoseidonT3 | 0x389B7E7d332d83157a580ead27884aa0CAB14815 |
| Groth16Verifier | 0x0CD5A7D426ED71D8d1e216FEEADBC2F6574D053D |
| ShieldedPool | 0xaf243B3bFc7D4cbD0e58fA175876fF51f7097f59 |
| StealthAnnouncer | 0xF6b3223aC0107e2bd64A982e0212C0b0751c269B |
Protocol fee: 0.5% on deposits and withdrawals.
11 Railgun Adapter
The Railgun adapter provides shielded ERC-20 transactions via the Railgun SDK's ZK-SNARK infrastructure. It uses a provider abstraction pattern—all SDK calls are isolated behind a single interface, making the adapter fully testable with mocks.
| Module | Responsibility |
|---|---|
| WalletManager | Engine initialization, wallet create/load |
| TransactionBuilder | Shield / unshield / transfer assembly |
| ProofService | Groth16 proof generation & verification |
| BalanceScanner | Shielded balance queries |
| ChainMap | ChainId ↔ NetworkName mapping |
Supported chains: Ethereum (1), BSC (56), Polygon (137), Arbitrum (42161).
12 Base L2
Base L2 uses our own ShieldedPool contracts — the same architecture as Bittensor, deployed natively on Base.
| Contract | Address |
|---|---|
| PoseidonT3 | 0x95C9521932F9Ed6bBF907b5e950C4BC7656d1439 |
| Groth16Verifier | 0xAC50E112F95fbf97bDAc64F5E0Ad1fcfe3a252be |
| ShieldedPool | 0xe01Aba8855c83f2A70eE0A0D7401F8B7DB289C86 |
| StealthAnnouncer | 0x1251E4E0B9c55406427aBef555dB580Da90D8A12 |
Chain ID 8453. Gas costs ~$0.01 per transaction. Protocol fee: 0.5%.
13 Package Map
20 packages total. 523 tests, all passing.
| Package | Description | Status |
|---|---|---|
@peekaboopay/types | Shared type definitions (9 modules) | Live |
@peekaboopay/core | Privacy engine, policy, sessions, treasury, stealth | 65 tests |
@peekaboopay/crypto | Cryptographic primitives, note encryption, key derivation | 48 tests |
@peekaboopay/storage-sqlite | SQLite persistence adapters | 41 tests |
@peekaboopay/contracts | Solidity contracts + ZK circuits | 41 tests |
@peekaboopay/adapter-bittensor | Bittensor Subtensor EVM adapter | 40 tests |
@peekaboopay/adapter-railgun | Railgun privacy backend | 74 tests |
@peekaboopay/x402 | Shielded x402 payment scheme | 21 tests |
@peekaboopay/sdk | Universal developer-facing API | Wired |
@peekaboopay/mcp-server | MCP server for agent tools | Wired |
@peekaboopay/settlement | L1/L2 providers, gasless relay | Stub |
@peekaboopay/adapter-ethereum | Native Ethereum L2 privacy | Stub |
@peekaboopay/adapter-aztec | Aztec private execution | Stub |
@peekaboopay/agent-crewai | CrewAI tool wrappers | Stub |
@peekaboopay/agent-langchain | LangChain tool wrappers | Stub |
14 SDK & MCP Server
The @peekaboopay/sdk provides a universal privacy interface wired to all chain adapters. Methods call real backends — shield, unshield, transfer, prove, and balance queries all work end-to-end. The @peekaboopay/mcp-server exposes these as MCP tools with a CLI entry point: npx peekaboopay-mcp.
Framework-specific wrappers for CrewAI and LangChain are scaffolded in @peekaboopay/agent-crewai and @peekaboopay/agent-langchain. These translate SDK calls into framework-native tool interfaces.
15 SDK Reference
The PASClient class is the primary developer-facing interface. It wraps a PrivacyBackend and exposes high-level operations for transactions, identity, and policy management.
Constructor & Lifecycle
Transaction API
| Method | Params | Returns |
|---|---|---|
pay(params) | { recipient, amount, token, memo? } | PASResult<PaymentReceipt> |
receive(params) | { token, singleUse? } | PASResult<ReceiveAddress> |
swap(params) | { fromToken, toToken, amount, slippageBps? } | PASResult<SwapReceipt> |
bridge(params) | { token, amount, fromChain, toChain } | PASResult<BridgeReceipt> |
Identity API
| Method | Params | Returns |
|---|---|---|
prove(id, disclosure) | credentialId: string, disclosure: DisclosureRequest | PASResult<Proof> |
credential(cred) | Credential { type, claims, issuer } | PASResult<string> (ID) |
disclose(attrs) | attributes: string[] | PASResult<Proof> |
Policy API
PrivacyBackend Interface
Every chain adapter implements this interface. Swap backends without changing application code.
16 MCP Tools
The @peekaboopay/mcp-server exposes 10 tools via the Model Context Protocol. Any MCP-compatible agent runtime can call these directly—no SDK integration required.
Transaction Tools
| Tool | Description | Inputs |
|---|---|---|
pas_pay | Send a shielded payment—no on-chain link between sender and recipient | recipient, amount, token, memo? |
pas_receive | Generate a single-use stealth receive address | token, singleUse? |
pas_swap | Private token swap within the shielded pool | fromToken, toToken, amount, slippageBps? |
pas_bridge | Bridge tokens privately between chains | token, amount, fromChain, toChain |
Treasury Tools
| Tool | Description | Inputs |
|---|---|---|
pas_get_balance | Query shielded balance—computed locally, never exposed on-chain | token |
pas_shield_funds | Move funds from a public address into the shielded pool | token, amount |
pas_unshield_funds | Withdraw from the shielded pool to a public address | token, amount, recipient |
Identity Tools
| Tool | Description | Inputs |
|---|---|---|
pas_prove | Generate a ZK proof for a credential without revealing underlying data | credentialId, attributes[] |
pas_credential_store | Store a ZK-provable credential in the vault | type, claims |
pas_disclose | Selectively reveal specific attributes for compliance | attributes[] |
MCP Resources
Read-only resources for querying protocol state without invoking tools.
| URI | Description |
|---|---|
pas://balance/{token} | Current shielded balance for a specific token |
pas://policies | Currently active policy rules |
pas://credentials | Available ZK-provable credentials |
pas://session | Current privacy session state |
All MCP tools accept amounts as string (not number) to preserve precision with large token values. Use Wei-denominated strings for TAO and ERC-20 tokens.
17 Note Encryption
Deposit notes (nullifier + secret + amount) are encrypted using ECDH + AES-256-GCM before being stored or emitted on-chain. This enables cross-session and cross-device recovery — scan on-chain events with your viewing key to find and decrypt your shielded funds.
Encryption Flow
The sender generates an ephemeral secp256k1 keypair, performs ECDH with the recipient's viewing public key, derives an AES-256 cipher key via keccak256, and encrypts the 96-byte payload (nullifier + secret + amount). The encrypted note and ephemeral public key are stored on-chain.
API
| Function | Description |
|---|---|
encryptNote() | Encrypt a deposit note for a recipient's viewing key |
decryptNote() | Decrypt using viewing private key |
tryDecryptNote() | Safe scan — returns null if key doesn't match |
18 Viewing Keys
Peek-a-boo implements a hierarchical key system that separates spending authority from viewing authority. Share viewing keys with dashboards and auditors without risking your funds.
Key Hierarchy
API
| Function | Description |
|---|---|
deriveKeySet(seed) | Derive full key hierarchy from 32-byte seed |
generateKeySet() | Generate a fresh random key set |
exportViewingKey(keySet) | Export viewing-only keys (no spending authority) |
hasSpendingAuthority(keySet) | Check if a key set can spend |
19 Compliance Proofs
Proof of Innocence — prove your shielded funds did NOT originate from sanctioned addresses, without revealing which deposit is yours. Privacy meets compliance.
How It Works
A sorted Poseidon Merkle tree of sanctioned addresses enables exclusion proofs. The user proves their deposit address falls between two adjacent sanctioned entries — demonstrating non-membership without revealing the address itself. An attestation hash binds the commitment, sanctioned set root, and timestamp via Poseidon hashing.
API
| Function | Description |
|---|---|
SanctionedSet | Sorted Poseidon Merkle tree of sanctioned addresses |
generateComplianceAttestation() | Generate attestation that deposit is not sanctioned |
verifyComplianceAttestation() | Verify an attestation's validity |
Compliance attestations are self-contained. Anyone can verify the exclusion proof and attestation hash without access to the depositor's identity or the full sanctioned set.
20 Lit Protocol
Decentralized key management via Lit Protocol. Agents sign transactions through Lit's threshold network — no raw private key is ever held locally. Keys are split across decentralized TEE nodes using Multi-Party Computation. More than 2/3 of nodes must combine their shares to sign.
Why Lit?
When an agent holds a raw private key in memory, a compromised process means stolen funds. Lit eliminates this — the full key never exists in any single location. Signing policies are enforced cryptographically by the network, not by SDK-level checks that can be bypassed.
PKP Signer (Drop-in Replacement)
createPKPSigner() returns an ethers.Signer backed by Lit's Programmable Key Pair. It's a drop-in replacement for ethers.Wallet — all existing adapter calls work unchanged.
Wrapped Keys
Import existing private keys into Lit's encrypted storage. The key is encrypted with Lit's BLS key, stored in their infrastructure, and only decrypted inside a TEE during signing — then wiped from memory.
| Function | Description |
|---|---|
wrapPrivateKey() | Import an existing key into Lit's encrypted storage |
generateWrappedKey() | Generate a new key entirely within Lit's TEE |
signWithWrappedKey() | Sign a transaction — key decrypted in TEE, wiped after |
Programmable Policies
Lit Actions are JavaScript programs that run inside Lit's TEE before signing. They enforce policies that can't be bypassed by the agent — spending limits, recipient whitelists, chain restrictions, and cooldown periods are cryptographically guaranteed.
| Function | Description |
|---|---|
createPolicyAction(policy) | Generate a Lit Action from a policy config |
SHIELD_ACTION | Pre-built: max deposit amount enforcement |
UNSHIELD_ACTION | Pre-built: daily limits + cooldown periods |
TRANSFER_ACTION | Pre-built: recipient whitelist enforcement |
With Lit Protocol, the agent's private key never exists on any single machine. Threshold signatures across decentralized TEE nodes ensure that even a fully compromised agent process cannot steal funds.
21 Relayer Network
Meta-transaction relay for stealth addresses. When a stealth address receives shielded funds, it has no ETH/TAO for gas. Funding it from a known wallet would break privacy. The relayer solves this — the recipient signs an EIP-712 request, the relayer submits the transaction and pays gas, deducting a small fee from the withdrawal.
How It Works
API
| Function | Description |
|---|---|
createRelayClient(config) | Create a relayer instance with RPC + private key |
signRelayRequest(signer, request) | EIP-712 sign a relay request from the stealth address |
submitRelayRequest(client, request) | Relayer verifies signature, submits tx, deducts fee |
verifyRelaySignature(request) | Verify a signed relay request is valid |
estimateRelayFee(client, request) | Estimate gas cost + relay fee before submitting |
buildWithdrawMetaTx(params) | Build a relay request for ShieldedPool withdrawal |
buildAnnounceMetaTx(params) | Build a relay request for StealthAnnouncer |
Relay requests use EIP-712 typed data signatures with nonce replay prevention and deadline enforcement. The relayer cannot modify the transaction — it can only submit what the stealth address holder signed. Maximum relay fee is capped at 5% (500 basis points).
22 Denomination Pools
Fixed-amount deposit pools following the Tornado Cash pattern. Every deposit in a pool is exactly the same amount — 0.1, 1, or 10 TAO. This creates much larger anonymity sets than variable-amount deposits, because all deposits and withdrawals look identical.
Why Fixed Denominations?
With variable amounts, an observer can correlate deposits and withdrawals by matching amounts. If you deposit 1.37 TAO and someone withdraws 1.37 TAO, that's a potential link. With fixed denominations, every deposit of 1 TAO looks identical to every other 1 TAO deposit — the anonymity set is everyone in the pool.
DenominatedPool.sol
A simplified ShieldedPool contract that enforces exact deposit amounts. deposit() requires msg.value == denomination. withdraw() always returns the denomination amount (minus the 0.5% protocol fee). No amount parameter needed — the contract knows exactly how much to send.
| Pool | Denomination | Use Case |
|---|---|---|
| Pool A | 0.1 TAO | Small payments, micro-transactions |
| Pool B | 1 TAO | Standard payments, inference fees |
| Pool C | 10 TAO | Larger transfers, bulk operations |
SDK Helpers
| Function | Description |
|---|---|
DENOMINATIONS | Standard denomination constants in wei (TAO_01, TAO_1, TAO_10, ETH_01, ETH_1) |
suggestDenomination(amount) | Optimal split for any amount into fixed denominations |
denominationLabel(amount, symbol) | Human-readable label (e.g., "1 TAO") |
isExactlyDenominatable(amount) | Check if amount splits evenly with no remainder |
totalDepositsNeeded(amount) | Number of deposits needed for a given amount |
DenominatedPool contracts deploy to any EVM chain. The same contract works on Bittensor (TAO pools) and Base L2 (ETH pools). All three denomination sizes share the same Groth16Verifier and PoseidonT3 library.
23 Nym Mixnet
Network-level privacy via the Nym mixnet. ZK proofs hide on-chain links and Lit Protocol protects keys — but RPC calls still leak IP addresses, timing, and metadata to providers and network observers. Nym closes this final gap by routing all blockchain traffic through a decentralized mixnet with packet mixing and dummy traffic.
Three Privacy Layers
| Layer | What's Protected | How |
|---|---|---|
| On-Chain | Who paid whom | ZK proofs + stealth addresses + denomination pools |
| Key Management | Agent's private keys | Lit Protocol threshold signing (no raw keys) |
| Network | IP, timing, metadata | Nym mixnet routing (mixFetch + SOCKS5) |
Two Modes
mixFetch — Drop-in replacement for fetch(). Routes HTTP RPC calls through the Nym mixnet. Best for browser-based and lightweight agents. Requires @nymproject/sdk.
SOCKS5 — Routes all TCP traffic through a local Nym SOCKS5 proxy. Best for Node.js and server-side agents. Requires the nym-socks5-client running locally.
Auto-detect — mode: "auto" checks if the Nym SDK is available and uses mixFetch; otherwise falls back to SOCKS5.
Usage
API
| Function | Description |
|---|---|
createNymProvider(config) | Create an ethers provider routed through Nym |
createNymClient(config) | Manage Nym mixnet client lifecycle |
detectMode() | Auto-detect mixFetch vs SOCKS5 availability |
nymProvider.send(method, params) | Send JSON-RPC call through mixnet |
nymProvider.getInfo() | Get connection info (mode, address, status) |
nymProvider.disconnect() | Disconnect from Nym network |
With Nym integrated, Peek-a-boo provides privacy at every layer: on-chain (ZK proofs), key management (Lit Protocol), and network (Nym mixnet). An agent using all three is untraceable — the blockchain can't link payments, no single node holds the key, and the network can't see who's making RPC calls.