For developers
Programs, accounts, the three-signer model and the SDK.
Programs
| Program | Role |
|---|---|
| ztonk-shield | Fixed-size shielded SOL pools: Poseidon commitment tree, Groth16 withdrawals verified on chain, one-time-address announcements. |
| ztonk-launchpad | Token launches and bonding curves, built so a trade can be signed, paid for and funded by three different parties. |
| ztonk-amm | The pools tokens graduate into, with the same three-party split. |
Addresses
| Program | Address |
|---|---|
| ztonk-launchpad | Gq7Mshz4MoWsMU3Cm8V2prvBwW4Ak8K4wwGXaCWKhNdT |
| ztonk-amm | 4d5NAbPM4kDY1iM3xYQyBAeyLWzZMsubEVTz36TUswBZ |
| ztonk-shield | SGwFmWvovDfGEH1TNrUuV8U6g6moGbGT9hnMeR6hSf6 |
Shield pools: 0.1, 1, 10 and 100 SOL. Relayer fee cap: 5% of the pool size. The same addresses are used on devnet and mainnet.
The three-signer model
This is what makes private trading possible. Where most programs have one signer do everything, every value-moving instruction here takes three:
| Signer | Role | On a private trade |
|---|---|---|
trader | Owns the tokens; must sign so nobody can redirect them | The fresh wallet — holds no SOL |
payer | Pays network fees and new-account rent | A relayer |
funder | Supplies SOL for a buy, receives SOL from a sell | The shield's withdrawal recipient |
The site takes the simplest safe path: the relayer submits the withdrawal to your private wallet, and the private wallet then signs its own trades as all three. The split exists so other clients can go further — for example a relayer paying the fees of a trade from a wallet that holds only tokens.
Relayer API
| Endpoint | What it does |
|---|---|
GET /api/relayer | The relayer's address and fee. Both go into the proof as public inputs. |
POST /api/relay/withdraw | Submits a withdrawal proof. It can only send it as-is: recipient, relayer and fee are fixed by the proof. |
POST /api/relay/migrate | Graduates a full curve. Anyone can do this on chain; the relayer just pays for it. |
GET /api/shield?denomination= | Every deposit in a pool, in order. Your browser rebuilds the tree and checks its root on chain before proving. |
Withdrawal public inputs
The withdraw circuit exposes five public inputs, in this order. Binding the recipient, relayer and fee is what stops a relayer from rewriting the payout:
[ root, nullifier_hash, recipient_hash, relayer_hash, fee ] recipient_hash = Poseidon(pubkey[0..16], pubkey[16..32]) relayer_hash = Poseidon(pubkey[0..16], pubkey[16..32])
Addresses are hashed in two halves because a 32-byte Solana address doesn't fit in a single BN254 field element, and splitting keeps distinct addresses distinct.
Shield accounts
| Account | Seeds |
|---|---|
| Pool (one per size) | "shield_pool", size_lamports as u64 LE |
| Verifying key | "vk" |
| Spent note | "nullifier", pool, nullifier_hash |
| Deposit record | "commitment", pool, commitment |
Pools keep only the tree's frontier on chain, not every leaf. To build a proof, rebuild the tree from the pool's Deposited events, which carry each commitment and its index.
SDK
import { createNote, encodeNote, deriveNote, CommitmentTree, proveWithdrawal } from "@ztonk/sdk";
const note = createNote(1_000_000_000n); // 1 SOL pool
save(encodeNote(note)); // "ztonk-1000000000-…" — the only key
const { commitment } = await deriveNote(note); // goes on chain with the deposit
// later
const tree = await CommitmentTree.fromLeaves(depositedEvents.map((e) => e.commitment));
const path = tree.proofFor(tree.indexOf(commitment));
const proof = await proveWithdrawal(note, path, freshWallet, relayer, fee, artifacts);import { generateMetaKeys, deriveStealthAddress, checkAnnouncement } from "@ztonk/sdk";
const me = generateMetaKeys(); // publish spendPub + viewPub
const out = deriveStealthAddress(me); // sender: fresh address + announcement
const hit = checkAnnouncement(me, out.ephemeralPub, out.viewTag); // recipient: is it mine?