Nostrmix: Nostr relays through the Nym mixnet, for any client including web clients (local relay gateway + CLI)

Mission: Build with Nym
GitHub: GitHub - apeirs/nostrmix: Nostr relays through the Nym mixnet: a local relay gateway for any client (incl. web clients) + CLI · GitHub (MIT)
Nyx address for rewards: n10c7h7wae558d5zejysktztheca6t3gc3vrnep9

What it does

nostrmix puts Nostr relay traffic on the Nym mixnet. It is one Go binary with two parts:

nostrmix nym setup        # starts nym-socks5-client on a random active exit gateway
nostrmix serve            # local relay gateway: ws://127.0.0.1:7777/<relay> -> mixnet -> wss://<relay>
nostrmix req / publish / info / probe   # Nostr over the mixnet from the command line
nostrmix ip               # shows the IP relays see (a Nym exit gateway)

In any Nostr client you replace wss://nos.lol with ws://127.0.0.1:7777/nos.lol, and that relay connection goes through the mixnet. The relay sees a Nym exit gateway instead of your IP. It needs no client changes and no proxy setting, so it also works for clients that have none: most desktop clients and every web client (Coracle, Snort, noStrudel, Iris, Primal web).

The problem it solves

A Nostr client holds WebSocket connections to 5 to 20 relays all day, from your IP. Every relay operator (and anyone who can watch or subpoena a relay) learns which IP publishes as which pubkey, which authors and threads that IP follows, and when it is online. The same IP on many relays links your activity across all of them. Signatures and encryption don’t help, because this is connection metadata. Tor helps only in clients that support a SOCKS proxy, mostly on mobile. nostrmix gives every client a mixnet path, and Nym’s mixing and cover traffic also target the timing correlation that long-lived, chatty WebSockets are exposed to.

How it uses Nym (real mixnet traffic, not a mock)

  • Drives the official nym-socks5-client. nostrmix nym setup asks the Nym API for active exit gateways whose network requester uses the exit policy, picks one at random, and turns on reply SURBs.
  • The gateway opens the relay’s WebSocket through the mixnet first, then accepts the client’s connection, and copies messages both ways unchanged. TLS runs end to end to the relay, so the exit only relays ciphertext.
  • Relay names are resolved with DNS-over-HTTPS over the mixnet, then connected by IPv4. The local resolver never learns which relays you use.
  • No clearnet fallback: with the mixnet client down, the gateway answers 502 and the CLI refuses to connect.
  • Client Origin, cookies and User-Agent are not forwarded. A DNS-rebinding guard, loopback-only listening and optional origin and relay allowlists protect the local port.
  • 30-second keepalive pings stop idle subscriptions over the mixnet from being dropped. permessage-deflate is negotiated with the relay, so fewer Sphinx packets are needed.
  • publish checks the event id and BIP-340 signature before sending. nostrmix never handles private keys.

Proof it works (live mainnet mixnet, 2026-10-16; full logs in the repo’s examples/)

The relay side sees the exit gateway, not me:

$ nostrmix ip
https://api.ipify.org sees this request coming from 79.141.169.35 (6.15s over the mixnet)
= Nym node 2002 (HK, exit gateway: true) in the nym-api node list

Which popular relays work over Nym exits (connect, then a REQ with signature checks):

$ nostrmix probe
RELAY               OK     CONNECT  EOSE  EVENTS
nos.lol             true   10.5s    2.0s  3
relay.primal.net    true   8.5s     1.2s  3
nostr.wine          true   9.4s     3.1s  3
relay.snort.social  true   9.7s     1.8s  3
offchain.pub        true   8.0s     1.1s  3
nostr.mom           true   7.9s     1.2s  3
relay.damus.io      false  ...   (also failing on clearnet at the time: HTTP 503)
relay.nostr.band    false  ...   (also timing out on clearnet)

An unmodified client through the gateway. nostr-tools SimplePool knows nothing about Nym:

querySync via ws://127.0.0.1:7777/nos.lol ws://127.0.0.1:7777/relay.primal.net ws://127.0.0.1:7777/nostr.mom ws://127.0.0.1:7777/offchain.pub
 -> 19 events, 19 verified by nostr-tools, 7.2s
 publish: 4 of 4 relays OK, 1.2s   (ephemeral kind-20001 event, throwaway key)

A live subscription through the gateway (plain python-websockets client) stayed open for 245 s and streamed 72 new notes in real time. CLI publish of a signed ephemeral event was accepted by 5 of 5 relays in 8 to 15 s each. A tampered copy was refused locally.

Current progress

  • v0.1.0 works: gateway, req (multi-relay, deduplicated, signature-checked JSON lines), publish (multi-relay with per-relay OK), info (NIP-11, also served through the gateway with CORS), probe, ip, and nym setup/start/stop/status/exits.
  • 16 offline tests on a fake mixnet (recording SOCKS5 proxy, DoH resolver, TLS relay). They check that every byte goes through the proxy and nothing resolves by name locally, that there is no fallback when the proxy is down, header stripping, the rebinding and allowlist rules, close-code propagation, and id and signature checks against 12 real events from public relays.
  • The README states the limits plainly. It hides your IP, not your pubkey. Media, NIP-05 and LNURL requests made by your client still go direct, which is the biggest gap. All relays share one exit, and some relays block Nym exits. Browsers are not tested yet (only Node and Python WebSocket clients).
  • Prior art is credited: notrustverify/nostr-nym (2023) runs a relay as a Nym service provider and needs a mixnet-aware client. nostrmix uses exit gateways, so it reaches any public relay with unmodified clients.

Future plans

  • A mixnet HTTP proxy for media, NIP-05 and LNURL, to close the biggest gap above.
  • Exit isolation: one Nym client identity and exit per relay, so relays can’t link connections by exit IP.
  • A NIP-65 relay-list rewriter (to gateway URLs), plus testing and notes for the main web clients in Chrome and Firefox.
  • Release binaries and a systemd user unit.