The server behind biscuitwallet.com: one small Go program that serves the website and its onion service, the public price data the app reads, the relay for exchange swaps through Trocador, and Biscuit's own exchanges, which work over Tor.
It is published so that anyone can read what our server does with the requests of Biscuit Wallet. What it records, why and for how long is also explained in the privacy policy. A server can't be inspected from the outside: this is the code we run, built as described below.
Biscuit calls the relay; the relay adds our Trocador partner key and passes the request on. The key never ships inside the app.
- Allow list: only
coins,new_rate,new_trade,tradeandvalidateaddressgo through, with GET. Everything else gets a 404. - Nothing about the user goes to Trocador: Trocador receives the key and a fixed
user agent (
biscuit-relay). No IP address, cookies or headers from the app. - Swap log: when a
new_tradesucceeds, and only then, one line is written: time, IP address, user agent, languages and swap ID. Trocador and its exchanges require it, to answer requests from authorities.- Each line is encrypted with a public key (age). The server cannot read it back: only the private key, kept offline, can.
- One file per day (
YYYY-MM-DD.log), deleted automatically after 12 months (checked every hour). - If the line can't be written, the app doesn't receive the deposit address: no swap without its record.
- No swaps through Tor: creating a swap is refused (403,
tor_exit) from Tor exits, according to two lists from the Tor Project (the bulk exit list and Onionoo, IPv6 included), reloaded every 30 minutes. While either list is missing or more than a day old, swap creation is refused (503). Rates and swap status still work. - VPNs are fine: only Tor exits are refused. Many people can share one VPN server's
address, hence generous limits: 120 requests a minute and 20 swap creations per
10 minutes per address (the app itself stays under 6 requests a minute), and a daily
cap of swap creations per address (
-trades-per-day, 100 by default). IPv6 addresses are counted per /64. Counters live in memory only and are forgotten at the end of each window. - No other log: Go's connection errors (which contain the IP address) are dropped; the program only writes its own errors, without addresses or requests.
- Static site on the main domain (
-site-domain,-site-dir),www.redirected to it, GET and HEAD only, no directory listings, no hidden files, no access log. The relay only answers onrelay.(/api/,/health). No reverse proxy: the client's address reaches the program directly, and goes nowhere else. - Public data under
/data/: the server itself fetches prices (CoinGecko), currency rates (the European Central Bank, through Frankfurter) and Monero's crowdfunding proposals, and serves the same copy to everyone, so those sources never see users. A bad answer from a source keeps the previous copy. /data/tor-checkanswersyes,noorunknown: whether the request came through Tor. Never the address, never logged.- Onion service (
-onion-listen,-onion-address): the website and/data/only, on a local address that Tor forwards to. The relay is not mounted there, since every request through the onion comes from 127.0.0.1. Over HTTPS, the site sends theOnion-Locationheader to Tor Browser. - HTTPS built in: Let's Encrypt certificates, obtained and renewed by the program.
GET /healthanswersok, for an external uptime check.
Trocador refuses swaps created through Tor, so Biscuit also calls a few exchanges itself: exchanges that never ask for KYC and accept Tor: PegasusSwap, ETZ-Swap and Stereoswap. The app uses them in Tor mode, through the onion service, and next to Trocador otherwise.
- Routes, GET only:
assets,quotes,new_trade,trade. The relay asks every exchange for a quote, the app ranks them with Trocador's, by what the user receives. - Keys stay here: each exchange's partner key is read from a file at start
(
-pegasus-public-key-file,-pegasus-secret-file;-etzwith optional-etz-public-key-file,-etz-secret-file;-stereoswap-key-filewith-stereoswap-referral-file, the referral code its swaps are created with). ETZ-Swap works without keys, they only bring the affiliate share. With no exchange configured,/api/x/is off. - ETZ-Swap swaps are private to their creator: created with ETZ's
secureTokentype. The token is part of the swap ID the app keeps (etz-<id>.<token>); the relay stores nothing. - Coins: those Biscuit holds, and the popular coins it does not hold (USDT on Tron, SOL, DOGE…) that the app swaps to or from a wallet outside Biscuit, each from the exchanges that map it.
- Tor welcome: no Tor exit check, and the routes also exist on the onion service.
- No log at all: no IP address, no swap record. The exchanges receive the swap itself (coins, amount, addresses) and a fixed user agent, nothing about the user.
- Status needs the payout address:
tradeanswers only when the request gives the address the coins go to, so a short swap ID is not enough to read someone's addresses. - Same limits per address as the Trocador relay; on the onion, where every request comes from 127.0.0.1, one larger shared budget.
- Amounts are cut to each coin's decimals: rounded up for what the user sends, down for what they get.
go test ./...
go build -o biscuit-relay .
./biscuit-relay keygen -out /tmp/test-identity.txt # prints the public key age1...
./biscuit-relay serve -dev 127.0.0.1:8080 -key-file <file with a Trocador key> \
-recipient age1... -log-dir /tmp/relay-trades
curl 'http://127.0.0.1:8080/api/coins'For the server: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o biscuit-relay-linux .
-
Keys, on your own computer:
./biscuit-relay keygen -out trades-identity.txt. This file is the private key of the swap log: it never goes on the server. Keep it offline. Without it the log can't be read, not even to answer a legal request. Theage1...line it prints is the public key, for the service. -
Server: a small VPS in the EU or EEA (1 vCPU and 1 GB of memory are enough). Turn off the host's backups and snapshots: they would copy the swap log outside its 12-month rotation. Automatic security updates, SSH by key only, and a firewall that only lets in SSH, 80 (certificates) and 443.
-
Program and Trocador key: copy the binary to
/usr/local/bin/biscuit-relayanddeploy/biscuit-relay.serviceto/etc/systemd/system/. Type the key without leaving it in the shell history:install -d -m 700 /etc/biscuit-relay read -rs K && printf '%s\n' "$K" > /etc/biscuit-relay/trocador-key && unset K chmod 600 /etc/biscuit-relay/trocador-key
The same way, PegasusSwap's public key and secret, for
/api/x/(optional):read -rs K && printf '%s\n' "$K" > /etc/biscuit-relay/pegasus-public-key && unset K read -rs K && printf '%s\n' "$K" > /etc/biscuit-relay/pegasus-secret && unset K chmod 600 /etc/biscuit-relay/pegasus-*
Without them, drop the two
-pegasus-*lines and theirLoadCredentiallines from the service file. -
Service: set the domains and the
age1...public key in the service file, thensystemctl daemon-reload && systemctl enable --now biscuit-relay. It runs as a dynamic user in a locked-down sandbox, and only the service can read the key. -
Onion service (optional), in
/etc/tor/torrc:SocksPort 0 HiddenServiceDir /var/lib/tor/biscuit_site/ HiddenServicePort 80 127.0.0.1:8081
If the relay goes down, only exchange swaps stop: the wallet and atomic swaps carry on.
The onion service also serves /api/x/, for the app in Tor mode.
Trocador forwards requests by email from an @trocador.app address.
- Check where it comes from: an email can be forged. Write back to the known Trocador address yourself (not with Reply), or check the DKIM signature.
- Copy the days concerned from
/var/lib/private/biscuit-relay/trades/to the offline computer that holds the private key. - Decrypt there and find the swap:
./biscuit-relay decrypt -identity trades-identity.txt 2026-09-26.log | grep <swap ID> - Send only the line of the swap asked about, then delete the local copies.
- Same paths and parameters as
https://trocador.app/api/, without theAPI-Keyheader. - Relay errors are JSON:
{"error": code, "message": text}. The codetor_exit(403) is not a refused key: the app shows it without turning swaps off. - Trocador answers
{"error": "Invalid API key"}(HTTP 404) when the key is refused; that answer is passed on as it is. - The user agent and languages the app sends are what gets logged.
/api/x/has its own answers (seeexchanges.go): assets as{ticker, network}with the app's names, amounts as plain decimal strings, statuses with the app's names (waiting,confirming,sending,finished,failed,expired,halted,refunded), and swap IDs of the form<exchange>-<exchange's ID>.
Pull requests are welcome, especially small and focused ones: bug fixes, documentation. We're a small team, so we can't promise when we'll review them, and for anything bigger please write to us first at biscuitwallet@tutamail.com. To report a vulnerability, see SECURITY.md.
BSD 3-Clause, see LICENSE.