Reference

For developers

Programs, accounts, the three-signer model and the SDK.

Programs

ProgramRole
ztonk-shieldFixed-size shielded SOL pools: Poseidon commitment tree, Groth16 withdrawals verified on chain, one-time-address announcements.
ztonk-launchpadToken launches and bonding curves, built so a trade can be signed, paid for and funded by three different parties.
ztonk-ammThe pools tokens graduate into, with the same three-party split.

Addresses

ProgramAddress
ztonk-launchpadGq7Mshz4MoWsMU3Cm8V2prvBwW4Ak8K4wwGXaCWKhNdT
ztonk-amm4d5NAbPM4kDY1iM3xYQyBAeyLWzZMsubEVTz36TUswBZ
ztonk-shieldSGwFmWvovDfGEH1TNrUuV8U6g6moGbGT9hnMeR6hSf6

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:

SignerRoleOn a private trade
traderOwns the tokens; must sign so nobody can redirect themThe fresh wallet — holds no SOL
payerPays network fees and new-account rentA relayer
funderSupplies SOL for a buy, receives SOL from a sellThe 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

EndpointWhat it does
GET /api/relayerThe relayer's address and fee. Both go into the proof as public inputs.
POST /api/relay/withdrawSubmits a withdrawal proof. It can only send it as-is: recipient, relayer and fee are fixed by the proof.
POST /api/relay/migrateGraduates 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

AccountSeeds
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

Deposit, then withdraw to a fresh wallet
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);
One-time addresses
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?