Service providers

A service provider is a third party reachable through the mixnet — not Nym infrastructure, but a bridge from it to external systems that need no awareness of Nym. Wallet requests arrive as Sphinx packets carrying pre-computed reply routes (SURBs), so the provider can answer without ever learning who asked.

The fourth participant

The Nym whitepaper describes three kinds of node that make up the Nym infrastructure — validators, gateways, and mix nodes. A service provider is the fourth kind of participant, and it is not infrastructure at all: it is a third party that makes itself reachable through the mixnet, acting as an interface between Nym and external systems that need no modification — or even awareness — of Nym. The whitepaper’s own example is a provider that anonymously relays Bitcoin transactions to the Bitcoin peer-to-peer network; swap in Zcash and lightwalletd and you have exactly the broadcast path this site recommends.

From the provider’s perspective, it is not possible to identify which user is behind any request. It sees Sphinx packets arriving from the mixnet’s last layer — never a client IP, never a client address.

Mixnet (5-hop)SURB reply
Mix L1+50 msMix L2+50 msMix L3+50 msClientEntry GWExit GWService provider

Forward: the wallet's request travels client → entry → three mix layers → exit → service provider as Sphinx packets. The reply rides a SURB — a pre-computed, sender-encrypted return route bundled with the request — back through the mixnet, so the provider never learns the client address.

ClientEntry GWMixExit GWService provider
Mixnet (5-hop) (mixing delays)

Replying without knowing who asked

Requests arrive as ordinary Sphinx packets, but each carries something extra: a bundle of single-use reply blocks (SURBs) the client pre-computed before sending. A SURB is an encrypted Sphinx header describing a return route that ends at the client — the provider attaches its reply payload to one and hands it back to the mixnet. Because the SURB is encrypted by the sender, the provider can read nothing from it: not the route, not the per-hop timing, not the client’s address. Each SURB works exactly once, and reply packets are indistinguishable from forward packets, so requests and replies blend into one anonymity set.

The provider never accepts an inbound connection. It is an ordinary mixnet client that dials out to its own gateway and pulls messages from it — so it can run in a DMZ behind a deny-all firewall, with its Nym address as the only way to reach it.

A service provider in Rust

The receive-and-reply loop is small. The sketch below is illustrative — the shape matches the nym-sdk examples (see surb_reply.rs), which are the source of truth for the real API. An example service provider will soon land there too, under examples/service-providers/.

use nym_sdk::mixnet::{AnonymousSenderTag, MixnetClient, MixnetMessageSender};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // A service provider is an ordinary mixnet client: it dials OUT to its
    // gateway and pulls messages from it — no inbound connections needed,
    // so it can sit in a DMZ behind a deny-all firewall.
    let mut client = MixnetClient::connect_new().await?;
    println!("service provider listening on {}", client.nym_address());

    while let Some(messages) = client.wait_for_messages().await {
        for msg in messages.into_iter().filter(|m| !m.message.is_empty()) {
            // The AnonymousSenderTag is an opaque token derived from the
            // SURBs bundled with the request. It lets us reply WITHOUT
            // ever learning the sender's address.
            let return_to: AnonymousSenderTag = match msg.sender_tag {
                Some(tag) => tag,
                None => continue, // no SURBs attached — cannot reply
            };

            // Execute the request against the local backend, e.g. a
            // lightwalletd SendTransaction gRPC call. The RPC sees only
            // this process — never the wallet that asked.
            let response = handle_grpc_request(&msg.message).await?;

            // The reply rides a pre-computed SURB back through the mixnet.
            client.send_reply(return_to, response).await?;
        }
    }
    Ok(())
}

Addressing: how clients reach you

Clients reach a service provider by its Nym address — the value the sketch above prints at startup. It has three parts, <user-identity-key>.<user-encryption-key>@<gateway-identity-key>: a base58-encoded Ed25519 identity key that routes to the client, the public key that encrypts the final Sphinx layer, and the identity of the gateway that holds the client’s messages.

That last component has an operational consequence: the gateway identity is baked into the address, so the address is only as stable as the gateway behind it. To have a stable address you need a stable gateway — one that is always part of the network. If the gateway disappears, every copy of your address stops working.

Run your own mainnet gateway and register your service provider with it. It is the only way to guarantee a stable address: you control the gateway’s uptime and its continued presence in the network, instead of depending on someone else’s. Note that other users of the Nym network will be able to use this gateway, just like any gateway in the network — a gateway only for your own users is not possible; all gateways are available to all users. Make sure the gateway is not in a DMZ and has a static, stable IP address on the Internet (it is the service provider behind it that belongs in the DMZ, not the gateway).

There is deliberately no discovery system for Nym addresses — clients should not be enumerable. Addresses are expected to be distributed out-of-band: a wallet would ship its service provider’s address in its configuration, not look it up.

Real world performance of mixnet mode on the Nym network

Be aware that by default the sending rate of a service provider using the Poisson process is capped at about 55 packets per second: one real packet every 20 ms on average (50/s) plus one loop cover packet every 200 ms (5/s) — the defaults in client-core. With ~2 KB Sphinx packets that is on the order of 100 KB/s at the very most — sent constantly, whether or not there is anything real to say. All of these numbers are configuration, not physics: a client built through the SDK (as in the sketch above) can override every one of them.

Client capacity

Every reply to every client comes out of the same ~50 real-packets-per-second default budget, so one service provider client can only serve so many wallets. Scale by capacity planning, not by hoping: measure your reply sizes and provision more mixnet clients when the budget runs out.

Cover traffic is expensive

When there is nothing real to send, the client sends cover packets instead — same size, same rate, same Sphinx processing. An idle service provider still pays nearly the full bandwidth and compute bill.

It never stops

The client is constantly sending, so your CPU keeps spinning on Sphinx packet creation and your gateway keeps forwarding the stream — 24/7, for as long as the service provider is up.

You can reduce the burden on your machine by turning off the Poisson pacing and the loop cover stream (disable_main_poisson_packet_distribution, disable_loop_cover_traffic_stream), but this comes at a privacy cost: without them, the shape of your traffic starts to reflect your real activity.

Use-case: the forked lightwalletd

The forked-lightwalletd reference architecture is this pattern made concrete: a lightwalletd fork embeds a Nym service provider, receives wallet requests over the mixnet, executes the gRPC calls locally — a SendTransaction broadcast, say — and replies via SURBs. The RPC endpoint never learns which client made the request, because no client address ever reaches it.

Every reply packet consumes one SURB, so this suits transaction broadcasts and small queries — not bulk block sync, which the recommended architecture carries over dVPN instead.