Developers
Tour

Tour of the Rust SDK

A quick walkthrough of the most important things you can do with nym-sdk. Each section shows working code and links to the module that covers it in depth.

⚠️

The Mixnet is not like regular internet networking — there are no persistent connections, no guaranteed message ordering, and no TCP underneath. At its core, the Mixnet is a message-based anonymity network: you send individual payloads that are Sphinx-encrypted, mixed through multiple nodes, and independently reconstructed at the destination.

The raw message API therefore works differently from what most developers expect. The Stream module bridges this gap by providing AsyncRead + AsyncWrite byte streams on top of the Mixnet. If you are coming from socket-based networking, start with streams.

Send a raw message payload

The message API gives you direct access to the Mixnet's native communication model: individually addressed payloads with no connections and no ordering guarantees. This is useful when you want full control, but it's not how most networking code works:

use nym_sdk::mixnet::{self, MixnetMessageSender};
 
#[tokio::main]
async fn main() {
    let mut client = mixnet::MixnetClient::connect_new().await.unwrap();
    let addr = *client.nym_address();
    println!("Connected: {addr}");
 
    // Send a message to ourselves
    client
        .send_plain_message(addr, "hello mixnet!")
        .await
        .unwrap();
 
    // Receive it (filter empty SURB management messages)
    if let Some(msgs) = client.wait_for_messages().await {
        for msg in msgs.iter().filter(|m| !m.message.is_empty()) {
            println!("Got: {}", String::from_utf8_lossy(&msg.message));
        }
    }
 
    // Always disconnect for clean shutdown
    client.disconnect().await;
}

The message is Sphinx-encrypted, mixed across 5 nodes, and reconstructed on arrival. The whole round trip takes a few seconds.

Next: Mixnet module | Tutorial: Send Your First Private Message

Reply anonymously with SURBs

Every received message carries a sender_tag, an opaque token that lets you reply without knowing the sender's Nym address. Replies travel back through pre-built Single Use Reply Blocks (SURBs):

// After receiving a message...
let tag = received_msg.sender_tag.expect("message includes sender tag");
client.send_reply(tag, "anonymous reply!").await.unwrap();

The replying side never learns where the reply is going, enabling anonymous communication without mutual identity disclosure.

Open a bidirectional stream

If you're used to working with TCP sockets, this is where you'll feel at home. The Stream module provides persistent, bidirectional byte channels that implement tokio's AsyncRead + AsyncWrite, so any code that works with sockets works with MixnetStream:

use nym_sdk::mixnet;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
 
#[tokio::main]
async fn main() {
    let mut sender = mixnet::MixnetClient::connect_new().await.unwrap();
    let mut receiver = mixnet::MixnetClient::connect_new().await.unwrap();
    let recv_addr = *receiver.nym_address();
 
    // Receiver creates a listener (activates stream mode)
    let mut listener = receiver.listener().unwrap();
 
    // Sender opens a stream to the receiver
    let mut out = sender.open_stream(recv_addr, None).await.unwrap();
 
    // Receiver accepts it
    let mut inc = listener.accept().await.unwrap();
 
    // Standard tokio I/O — write, flush, read
    out.write_all(b"hello stream").await.unwrap();
    out.flush().await.unwrap();
 
    let mut buf = vec![0u8; 1024];
    let n = inc.read(&mut buf).await.unwrap();
    println!("{}", String::from_utf8_lossy(&buf[..n]));
 
    drop(out);
    drop(inc);
    sender.disconnect().await;
    receiver.disconnect().await;
}

Activating stream mode (by calling listener() or open_stream()) disables message-based methods like send_plain_message() and wait_for_messages(). A single client operates in one mode at a time.

Next: Stream module | Tutorial: Build a Private Echo Server

Use a client pool for bursty traffic

Creating a MixnetClient takes several seconds (gateway handshake, key generation, topology fetch). The Client Pool pre-creates clients in the background so they're ready when you need them:

use nym_sdk::client_pool::ClientPool;
 
#[tokio::main]
async fn main() {
    let pool = ClientPool::new(3); // maintain 3 clients in reserve
    let bg = pool.clone();
    tokio::spawn(async move { bg.start().await });
 
    // Wait for pool to fill, then grab a ready client
    tokio::time::sleep(std::time::Duration::from_secs(15)).await;
 
    if let Some(client) = pool.get_mixnet_client().await {
        println!("Got client: {}", client.nym_address());
        client.disconnect().await;
    }
 
    pool.disconnect_pool().await;
}

Clients are consumed, not returned; the pool creates replacements automatically.

Next: Client Pool module | Tutorial: Handle Bursty Traffic

Persist your identity

By default, connect_new() creates ephemeral keys that are discarded on disconnect. To keep the same Nym address across restarts, use the builder with on-disk storage:

use nym_sdk::mixnet::{MixnetClientBuilder, StoragePaths};
use std::path::PathBuf;
 
let storage = StoragePaths::new_from_dir(
    &PathBuf::from("/tmp/my-nym-client")
).unwrap();
 
let client = MixnetClientBuilder::new_with_default_storage(storage)
    .await
    .unwrap()
    .build()
    .unwrap()
    .connect_to_mixnet()
    .await
    .unwrap();
 
// This address is the same every time you run with the same path
println!("Persistent address: {}", client.nym_address());

Where to go next