I checked Zeto.cash (BTC -> XMR atomic swaps in the browser, eigenwallet under the hood). Poked at the live site and the JS they ship. Not an audit, didn't run a real swap with money. Just what you can see from the outside.
Stuff that bothered me:
- "Keys never leave the browser" is only half the story. Signing keys stay client-side, fine. But if you use the hosted daemon (which is the default), that machine sees your XMR receive address, BTC refund/change address, which maker you picked, approvals, wallet balance hints, etc. Not the same as running asb/swap locally. If you care about that, force a local daemon and don't let it auto-connect to theirs.
- No CSP, no HSTS, no X-Frame-Options / frame-ancestors, no nosniff. For a page that keeps an encrypted seed + session tokens in localStorage, that's sloppy. One XSS and you're done.
- When you start a swap the client sends the expected receive pool (your addr 99.3% + their fee addr 0.7%). The approval UI then just displays whatever monero_receive_pool the daemon sends back. I didn't see a hard client-side assert that it still matches the hardcoded fee address before you hit accept. If the daemon is evil or pwned, don't blindly trust that screen - check the actual addresses.
- They say the Rust crates are audited / from eigenwallet. Cool. The thing signing PSBTs in your browser is a fat minified blob with no public repo that I could find. Trust model is "trust this website's JS". Filename is content-hashed which is better than nothing, still not source.
- Daemon token on the websocket URL. events?token=... Tokens in query strings end up in proxy logs, CF logs, random extensions. Put it in a header like a normal person.
- Keystore KDF is on the light side. scrypt N=2^15, r=8, p=1. Password gate is length >= 8. If someone yoinks the .json from disk/localStorage, that's not a lot of work by 2026 standards. Bump N or use argon2id, and don't allow "password1".
- Session PoW is ~18 bits of SHA256. Seconds on a laptop. They 429 you without the header, which is something, but 18 bits won't stop anyone who wants to burn sessions. /health also happily tells you how many sessions are live.
- Cloudflare beacon on every page. Browser hits Kraken + CoinGecko for XMRBTC and mempool.space / blockstream for fees and broadcast. Your IP, their edge, your UTXO activity - you do the math. At least make analytics opt-in and let people set their own electrum/explorer.
- Manual "Daemon URL" field. Placeholder is 127.0.0.1:9976 but it will talk to whatever you paste. Classic phishing angle: fake daemon, convince you to unlock wallet and approve a lock. Warn hard if the URL isn't localhost / their known host.
- Number(x) || 0 on approval amounts. Garbage from the daemon becomes zero in the UI. Also expiration_ts uses || so a falsy/0 value becomes "now + 180 seconds". Don't use || for numeric fields, use nullish checks.
- Wallet state uploaded to the daemon. balance, max giveable, next receive address. Needed for their architecture maybe, still a data leak on the hosted path.
- When I looked, a bunch of makers had maxSwapAmount=0 or version < 4.13.0 (client filters those). Clearnet TCP multiaddrs still show up. Prefer onion/wss if you're swapping for privacy reasons. Also discovery can fall back to api.unstoppableswap.net - third party in the loop.
- Suspend path can clear local state even if the daemon didn't ack ("clearing locally anyway"). History is localStorage only - wipe the profile and the in-app history is gone. Download the refund/redeem kits. Seriously.
- Some of the hostnames look like shitposts. You're trusting whoever runs those for lock checks if the UI uses them. Run your own daemon/node if you can.
What I liked
- They do check change address against what you started with before building
- They refuse lock amount above your approved budget
- Refund kit hex gets some local sanity checks before you broadcast
- CORS on the API is locked to
https://zeto.cash (not *)
- Asset filenames are hashed; CF insights script has SRI
Bottom line:
Interesting project, better than another custodial "instant exchange". I would not put serious coin through the hosted path until the frontend is public, CSP/HSTS are on, and the approval screen cryptographically checks the fee outputs. If you use it, local zetod + download kits + verify every address on the lock screen yourself.
Again: read-only look at the shipped JS and public API. No funded swap from me. DYOR.
NB: I have used my AI assistant tool to decorate this post.
