PrettyGoodPrivacy

Fund a fresh wallet through a privacy pool, then trade it ! origin hidden, trade public.

PrettyGoodPrivacy

Created At

ETHGlobal New York 2026

Project Description

Unlink × Uniswap is a live, testnet demo of gasless, unlinkable on-chain trading. On public chains every trade is permanently tied to the wallet that made it — which lets anyone profile a trader, copy their moves by reputation, and link their activity back to their identity. This project shows how to break that link while keeping the trade fully public and verifiable.

The flow: funds are deposited into Unlink's privacy pool (a shielded account layer), then a trade is executed on Uniswap v3 from a brand-new ERC-4337 smart account that is funded from the pool and whose gas is sponsored by a relayer. The result is that the swap is visible to everyone on-chain, but the trading account has no funding-graph connection to the origin wallet, and the user spends no ETH for gas. In short: the trade is public, the trader's origin is hidden, and there's no persistent address for copy-bots to follow.

It runs on Base Sepolia (chain 84532), where both Unlink and Uniswap are deployed. The web app walks through each step — preflight checks that both protocols are live, deposit, the gasless execute, and a verification step that confirms gas was paid by a relayer (not the origin) and the trading account differs from the origin. Every on-chain action surfaces a "Verify on BaseScan" link so anyone can confirm it independently. An optional MetaMask mode lets you run a normal swap from your own wallet — public and linkable to you — as a deliberate contrast to the unlinked path.

Built with the Unlink SDK, Uniswap v3, viem, and ERC-4337 execution accounts; hosted on Vercel as a static frontend plus serverless API functions (the privacy-layer admin key stays server-side). Testnet only.

How it's Made

Stack & architecture. Everything runs on Base Sepolia (84532), the one testnet where both Unlink and Uniswap are live. The app is a build-free static frontend (vanilla JS/CSS) plus Vercel serverless functions (api/.ts) that wrap a shared TypeScript core (src/actions.ts). A local node:http dev server dispatches /api/ to the exact same handlers, so local behaves identically to prod. viem handles all chain reads/writes and ABI encoding.

The gasless core (Unlink). We use the Unlink SDK in a server/custodial setup: createUnlinkAdmin (holds the API key, registers users, issues auth tokens) + createUnlinkClient (account.fromMnemonic, evm.fromViem). The unlinkable+gasless trade is a single primitive — client.execute({ token, amount, calls }) — which privately withdraws pool funds into a fresh ERC-4337 execution account and runs an atomic [approve, swap] batch with relayer-sponsored gas. Unlink was the key partner tech: it gave us shielded funding and account-abstraction gas sponsorship in one call. Without it we'd have had to stitch together a mixer + a paymaster + an AA bundler ourselves.

Uniswap. Swaps go through Uniswap v3 SwapRouter02.exactInputSingle. Notable hack: we set the swap recipient to Uniswap's MSG_SENDER sentinel (0x…01) so the output routes to the execution account without us needing to know its counterfactual address up front — that let us drop the reserve() + by_index dance entirely.

Vercel. Static public/ + serverless api/, with the Unlink admin key as an encrypted env var that never reaches the browser. vercel.json sets maxDuration: 60 so the execute() poll has room to finish.

Hacky / notable bits:

  • execute() is undocumented, so we reverse-engineered the exact params/shape from the SDK's TypeScript .d.ts files.
  • Debugging by on-chain probing. We first targeted Monad testnet, then found its Uniswap deployment had been wiped (a reset) by checking eth_getCode at the documented addresses across 4 RPCs — all empty. Pivoted to Base Sepolia and built a preflight that verifies on-chain code + the Unlink engine before anything runs.
  • "tenant not provisioned" turned out to be the wrong environment — we probed the API key against every Unlink env and found it was base-sepolia-production, not -staging. (No provisioning step exists; just a name mismatch.)
  • Auto-discovered the test token address from the pool balance (getBalances) because the dashboard truncated it.
  • No pool existed for the test token, so we wrote a one-time seeding script: createAndInitializePoolIfNecessary at a 1:1 price + a full-range mint (ticks ±887220, aligned to the fee-60 spacing).
  • Serverless-safe state. Vercel's FS is read-only except /tmp and functions don't share state, so state writes are best-effort, gaslessTrade re-resolves the pool fee from the factory itself, and verify is a fast tx-based check with the client carrying context (origin/fresh/swap tx) between calls.
  • Per-step BaseScan receipts and an optional MetaMask mode (viem loaded from esm.sh in-browser, build-free) that does a normal linkable swap from your own wallet as a deliberate contrast.
background image mobile

Join the mailing list

Get the latest news and updates

PrettyGoodPrivacy | ETHGlobal