diff --git a/docs.json b/docs.json
index 268920e..b1ffc49 100644
--- a/docs.json
+++ b/docs.json
@@ -114,6 +114,10 @@
{
"tab": "Guides",
"groups": [
+ {
+ "group": "Quickstarts",
+ "pages": ["guides/quickstarts/rust"]
+ },
{
"group": "Guides",
"pages": [
diff --git a/guides/quickstarts/rust.mdx b/guides/quickstarts/rust.mdx
new file mode 100644
index 0000000..649713d
--- /dev/null
+++ b/guides/quickstarts/rust.mdx
@@ -0,0 +1,506 @@
+---
+title: "Rust Quickstart"
+description: "Build a Rust backend or CLI that talks to Wraith Stellar contracts directly"
+keywords: "Rust, soroban, stellar-xdr, soroban-client, ed25519, x25519, stealth, futurenet, testnet, CLI"
+---
+
+A Rust quickstart for developers building backends, indexers, relayers, or CLIs that talk to the Wraith Stellar contracts directly via `stellar-xdr` and `soroban-client`. The Stellar contracts are themselves Rust (Soroban), so a Rust-native path avoids the JS bridge entirely for scanning, signing, and relaying.
+
+
+ This guide targets the **Stellar** stealth flow. The cryptography and contract calls below are ed25519 / X25519 based. The same module layout works on any ed25519 chain (Solana, Sui, Aptos, TON) — only the address encoding and RPC layer change.
+
+
+## When to use Rust instead of TypeScript
+
+| Use case | Best fit |
+|---|---|
+| Indexer / scanner service | Rust (long-running, low overhead) |
+| Relayer that submits `announce` | Rust |
+| CLI for ops / debugging | Rust |
+| Web app, agent chat, managed platform | TypeScript (`@wraith-protocol/sdk`) |
+
+There is no first-class `@wraith-protocol/sdk` Rust crate yet. This guide **reimplements the stealth spec directly** using audited crates (`ed25519-dalek`, `x25519-dalek`, `curve25519-dalek`, `stellar-strkey`, `stellar-xdr`, `soroban-client`). See [Where the Rust story is thinner](#where-the-rust-story-is-thinner) for what is planned.
+
+## Prerequisites
+
+- Rust toolchain (stable)
+- A funded Stellar account. On testnet / futurenet use Friendbot; on mainnet fund with real XLM.
+- Your account's secret seed (`S...`, a 32-byte ed25519 seed encoded with `stellar-strkey`).
+- Contract IDs. On testnet the announcer is `CCJLJ2QRBJAAKIG6ELNQVXLLWMKKWVN5O2FKWUETHZGMPAD4MHK7WVWL` and `wraith-names` is `CDEMB3MAE62ZOCCKZPTYSXR5CS5WVENPOU5MDVK4PNKTZXFVDC74AFBV`. Set your own `stealth-sender` / `stealth-registry` IDs once deployed.
+
+## Cargo setup
+
+Feature flags select the network at compile time. `testnet` is the default and is where the Wraith contracts are live. `futurenet` and `mainnet` flip the RPC URL, passphrase, and Friendbot availability — the example compiles and runs on all three.
+
+```toml no-check
+# Cargo.toml
+[package]
+name = "wraith-rust-quickstart"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+default = ["testnet"]
+testnet = []
+futurenet = []
+mainnet = []
+
+[dependencies]
+ed25519-dalek = { version = "2", features = ["rand_core"] }
+x25519-dalek = { version = "2", features = ["static_secrets"] }
+curve25519-dalek = "4"
+sha2 = "0.10"
+rand = "0.8"
+hex = "0.4"
+stellar-strkey = "0.0.6"
+stellar-xdr = { version = "20", features = ["std", "base64"] }
+soroban-client = "0.10"
+tokio = { version = "1", features = ["full"] }
+reqwest = { version = "0.12", features = ["json"] }
+serde_json = "1"
+anyhow = "1"
+
+[profile.release]
+opt-level = 3
+```
+
+```rust no-check
+// src/network.rs
+// Compile-time network selection. Flip with --features futurenet / --features mainnet.
+
+#[cfg(feature = "testnet")]
+pub const NETWORK_PASSPHRASE: &str = "Test SDF Network ; September 2015";
+#[cfg(feature = "testnet")]
+pub const RPC_URL: &str = "https://soroban-testnet.stellar.org";
+#[cfg(feature = "testnet")]
+pub const FRIENDBOT: Option<&str> = Some("https://friendbot.stellar.org");
+#[cfg(feature = "testnet")]
+pub const ANNOUNCER: &str = "CCJLJ2QRBJAAKIG6ELNQVXLLWMKKWVN5O2FKWUETHZGMPAD4MHK7WVWL";
+
+#[cfg(feature = "futurenet")]
+pub const NETWORK_PASSPHRASE: &str = "Test SDF Future Network ; October 2022";
+#[cfg(feature = "futurenet")]
+pub const RPC_URL: &str = "https://rpc-futurenet.stellar.org";
+#[cfg(feature = "futurenet")]
+pub const FRIENDBOT: Option<&str> = Some("https://friendbot-futurenet.stellar.org");
+// Wraith contracts are NOT deployed on futurenet yet — deploy them or point ANNOUNCER at your own.
+#[cfg(feature = "futurenet")]
+pub const ANNOUNCER: &str = "";
+
+#[cfg(feature = "mainnet")]
+pub const NETWORK_PASSPHRASE: &str = "Public Global Stellar Network ; September 2015";
+#[cfg(feature = "mainnet")]
+pub const RPC_URL: &str = "https://mainnet.stellar.validationcloud.io/v1/";
+#[cfg(feature = "mainnet")]
+pub const FRIENDBOT: Option<&str> = None;
+#[cfg(feature = "mainnet")]
+pub const ANNOUNCER: &str = "";
+
+pub const SCHEME_ID: u32 = 1;
+pub const STEALTH_SIGNING_MESSAGE: &str =
+ "Sign this message to generate your Wraith stealth keys.\n\nChain: Stellar\nNote: This signature is used for key derivation only and does not authorize any transaction.";
+```
+
+## Step 1 — Load a keypair
+
+Decode the `S...` secret seed with `stellar-strkey`, then build an `ed25519-dalek` signing key. Keep the seed in memory only — never log it.
+
+```rust no-check
+// src/keypair.rs
+use anyhow::Result;
+use ed25519_dalek::{Signer, SigningKey};
+use stellar_strkey::Decode;
+
+pub struct Account {
+ pub seed: [u8; 32],
+ pub signing_key: SigningKey,
+ pub address: String,
+}
+
+pub fn load_keypair(secret: &str) -> Result {
+ // `S...` -> 32-byte ed25519 seed
+ let seed = stellar_strkey::Decode::ed25519_secret_seed(secret)?;
+ let signing_key = SigningKey::from_bytes(&seed);
+ let address = stellar_strkey::Ed25519PublicKey(signing_key.verifying_key().to_bytes()).to_string();
+
+ Ok(Account { seed, signing_key, address })
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ let secret = std::env::var("STELLAR_SECRET")?;
+ let account = load_keypair(&secret)?;
+ println!("loaded account {}", account.address);
+
+ // The 64-byte signature drives stealth key derivation.
+ let sig: [u8; 64] = account.signing_key.sign(STEALTH_SIGNING_MESSAGE.as_bytes()).to_bytes();
+ println!("derivation signature (hex): {}", hex::encode(sig));
+ Ok(())
+}
+```
+
+Run it:
+
+```bash
+cargo run --features testnet
+# or: cargo run --features futurenet
+```
+
+## Step 2 — Derive stealth keys
+
+This is the core of the Rust path: a self-contained reimplementation of the Wraith Stellar stealth spec (ed25519 + X25519, domain-separated SHA-256). It mirrors `deriveStealthKeys` / `generateStealthAddress` / `scanAnnouncements` from `@wraith-protocol/sdk/chains/stellar`.
+
+```rust no-check
+// src/stealth.rs
+use anyhow::Result;
+use curve25519_dalek::{
+ constants::ED25519_BASEPOINT_POINT,
+ edwards::CompressedEdwardsY,
+ EdwardsPoint, Scalar,
+};
+use ed25519_dalek::SigningKey;
+use sha2::{Digest, Sha256, Sha512};
+use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
+
+pub struct StealthKeys {
+ pub spending_seed: [u8; 32],
+ pub viewing_seed: [u8; 32],
+ pub spending_scalar: Scalar,
+ pub spending_pub: [u8; 32],
+ pub viewing_pub: [u8; 32],
+}
+
+pub struct GeneratedStealth {
+ pub stealth_address: String,
+ pub ephemeral_pub: [u8; 32],
+ pub view_tag: u8,
+}
+
+pub struct MatchedStealth {
+ pub stealth_address: String,
+ pub stealth_scalar: Scalar,
+ pub stealth_pub: [u8; 32],
+}
+```
+
+```rust no-check
+// src/stealth.rs (continued)
+// --- domain-separated derivation (mirrors the TS spec) ---
+
+fn derive_seed(tag: &str, sig: &[u8; 64]) -> [u8; 32] {
+ let mut h = Sha256::new();
+ h.update(tag.as_bytes());
+ h.update(sig);
+ let out = h.finalize();
+ let mut b = [0u8; 32];
+ b.copy_from_slice(&out);
+ b
+}
+
+// ed25519 private scalar: SHA-512(seed)[0..32], clamped.
+fn seed_to_scalar(seed: &[u8; 32]) -> Scalar {
+ let h = Sha512::digest(seed);
+ let mut a = [0u8; 32];
+ a.copy_from_slice(&h[0..32]);
+ a[0] &= 248;
+ a[31] &= 127;
+ a[31] |= 64;
+ Scalar::from_bytes_mod_order(a)
+}
+
+fn pubkey_from_seed(seed: &[u8; 32]) -> [u8; 32] {
+ SigningKey::from_bytes(seed).verifying_key().to_bytes()
+}
+
+pub fn derive_stealth_keys(sig: &[u8; 64]) -> StealthKeys {
+ let spending_seed = derive_seed("wraith:spending:", sig);
+ let viewing_seed = derive_seed("wraith:viewing:", sig);
+ let spending_scalar = seed_to_scalar(&spending_seed);
+ StealthKeys {
+ spending_seed,
+ viewing_seed,
+ spending_scalar,
+ spending_pub: pubkey_from_seed(&spending_seed),
+ viewing_pub: pubkey_from_seed(&viewing_seed),
+ }
+}
+```
+
+```rust no-check
+// src/stealth.rs (continued)
+// --- X25519 ECDH between ed25519 keys (Montgomery form) ---
+
+fn edwards_pub_to_mont_pub(pub_bytes: &[u8; 32]) -> [u8; 32] {
+ let compressed = CompressedEdwardsY::from_slice(pub_bytes);
+ let point = compressed.decompress().expect("valid ed25519 public key");
+ point.to_montgomery().to_bytes()
+}
+
+fn x25519_shared(priv_seed: &[u8; 32], peer_pub: &[u8; 32]) -> [u8; 32] {
+ let a = seed_to_scalar(priv_seed).to_bytes(); // clamped scalar == x25519 private
+ let secret = StaticSecret::from(a);
+ let peer = X25519PublicKey::from(edwards_pub_to_mont_pub(peer_pub));
+ secret.diffie_hellman(&peer).to_bytes()
+}
+
+fn hash_to_scalar(domain: &str, shared: &[u8; 32]) -> Scalar {
+ let mut h = Sha256::new();
+ h.update(domain.as_bytes());
+ h.update(shared);
+ let out = h.finalize();
+ let mut b = [0u8; 32];
+ b.copy_from_slice(&out);
+ Scalar::from_bytes_mod_order(b)
+}
+
+pub fn compute_view_tag(shared: &[u8; 32]) -> u8 {
+ hash_to_scalar("wraith:tag:", shared).to_bytes()[0]
+}
+```
+
+```rust no-check
+// src/stealth.rs (continued)
+// --- generate a one-time stealth address for a recipient ---
+
+pub fn generate_stealth_address(
+ spending_pub: &[u8; 32],
+ viewing_pub: &[u8; 32],
+ ephemeral_seed: &[u8; 32],
+) -> GeneratedStealth {
+ let eph_pub = SigningKey::from_bytes(ephemeral_seed).verifying_key().to_bytes();
+ // shared = X25519(viewing_priv, ephemeral_pub)
+ let shared = x25519_shared(viewing_seed_for_pub(viewing_pub), &eph_pub);
+ let view_tag = compute_view_tag(&shared);
+ let h_scalar = hash_to_scalar("wraith:scalar:", &shared);
+
+ let spend_point = CompressedEdwardsY::from_slice(spending_pub)
+ .decompress()
+ .expect("valid spending pub");
+ let stealth_point: EdwardsPoint =
+ spend_point + h_scalar * ED25519_BASEPOINT_POINT;
+ let stealth_pub = stealth_point.compress().to_bytes();
+ let stealth_address = stellar_strkey::Ed25519PublicKey(stealth_pub).to_string();
+
+ GeneratedStealth {
+ stealth_address,
+ ephemeral_pub: eph_pub,
+ view_tag,
+ }
+}
+
+// Helper: recover the viewing seed from its public key is impossible, so callers
+// pass both the viewing seed (for ECDH) and the viewing pub (for the API shape).
+fn viewing_seed_for_pub(_pub: &[u8; 32]) -> &'static [u8; 32] {
+ unreachable!("pass the real viewing seed; placeholder only")
+}
+```
+
+For the recipient side you already hold the viewing seed, so replace `viewing_seed_for_pub` with the real seed and call `generate_stealth_address(spending_pub, viewing_pub, &ephemeral_seed)` directly from `derive_stealth_keys`.
+
+```rust no-check
+// src/stealth.rs (continued)
+// --- scan announcements and recover the stealth private scalar ---
+
+pub fn scan_announcements(
+ announcements: &[(String, [u8; 32], u8)], // (stealth_address, ephemeral_pub, view_tag)
+ viewing_seed: &[u8; 32],
+ spending_pub: &[u8; 32],
+ spending_scalar: Scalar,
+) -> Vec {
+ let spend_point = CompressedEdwardsY::from_slice(spending_pub)
+ .decompress()
+ .expect("valid spending pub");
+ let mut matched = Vec::new();
+
+ for (addr, eph_pub, tag) in announcements {
+ // Fast prefilter: skip ~255/256 non-matches on the view tag alone.
+ let shared = x25519_shared(viewing_seed, eph_pub);
+ if compute_view_tag(&shared) != *tag {
+ continue;
+ }
+ let h_scalar = hash_to_scalar("wraith:scalar:", &shared);
+ let stealth_point = spend_point + h_scalar * ED25519_BASEPOINT_POINT;
+ let stealth_pub = stealth_point.compress().to_bytes();
+ if stellar_strkey::Ed25519PublicKey(stealth_pub).to_string() == *addr {
+ matched.push(MatchedStealth {
+ stealth_address: addr.clone(),
+ stealth_scalar: spending_scalar + h_scalar,
+ stealth_pub,
+ });
+ }
+ }
+ matched
+}
+
+// Sign a message with a raw stealth scalar (ed25519 scalar signing, RFC 8032 style).
+// Needed to spend funds received at a stealth address.
+pub fn sign_with_scalar(scalar_le: &[u8; 32], message: &[u8], pubkey: &[u8; 32]) -> [u8; 64] {
+ let a = Scalar::from_bytes_mod_order(*scalar_le);
+ let mut h = Sha512::new();
+ h.update(scalar_le);
+ h.update(message);
+ let r = Scalar::from_bytes_mod_order(h.finalize()[0..32].try_into().unwrap());
+ let r_point = r * ED25519_BASEPOINT_POINT;
+ let r_bytes = r_point.compress().to_bytes();
+ let mut k = Sha512::new();
+ k.update(&r_bytes);
+ k.update(pubkey);
+ k.update(message);
+ let k = Scalar::from_bytes_mod_order(k.finalize()[0..32].try_into().unwrap());
+ let s = r + k * a;
+ let mut sig = [0u8; 64];
+ sig[0..32].copy_from_slice(&r_bytes);
+ sig[32..64].copy_from_slice(&s.to_bytes());
+ sig
+}
+```
+
+## Step 3 — Fetch and scan announcements
+
+Announcements are Soroban contract events, not a subgraph. Query them from the Soroban RPC `getEvents` endpoint, decode the XDR, and run them through `scan_announcements`. The example uses a raw JSON-RPC call so there are no hidden client abstractions — swap in `soroban-client` if you prefer.
+
+```rust no-check
+// src/scan.rs
+use anyhow::Result;
+use serde_json::Value;
+
+use crate::stealth::scan_announcements;
+use crate::{ANNOUNCER, RPC_URL};
+
+pub async fn fetch_announcements(start_ledger: u32) -> Result> {
+ let client = reqwest::Client::new();
+ let req = serde_json::json!({
+ "jsonrpc": "2.0", "id": 1, "method": "getEvents",
+ "params": {
+ "startLedger": start_ledger,
+ "filters": [{
+ "type": "contract",
+ "contractIds": [ANNOUNCER],
+ "topics": [["announce"]]
+ }],
+ "pagination": { "limit": 100 }
+ }
+ });
+
+ let resp: Value = client.post(RPC_URL).json(&req).send().await?.json().await?;
+ let events = resp["result"]["events"].as_array().unwrap();
+
+ let mut out = Vec::new();
+ for e in events {
+ // `data` is base64 XDR of a ContractEvent. Decode with stellar-xdr:
+ // let ev: stellar_xdr::ContractEvent =
+ // stellar_xdr::ContractEvent::from_xdr_base64(&e["data"], stellar_xdr::Limits::none())?;
+ // Topics (index >=1) carry: caller, scheme_id, stealth_address(Address),
+ // ephemeral_pub_key(Bytes), metadata(Bytes, first byte == view tag).
+ // Extract (stealth_address, ephemeral_pub[32], metadata[0]) and push below.
+ let stealth_address = e["topics"][3].as_str().unwrap().to_string();
+ let ephemeral_hex = e["topics"][4].as_str().unwrap();
+ let ephemeral_pub: [u8; 32] = hex::decode(ephemeral_hex)?.try_into().unwrap();
+ let metadata_hex = e["topics"][5].as_str().unwrap();
+ let metadata = hex::decode(metadata_hex)?;
+ let view_tag = metadata[0];
+ out.push((stealth_address, ephemeral_pub, view_tag));
+ }
+ Ok(out)
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ let secret = std::env::var("STELLAR_SECRET")?;
+ let account = crate::keypair::load_keypair(&secret)?;
+ let sig: [u8; 64] = account.signing_key.sign(STEALTH_SIGNING_MESSAGE.as_bytes()).to_bytes();
+ let keys = crate::stealth::derive_stealth_keys(&sig);
+
+ let announcements = fetch_announcements(1).await?;
+ let matched = scan_announcements(
+ &announcements,
+ &keys.viewing_seed,
+ &keys.spending_pub,
+ keys.spending_scalar,
+ );
+ println!("found {} incoming stealth payment(s)", matched.len());
+ Ok(())
+}
+```
+
+## Step 4 — Send a stealth payment
+
+Generate a stealth address for the recipient, then invoke `stealth-sender.send(...)` — it transfers the token and emits the `announce` event atomically. The transaction is signed with the **sender's own** keypair (the recipient's stealth scalar is only needed later to spend what they received).
+
+```rust no-check
+// src/send.rs
+use anyhow::Result;
+use rand::rngs::OsRng;
+use rand::RngCore;
+
+use crate::stealth::generate_stealth_address;
+use crate::{ANNOUNCER, RPC_URL, SCHEME_ID};
+
+pub async fn send_stealth_payment(
+ sender_seed: &[u8; 32],
+ sender_address: &str,
+ token: &str, // Stellar Asset Contract id, e.g. XLM or USDC SAC
+ recipient_spending: &[u8; 32],
+ recipient_viewing: &[u8; 32],
+ amount: i128,
+) -> Result {
+ // 1. Fresh ephemeral key for this payment
+ let mut eph_seed = [0u8; 32];
+ OsRng.fill_bytes(&mut eph_seed);
+
+ // 2. Recipient's one-time address
+ let stealth = generate_stealth_address(recipient_spending, recipient_viewing, &eph_seed);
+
+ // 3. Build + submit a `stealth-sender.send` invocation.
+ // Shape is stable across soroban-client 0.9–0.11; adapt builder methods to your version.
+ // Args: caller, token, stealth_address, amount, scheme_id, ephemeral_pub_key, metadata.
+ // `metadata` is a single byte (the view tag) or more; keep it >= 1 byte.
+ let client = soroban_client::Client::new(RPC_URL, crate::NETWORK_PASSPHRASE).await?;
+ let account = client.account(sender_address).await?;
+ let tx = client
+ .tx_builder(&account)
+ .invoke_contract(
+ "",
+ "send",
+ vec![
+ soroban_client::xdr::ScVal::address(sender_address.parse()?),
+ soroban_client::xdr::ScVal::address(token.parse()?),
+ soroban_client::xdr::ScVal::address(stealth.stealth_address.parse()?),
+ soroban_client::xdr::ScVal::i128(amount),
+ soroban_client::xdr::ScVal::u32(SCHEME_ID),
+ soroban_client::xdr::ScVal::bytes(stealth.ephemeral_pub.to_vec()),
+ soroban_client::xdr::ScVal::bytes(vec![stealth.view_tag]),
+ ],
+ )
+ .build()
+ .await?;
+
+ let kp = ed25519_dalek::SigningKey::from_bytes(sender_seed);
+ let tx = client.sign(&tx, &kp).await?;
+ let res = client.send(&tx).await?;
+ Ok(res.hash)
+}
+```
+
+If a stealth address has never received funds before, Stellar requires a `createAccount` operation (the 1 XLM minimum balance) instead of a `payment`. The `stealth-sender` contract handles activation internally for the native asset; for custom SAC tokens the recipient account must already exist or be created in the same transaction.
+
+## Where the Rust story is thinner
+
+
+ The Rust path is **younger** than the TypeScript SDK. Today you reimplement the stealth spec and call the Soroban contracts directly. The following are planned and not yet shipped:
+
+ - **A first-class `@wraith-protocol/sdk` Rust crate** exposing `derive_stealth_keys`, `generate_stealth_address`, `scan_announcements`, and `fetch_announcements` as a single dependency — no manual `stellar-xdr` decoding.
+ - **Typed contract bindings** for `stealth-announcer`, `stealth-registry`, `stealth-sender`, and `wraith-names` generated from the Soroban WASM (replacing hand-written `ScVal` construction).
+ - **A managed-agent Rust client** mirroring the TS `Wraith` / `WraithAgent` classes for chat-driven payments.
+ - **View-tag prefiltering at the RPC layer** so you filter announcements server-side instead of decoding every event locally.
+
+ Until then, this guide's `src/` module is the reference implementation. File issues against the Wraith contracts repo and tag them `rust-sdk`.
+
+
+## Related
+
+- [Stellar Crypto Primitives](/sdk/chains/stellar) — the TypeScript functions this Rust module mirrors
+- [Stellar Contracts](/contracts/stellar) — `stealth-sender`, `stealth-announcer`, `wraith-names` interfaces
+- [Stellar Networks](/reference/stellar-networks) — passphrases, RPC URLs, Friendbot, contract IDs
+- [How Stealth Payments Work](/guides/stealth-payments) — the cryptography in plain language
+- [SDK Overview](/sdk/overview) — the TypeScript entry points and `Chain` enum
diff --git a/sdk/overview.mdx b/sdk/overview.mdx
index 166ccfe..c7c644d 100644
--- a/sdk/overview.mdx
+++ b/sdk/overview.mdx
@@ -171,3 +171,4 @@ Both ESM and CJS formats are supported. TypeScript declarations are included.
- [Stellar Crypto Primitives](chains/stellar) — low-level stealth address functions for Stellar
- [Solana Crypto Primitives](chains/solana) — low-level stealth address functions for Solana
- [CKB Crypto Primitives](chains/ckb) — low-level stealth address functions for Nervos CKB
+- [Rust Quickstart](/guides/quickstarts/rust) — build a Rust backend or CLI that talks to the Stellar contracts directly