A pool holds one amount and nothing else. Let people choose their own figure and the figures alone sort everybody back out. Take the deepest pool your budget allows — the deposit count is your privacy, and no proof system tops it up. The note is created here, in this tab, before a lamport moves, and it is the only thing that can ever bring the deposit back out.
Two random numbers, drawn in this tab and hashed into one commitment. The chain stores the hash and never the numbers. They are not uploaded, not logged, and not recoverable from anything the server holds. Write them down now; there is no second copy to issue.
-
-
-
-
The commitment is a one-way hash. Nobody can run it backwards to recover the two numbers, and nobody can tell two commitments apart - which is exactly why your deposit blends into every other one.
This is the only thing that can withdraw your money. It is not stored anywhere. If you lose it, the funds are gone permanently - there is no reset, no support, no recovery. Nobody can help you, including us.
Copy it into a password manager, or write it down. This page deliberately offers no file download: a tool that trains you to save files it hands you is a tool that teaches a habit worth exploiting.
Your wallet sends the amount plus the commitment. The contract appends the commitment to the Merkle tree and pools your funds with everyone else's.
Wait as long as you can before doing this. Withdrawing minutes after depositing links the two by timing alone, no matter how good the cryptography is.
Your withdrawal path is built from the pool's leaves. They are public chain data, so any indexer for this program can serve them - and whatever is served is checked against the on-chain root in your browser before it is used. If this front-end is down or you do not trust it, point somewhere else.
Use an address with no history. Sending to a wallet that already holds your funds undoes everything you just paid for.
Your browser builds a zero-knowledge proof of one statement: “I know the secrets behind some leaf in this tree.” Not which leaf. That word is what breaks the link. This takes a few seconds and runs entirely on your machine.
------The pool hides which deposit is yours. It cannot hide when: a withdrawal moments after a deposit is matched to it by the clock alone. Waiting is what breaks that, and it costs nothing but time.
A withdrawal is indistinguishable from any other deposit in the same pool, so the anonymity set is simply the number of deposits. A pool with a handful of deposits provides essentially no privacy - timing alone deanonymises it.
Deposits do not drop straight into a pool here. They arrive at a holding wallet run by this service, outside the contract entirely, and sit there until they are accepted or sent back.
The sequence exists because a pool deposit is final. The program stores no sender, and it has no refund instruction — and it should not, since any instruction capable of returning your money is an instruction capable of moving somebody else's. The holding wallet is the last moment at which anything is still reversible.
Accepted, the amount is broken into whole denominations and deposited: 1.5 SOL goes in as one 1 SOL note plus four 0.1 SOL notes, and anything left under the smallest denomination stays put. Pools accept exact multiples only. Declined, the SOL goes back to the address it came from, or to one you nominate if it arrived from an exchange that cannot receive a return.
The holding step is a property of how SolMix is operated, not of the program. The contract never learns it happened. Everything after this point is the pool.
Your browser draws two random field elements — a nullifier and a secret — and hashes them into commitment = Poseidon(nullifier, secret). The program takes a fixed amount of SOL plus that hash, appends the hash to a Merkle tree twenty levels deep, and the SOL joins a single shared vault.
The pair of numbers is the note. Nothing ever sends it anywhere.
Every deposit looks the same as every other: one amount, and a hash that discloses nothing. Each later arrival makes the crowd around yours bigger. The crowd is what protects you; the cryptography only guarantees the crowd is genuine.
A withdrawal is a Groth16 proof of one sentence: I hold the nullifier and secret behind some leaf in this tree. The secrets and the Merkle path stay in your browser. Exactly three values go public — the root, a nullifierHash and an extDataHash.
Everything hinges on some. The proof establishes that a matching leaf exists and that you own its secrets. It never identifies the leaf, so there is no thread to pull.
On chain the program runs the pairing check — about 110,000 compute units through Solana's BN254 syscall — confirms your root is one of the last hundred it recorded, and creates an account keyed by the nullifierHash.
That account is the double-spend guard. Present the same note again and the account is already there, so the transaction fails. Because the hash derives from the nullifier only and never from the commitment, it proves a note was spent without revealing which note.
Wallet A put 1 SOL in.
Someone who knew a valid secret took 1 SOL out.
Nothing links those two rows — not an address, not an amount, not a timestamp that has to match. All that remains is arithmetic on the crowd size. One of 400 is real cover. One of 2 is a name tag.
"How anonymous am I" has two answers. Quoting only the friendlier one is how these services flatter themselves:
One of all the deposits in the pool. Deposits placed by the operator count here in full, because from outside they are indistinguishable from yours.
One of the deposits whose notes the operator does not already hold. They recognise their own withdrawals instantly, and whatever is left over is yours.
Both figures appear on the pool picker. While a pool is thin, treat the smaller one as the true one.
A proof that said nothing but "I know a leaf" could be grabbed in flight and pointed at another wallet. So the recipient, the relayer and the fee are hashed into extDataHash and bound into the proof itself. Alter any of the three and verification fails. That is precisely why a stranger can broadcast your withdrawal and still be unable to take it.
A fresh wallet owns no SOL, so it cannot pay for its own transaction, and funding it from your old wallet re-draws the exact line you paid to erase. A relayer sends the transaction instead and deducts its fee from the withdrawal. Nothing is signed by you. The destination ends up holding one transaction and showing no funder.
Measured rather than estimated — scripts/measure-costs.js reads the lamport deltas off a real deposit and a real withdrawal:
| step | lamports | SOL |
|---|---|---|
| deposit transaction fee | 4,992 | 0.000005 |
| nullifier record (permanent) | 1,002,240 | 0.001002 |
| withdrawal transaction fee | 5,000 | 0.000005 |
| relayer margin at its floor | 15,000 | 0.000015 |
| total per note | 1,027,232 | 0.001027 |
97% of it is the nullifier record, and it is not anybody's margin: the entry that marks a note spent has to survive for as long as the pool exists, or the note could be spent twice. That is rent paid to the chain.
The charge does not move with the amount, so the pool you pick decides how it feels: the same ~0.001 SOL is 1% of a 0.1 SOL mix and 0.01% of a 10 SOL one.
Leaves are not stored on chain at all — the program keeps the root and one node per level. An indexer replays the deposit events to rebuild the tree so your browser can assemble a path through it.
Your note is not on this server and never has been. What the site hands out is the leaf set, which is public chain data — any indexer for this program can serve it, and the Leaf source control on the withdraw tab lets you name a different one.
Whatever that source returns is checked against the on-chain root in your browser before any proof is built, so a doctored leaf set is thrown out rather than used. That is verification, not trust.
Stated honestly: no fallback indexer is configured yet, so in practice there is one. The mechanism is real; running a second copy elsewhere is a deployment decision nobody has taken.
Skip the maths. Here is the shape of it.
In this cloakroom there is a counter before the rail. Coats stop at the counter while somebody decides whether to accept them.
Accepted, the coat goes on the rail and you get the ticket. Refused, you get the coat back, and if you arrived by a route that cannot take a return you can name another address.
The counter exists for one reason: once a coat is on the rail, a ticket is the only thing that gets it off. Nobody working there can tell whose coat is whose — that is the whole design succeeding — so there is no handing one back later.
Coat in, numbered ticket out. Later the ticket buys a coat back. Your name never appears.
Now imagine every coat is the same coat. You leave with a coat instead of your coat, which costs you nothing and costs the person watching the door everything: there is no longer any way to pair a coat with a customer.
That is what fixed amounts buy. Put one 7.3 SOL deposit in a pool of 1 SOL deposits and you have marked precisely one person worth following.
Here is the part that sounds impossible. The ticket is never handed over, because a ticket could be traced back to the moment it was issued. What you hand over is a claim:
“I am holding a valid ticket for this coat check.”
You demonstrate the claim is true without producing the ticket and without narrowing it to one of them. The attendant is satisfied, hands over a coat, and finishes the exchange knowing nothing about which coat was yours. That is a zero-knowledge proof — arithmetic, not sleight of hand.
Then what stops you claiming ten coats? Collecting means surrendering one unforgeable scrap — a serial number derived from your ticket — which is written into a book. The scrap leads nowhere near the coat you handed in, but a repeat is spotted the instant it appears.
That book is permanent, and buying your single line in it forever is where almost the whole fee goes.
Collection costs a small handling charge. Pay it from your own pocket and the attendant sees your wallet again, which throws away everything you just bought.
So a courier collects for you and takes the charge out of the coat. The claim names that courier and that exact charge, so nothing more can be taken and nowhere else can be delivered to.
Be the only customer and everybody knows whose coat it is, whatever the proof says. Be one of a thousand and you are one of a thousand.
The crowd is the privacy. The maths just makes the crowd genuine. Walk in and back out a minute later and the clock identifies you no matter how full the room is.
Put a few names in, as though labelling coats. Mix them, and see what is left for somebody watching from outside.
Roughly 0.001 SOL — ten cents or so — to go in and come back out. Nearly all of it buys the permanent book entry that blocks a second claim. It has to last indefinitely, so it is bought once, outright.
Small mix or large mix, the charge is identical, which means it bites hardest on the small ones. Use the largest pool you are comfortable using.
Most coats on this rail were hung there by the operator. They can identify their own on sight, so a coat leaving that they do not recognise says exactly whose it was.
To someone watching the door: one of many. To the operator: possibly one of one. The pool picker prints both figures, because printing only the good one would be a lie.
The cloakroom needs a lock, and it has to exist before the first coat arrives. A single locksmith could keep the shape of the master key, so the lock is passed between several unrelated strangers instead, each cutting their own secret pattern into it before passing it on. Making a master key afterwards would take every one of those patterns at once.
Which is what puts the lock beyond everyone, including the people who made it. No contributor can open it alone — not a stranger, not a contributor, and not whoever operates SolMix.
The last cut uses something nobody could have arranged beforehand: the fingerprint of a Bitcoin block that had not yet been mined. Going last is no advantage when you cannot predict what you are mixing with.
And every step is checkable by you. Each contributor publishes a fingerprint of the lock as they handed it on, somewhere this service has no ability to edit. Compare it with the chain on the Proof & trust tab.
Your ticket was never on the premises: two numbers that only ever existed on your own machine. That is why losing them is final, and why nobody else can lose them for you.
What the building publishes is the list of coats on the rail, and that list is public. Anyone may keep a copy, and the withdraw tab lets you point at somebody else's. Your browser measures any list against the rail itself before believing a word of it, so a forged list is caught rather than used.
Using any mixer means accepting a pile of claims you have no way to inspect. This page separates the ones you can check from the ones you cannot.
Check the deployed bytes for yourself: node scripts/verify-deployment.js pulls the on-chain program, diffs it against a local build, and confirms the ceremony key baked into it.
No amount of reading the source settles this part, which is why it gets its own section. It also has to be correct before money moves — there is no fixing it retrospectively.
The problem. A proof system needs public parameters — the proving key — and generating them requires secret randomness. That randomness is toxic waste: once the key exists it has to be destroyed. Anyone who kept a copy can manufacture a valid proof for a deposit that was never made. The pool pays out, the proof verifies, and the chain shows nothing odd. No contract audit will ever surface it.
The fix. Hand the job to several people in sequence rather than one. Each takes the previous file, folds in fresh randomness of their own, and passes it along. Forging afterwards would need every secret in the chain. Which gives:
One honest link is enough. If even a single contributor really destroyed their randomness, the setup holds regardless of what the others did.
So both the number of contributors and their independence matter. A ceremony whose only contributor is the operator reduces to "trust the operator", which is the assumption a mixer is supposed to remove.
The beacon. The final step folds in a value nobody could have known ahead of time: the hash of a future Bitcoin block. Rigging a contribution would have meant predicting it, which closes the door on a last participant quietly cancelling out everyone before them.
Why publishing matters. Every contribution file has a SHA-256 fingerprint, and each contributor posts theirs somewhere this service cannot reach. Anyone can then confirm the same value appears in the table below. Without that step, three contributions from three people are indistinguishable from one operator going round three times under three names — the files are identical either way. An unpublished contribution is the operator's word, and the operator's word is the thing being replaced.
Read downwards. One row per contribution; the hash is the proving key as it stood after that person's randomness went in. Any row can be reproduced with snarkjs zkey verify against the file the API serves.
Every line here is an automated test, and the build fails the moment one of these attacks works. scripts/test-attacks.js, test-spl.js, test-relayer.js.
| attack | stopped by |
|---|---|
| Spend the same note twice | nullifier account already exists |
Resubmit with nullifierHash + r - same field element, different account | canonicality check |
| Relayer swaps in its own recipient | extDataHash mismatch |
| Use a 5 SOL proof against the 10 SOL pool | pool bound into extDataHash |
| Charge a fee larger than the deposit | FeeTooHigh |
| Flip a single byte of the proof | pairing check fails |
| Prove against a made-up root | UnknownRoot |
| Reclaim a spent nullifier to replay it | no close instruction exists |
| Withdraw a mint-B note from the mint-A pool | pool address bound into the proof |
| Feed the relayer junk to burn its funds | simulated before broadcast |
Proving happens locally in WebAssembly. What reaches the server is a proof and a destination — no nullifier, no secret. Open the network tab and look; not one request contains them.
Recipient and fee are bound into the proof. A relayer that edits either one produces something that fails verification. It can decline to broadcast; it cannot redirect.
The indexer rebuilds the tree from chain events and compares its own computed root with the one the contract reports. A mismatch returns an error instead of a leaf set that would silently produce a proof nothing accepts.