SolanaMixer.fun
starting…
SolanaMixer.fun
Official domains
connecting…
  1. 1Pick an amount
  2. 2Create your note
  3. 3Save it
  4. 4Send the funds

1 - Pick an amount

One pool, one amount. If people sent whatever they liked, the amounts alone would sort everyone back out again. Go for the pool with the most deposits you can afford — that count is the whole of your privacy, and no amount of cryptography adds to it. Your note is made right here, before any SOL moves, and it is the only thing that will ever get the deposit back out.

2 - Create your note

Two random numbers, generated on this page and hashed into one commitment. The chain only ever sees the hash. The numbers themselves go nowhere — not to us, not to the relayer, not into any log. Save them now, because nobody can make you a second copy.

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

Your SOL does not land in the pool immediately. It goes to a holding wallet this service controls, out of the contract's reach, and waits there for a yes or a no.

The order matters because a deposit is one-way. The contract keeps no record of who sent what, and it has no refund instruction — nor should it, because any code able to hand your deposit back is code able to hand anyone's deposit to anyone. The holding step is the last point where a decision is still possible at all.

A yes splits the amount into whole denominations and deposits them — 1.5 SOL becomes one 1 SOL note and four 0.1 SOL notes, with any remainder too small for the smallest pool left where it is. Pools take exact multiples, nothing else. A no sends the money back to its sender, or to another address you nominate if it came from an exchange withdrawal that cannot accept a return.

The holding step belongs to how SolanaMixer.fun is operated, not to the program. The contract has no idea it happened. Everything below is the pool itself.

1

Deposit

Your browser picks two random field elements — a nullifier and a secret — and hashes them: commitment = Poseidon(nullifier, secret). The contract receives a fixed amount of SOL and that hash, writes the hash into a Merkle tree twenty levels deep, and adds the SOL to a shared vault.

Those two numbers are the note. Nothing sends them anywhere.

2

Wait

Deposits are interchangeable by construction: one amount, and a hash that says nothing. Each one that arrives after yours is another figure in the crowd. The crowd is where the privacy comes from — the maths only certifies that the crowd is real.

3

Prove

Withdrawing means building a Groth16 proof of a single sentence: I know the nullifier and secret behind some leaf of this tree. Secrets and Merkle path stay private. Three values are published — the root, a nullifierHash, and an extDataHash.

Some is doing all the work. The proof shows a valid leaf exists and that you hold its secrets. It never says which leaf, so there is no line to follow back.

4

Withdraw

The contract runs the pairing check on chain — roughly 110,000 compute units through Solana's BN254 syscall — checks your root is among the last hundred it has recorded, and opens an account keyed to the nullifierHash.

That account is what stops a second spend. Try the same note twice and the account already exists, so the second attempt dies. The hash comes from the nullifier alone, never the commitment, so it says a note was spent without saying which one.

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.

Those two statements share nothing — no address, no amount trail, no timestamp that has to line up. What is left is a count. One of 400 is strong. One of 2 is a signature.

Two crowds, not one

"How anonymous am I" has two answers, and running them together flatters us at your expense:

against someone watching the chain

One of every deposit in the pool. Deposits the operator placed count fully here — from outside they look exactly like yours.

against whoever seeded the pool

One of the deposits whose notes they do not hold. They know their own withdrawals on sight and can reach yours by elimination.

Both numbers are on the pool picker. On a young pool, assume the smaller one is the one that matters.

The part that stops a relayer robbing you

A proof that only claimed "I know a leaf" could be caught in transit and aimed at somebody else's wallet. So the recipient, the relayer and the fee are hashed into extDataHash and sealed into the proof. Change one of them and it stops verifying. That is why a stranger can broadcast your withdrawal without being able to touch it.

Why a relayer exists at all

A new wallet has no SOL and cannot pay its own fee, and topping it up from your old one would rebuild the link you came here to break. A relayer broadcasts instead and takes its fee from the withdrawal. You sign nothing. The destination ends up with one transaction and no visible funder.

What it costs

Measured, not guessed — scripts/measure-costs.js reads the lamport deltas from a real deposit and 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 that is the nullifier record. Nobody chose to charge it: the entry marking a note spent has to outlast the note forever, or the note could be spent again. It is rent to the chain, not income for anyone.

The cost does not scale with the amount, so the denomination decides how it feels: the same ~0.001 SOL on a 0.1 SOL mix is 1%, and on a 10 SOL mix it is 0.01%.

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

The contract keeps no leaves at all — only the root and the rightmost node per level. An indexer replays deposit events to rebuild the tree so your browser can construct a path.

If this front-end disappears

The note is not stored here and never was. What this site provides is the leaf set, and that is public chain data — any indexer for this program serves it, and the Leaf source control on the withdraw tab lets you choose a different one.

Whatever comes back is measured against the on-chain root in your browser before a proof is built, so a bad leaf set is rejected instead of used. You are not trusting an indexer, you are checking it.

The limit, stated plainly: no fallback indexer is configured, so there is one today. The mechanism works; running a second elsewhere is a deployment choice.

How it works, for dummies

No equations. Just the picture.

First, a desk before the cloakroom

At this cloakroom there is a desk in front of the rail. Your coat goes to the desk first and waits while somebody decides whether to take it.

Taken, it goes on the rail and the ticket is yours. Refused, you get the coat back — and if you came by a route that cannot receive it, you can name somewhere else to send it.

The desk is there for one reason: a coat on the rail comes off for a ticket and for nothing else. Nobody behind the counter knows whose coat is whose — that is the design working — so there is no handing one back afterwards.

Imagine a coat check

You give up a coat, you get a numbered ticket, and later the ticket gets you a coat. Your face never enters into it.

Now make every coat identical

Now make every coat the same coat. You collect a coat rather than your coat, which costs you nothing at all — and costs anyone watching the door everything, because there is no longer a way to match a coat to a person.

Hence fixed amounts. A pool where one person puts in 7.3 SOL and the rest put in 1 SOL has exactly one person in it worth watching.

Now lose the ticket, and prove it anyway

Now the odd part. The ticket never changes hands, because a ticket could be traced to the moment it was issued. What changes hands is a claim:

“I am holding a valid ticket for this coat check.”

You show the claim is true without producing the ticket and without narrowing down which one. The attendant is convinced, hands over a coat, and ends the transaction knowing nothing about which one was yours. That is what a zero-knowledge proof is — no trick, just arithmetic.

Stopping people claiming twice

So what stops you claiming ten coats? On collection you surrender one unforgeable scrap — a serial number derived from your ticket — and it goes in a ledger. The scrap points nowhere near the coat you handed in, but a second appearance is refused on sight.

The ledger is permanent, and paying for your one line of it to exist forever is where nearly all of the fee goes.

Why someone else collects for you

Collection carries a small handling charge. Pay it yourself and the attendant sees your wallet again, which undoes the point of the exercise.

So a courier collects for you and takes the charge out of the coat. The instructions name that courier and that exact charge, so there is no taking more and no delivering elsewhere.

The one thing that actually protects you

If you are the only customer, everyone knows whose coat it is and no proof on earth helps. If a thousand people are, you are one of a thousand.

The crowd is the privacy. The maths only makes the crowd real. Walk in and straight out again and the clock gives you away no matter how many people are inside.

Try it - put some coats in

Add a few names, as if labelling coats. Shuffle them, and look at what an outsider is left with.

Going in Coming out clean
Smart contract

What it costs

Call it 0.001 SOL — about ten cents — in and out. Almost all of it buys the permanent ledger line that stops a second claim. It has to last forever, so it is paid for once, forever.

The charge is the same on a small amount as a large one, so it stings proportionally on the small ones. Use the biggest pool you are comfortable with.

Why this particular coat check is not very private yet

Most of the coats on this rail were hung there by the operator. They know their own — so a coat they do not recognise walking out tells them exactly whose it was.

To a stranger at the door, one of many. To the operator, possibly one of one. The pool picker prints both, because printing only the good number would be a lie.

Who made the lock, and why strangers had to help

The cloakroom needs a lock, made before the first coat arrives. One person making it could keep a copy of the master-key shape, so instead it passes between several unrelated strangers, each scoring it with their own secret pattern before handing it on. Cutting a master key later would take every one of those patterns.

Which is what puts the lock past everybody, its own makers included. No contributor can open it alone. Not a stranger, not a contributor, not whoever runs SolanaMixer.fun.

The last mark uses something nobody could have set up beforehand: the fingerprint of a Bitcoin block that had not been mined. You cannot rig what you cannot predict — not even if you go last.

And you can audit all of it. Each contributor publishes a fingerprint of the lock as they left it, somewhere this service cannot reach in and edit. Match it against the chain on the Proof & trust tab.

What if the coat check burns down?

Your ticket was never on the premises. Two numbers, only ever on your own machine — which is why losing them ends it, and why nobody else can lose them for you.

The building supplies the list of coats on the rail, and that list is public. Anyone can hold a copy, and the withdraw tab lets you point at somebody else's. Your browser checks any list against the rail before believing it, so a forgery is caught rather than used.

Proof & trust

A mixer asks you to swallow a stack of claims you cannot inspect. This page sorts the checkable ones from the rest.

This deployment

Check the bytes yourself: node scripts/verify-deployment.js downloads the on-chain program, diffs it against a local build, and confirms the ceremony key inside it.

The trusted setup, and what the contributors are for

This is the part of a mixer that reading the source will never settle, so it gets the space it deserves. It also has to be right before any money moves; there is no repairing it afterwards.

The problem. The proof system needs public parameters — a proving key — and making them takes secret randomness. That randomness is called toxic waste, because once the key exists it must be destroyed. Whoever kept a copy can mint a valid proof for a deposit that never existed. The pool pays out, the proof checks, and the chain shows nothing unusual. No audit of the contract finds it.

The fix. Several people take turns instead of one. Each takes the previous file, mixes in fresh randomness, passes it on. Forging later would need everyone's secret. So:

One honest participant is enough. If a single contributor destroyed their randomness, the setup stands, whatever anyone else did.

So the count matters, and so does their independence. One contributor who is also the operator collapses the whole thing back to "trust the operator" — the assumption a mixer exists to delete.

The beacon. The final step mixes in something nobody could have known in advance: the hash of a Bitcoin block from the future. Rigging a contribution would have meant predicting it. It closes the door on the last participant cancelling out everyone before them.

Why publishing matters. Each file has a SHA-256 fingerprint, and each contributor posts theirs somewhere this service cannot edit. Anyone can then confirm it appears below. Skip that step and three contributions by three people are indistinguishable from one operator going three times under three names — the files look identical either way. An unpublished contribution is the operator's word, which is the thing being replaced.

This pool's ceremony, step by step

Top to bottom. One row per contribution; the hash is the proving key after that person's randomness went in. Reproduce any of it with snarkjs zkey verify against the file the API serves.

Attacks that were tried, and what stopped them

Each line is an automated test, and the build fails if the attack ever lands. 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 runs locally in WebAssembly. The server gets a proof and a destination, and nothing else — no nullifier, no secret. Open the network tab and check; not one request carries them.

The relayer cannot steal or overcharge enforced on chain

Recipient and fee are sealed into the proof. A relayer that edits either produces something that will not verify. It can refuse to send; it cannot reroute.

Leaf data is checked, not assumed enforced

The indexer rebuilds the tree from chain events and compares its own root with the one the contract reports. Disagreement returns an error rather than a leaf set that would quietly produce a useless proof.