SolMix
starting…
Verify the domain
connecting…
  1. 1Pick an amount
  2. 2Create your note
  3. 3Save it
  4. 4Send the funds

1 - Pick an amount

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.

2 - Create your note

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.

nullifier private
-
secret private
-
Poseidon(nullifier, secret) ↓
commitment goes on chain
-
Poseidon(nullifier) ↓
nullifier hash revealed only when you withdraw
-

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.

3 - Save your note

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 note
copied

4 - Send the funds

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.

  1. 1Your note
  2. 2Where to send it
  3. 3Prove it
  4. 4Receive

1 - Paste your note

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.

Leaf source

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.

2 - Where should it go?

Use an address with no history. Sending to a wallet that already holds your funds undoes everything you just paid for.

3 - Prove you own a deposit

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.

merkle root public
-
nullifier hash public
-
proof A
-
proof B
-
proof C
-
your leaf index never sent
-
When should it land?

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.

4 - Done

Pools

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.

How it works

0

Before anything reaches the contract

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.

1

Deposit

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.

2

Wait

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.

3

Prove

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.

4

Withdraw

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.

Why the link actually breaks

the deposit says

Wallet A put 1 SOL in.

the withdrawal says

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.

Two crowds, not one

"How anonymous am I" has two answers. Quoting only the friendlier one is how these services flatter themselves:

against someone watching the chain

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.

against whoever seeded the pool

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.

The part that stops a relayer robbing you

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.

Why a relayer exists at all

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.

What it costs

Measured rather than estimated — scripts/measure-costs.js reads the lamport deltas off a real deposit and a real withdrawal:

steplamportsSOL
deposit transaction fee4,9920.000005
nullifier record (permanent)1,002,2400.001002
withdrawal transaction fee5,0000.000005
relayer margin at its floor15,0000.000015
total per note1,027,2320.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.

What is and is not on chain

on chain
  • the Merkle root and the last 100 roots
  • one account per spent nullifier
  • the vault holding pooled funds
never on chain
  • your nullifier and secret
  • which leaf you withdrew
  • any link between deposit and withdrawal

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.

If this front-end disappears

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.

How it works, for dummies

Skip the maths. Here is the shape of it.

First, a desk before the cloakroom

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.

Imagine a coat check

Coat in, numbered ticket out. Later the ticket buys a coat back. Your name never appears.

Now make every coat identical

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.

Now lose the ticket, and prove it anyway

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.

Stopping people claiming twice

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.

Why someone else collects for you

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.

The one thing that actually protects you

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.

Try it - put some coats in

Put a few names in, as though labelling coats. Mix them, and see what is left for somebody watching from outside.

Going in Coming out clean
Smart contract

What it costs

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.

Why this particular coat check is not very private yet

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.

Who made the lock, and why strangers had to help

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.

What if the coat check burns down?

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.

Proof & trust

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.

This deployment

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.

The trusted setup, and what the contributors are for

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.

This pool's ceremony, step by step

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.

Attacks that were tried, and what stopped them

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.

attackstopped by
Spend the same note twicenullifier account already exists
Resubmit with nullifierHash + r - same field element, different accountcanonicality check
Relayer swaps in its own recipientextDataHash mismatch
Use a 5 SOL proof against the 10 SOL poolpool bound into extDataHash
Charge a fee larger than the depositFeeTooHigh
Flip a single byte of the proofpairing check fails
Prove against a made-up rootUnknownRoot
Reclaim a spent nullifier to replay itno close instruction exists
Withdraw a mint-B note from the mint-A poolpool address bound into the proof
Feed the relayer junk to burn its fundssimulated before broadcast

What you still have to trust - and should not

The trusted setup unknown
Your note never leaves the browser verifiable

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.

The relayer cannot steal or overcharge enforced on chain

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.

Leaf data is checked, not assumed enforced

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.