The MadeOnSol Rust SDK is live on crates.io. Full REST surface parity with the TypeScript SDK — KOL feed, leaderboard, coordination, pairs, hot, trending, entry-order, timing, PnL, deployer alerts + leaderboards, wallet tracker, alpha wallet intelligence — packaged as an async client you can drop into a tokio runtime.
If you've been running your Solana trading bot in Node and wondering whether Rust is worth the rewrite, this post walks through the specifics: when Rust makes the difference, what ships in the crate, and how it plugs into the trading-loop architecture.
When Rust actually matters for Solana bots
For most people building around the MadeOnSol API — KOL alert bots, dashboards, trade journaling — TypeScript or Python is fine. You're bound by network latency, not by local compute. A Node-based bot sniping pump.fun will not be materially slower to signal than a Rust one.
Rust becomes the right choice in a few specific scenarios:
- Co-located execution. You're running signing and transaction submission in the same process as signal handling and want a single async Rust runtime. This can simplify a latency-sensitive architecture, but network/RPC latency still dominates many paths.
- Memory-constrained deployments. A compiled Rust service can have a smaller runtime footprint than a comparable Node/Python deployment, but measure your own binary and workload rather than relying on a generic MB claim.
- Predictable local runtime behavior. Rust avoids garbage collection and gives tight control over allocation and concurrency. That can reduce one source of jitter; it does not guarantee that a remote trade lands within a fixed latency.
- Cross-compilation. Shipping the bot to an ARM VPS, a RISC-V edge node, or a Docker image without node_modules is a
cargo build --release --target=... away.
Outside those cases, stick with the TypeScript SDK. It's faster to iterate and the runtime difference isn't where your bot will win or lose.
What's in the crate
cargo add madeonsol
Design choices worth noting:
tokio async runtime. All methods return Futures — no blocking calls, no separate sync/async client split.
reqwest HTTP client with rustls. No OpenSSL dependency. Cross-compiles to Alpine containers, ARM boards, and static binaries without system-library pain.
- Typed response structs. Every endpoint returns a concrete type that mirrors the live REST payload. Serde deserialization, so field mismatches fail at compile time (for required fields) or at parse time (for optionals).
- Structured errors. Server-side error bodies are surfaced as typed variants, not just HTTP status codes. If a call fails because your tier is insufficient, you get that specific reason.
- Same
msk_ Bearer auth as every other SDK. One key across TS, Python, Rust, MCP, ElizaOS.
The covered surface: KOL endpoints (feed, leaderboard, coordination, pairs, hot, trending, entry-order, timing, PnL, compare, wallet profile), deployer-hunter (alerts, leaderboards, profiles, trajectory, best-tokens, recent-bonds), wallet tracker (watchlist CRUD, trades, summary), alpha wallet intelligence (leaderboard, wallet profile, linked wallets), token intel, and x402 discovery. Copy-trade CRUD ships via the REST surface on parity.
Install and auth
Add to Cargo.toml:
[dependencies]
madeonsol = "0.28" # published version verified 2026-09-26
tokio = { version = "1", features = ["full"] }
anyhow = "1"
Bootstrap the client:
use madeonsol::Client;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Client::new(std::env::var("MADEONSOL_API_KEY")?);
// ... use the client
Ok(())
}
Grab an msk_ key from the developer portal — free tier gives you 200/day to prototype without a credit card.
Workflow sketches
The shape of the client is simple: one type per endpoint group, methods returning typed responses. Specifics (method names, option structs) are on docs.rs/madeonsol — I won't invent names here. What's worth showing is how Rust changes the architecture of your bot loop.
Pattern 1: Streaming decision loop
The hottest path on most Solana trading bots is: get signal → score it → decide. In Rust that loop runs without GC hitches:
use tokio::time::{sleep, Duration};
async fn decide_loop(client: &madeonsol::Client) -> anyhow::Result<()> {
loop {
let feed = client.kol_feed()
.limit(20)
.action("buy")
.min_sol(1.0)
.await?;
for trade in &feed.trades {
if should_act(trade) {
spawn_execution(trade.token_mint.clone()).await;
}
}
sleep(Duration::from_secs(10)).await;
}
}
tokio::spawn lets you fan out per-trade analysis onto the runtime without blocking the poll loop. In Node you'd use Promise.all and hope the GC doesn't chew through the cold loop you just queued.
Pattern 2: WebSocket stream with reconnect
For push-delivery (copy-trade signals, KOL trades, coordination alerts), the idiomatic Rust pattern pairs the SDK's REST calls for token generation with tokio-tungstenite for the WS layer:
use futures_util::{SinkExt, StreamExt};
use tokio_tungstenite::connect_async;
async fn run_stream(client: &madeonsol::Client, channels: Vec<&str>) -> anyhow::Result<()> {
let mut backoff = Duration::from_secs(2);
loop {
match connect_and_listen(client, &channels).await {
Ok(()) => backoff = Duration::from_secs(2),
Err(e) => {
eprintln!("stream err {e:?}; reconnect in {backoff:?}");
tokio::time::sleep(backoff).await;
backoff = (backoff * 3 / 2).min(Duration::from_secs(60));
}
}
}
}
See the stream-token reconnect guide for the full pattern. Stream tokens no longer expire on a 24-hour timer. Fetch the current token before a connection/reconnect; repeated fetches normally return the same token. An in-band token_refresh is associated with an explicit rotation, not a scheduled lifecycle event.
Pattern 3: Trade execution alongside signal detection
This is where Rust actually earns the rewrite. Some teams choose to separate signal ingestion and transaction signing into different processes; others keep both in one Node process. Rust gives you another option: keep the networking, decision logic and signing in one typed async runtime without requiring IPC.
In Rust you don't have that split. Pull a signal from the MadeOnSol SDK, hand it to solana-client for blockhash, hand it to jito-sdk-rust for bundle submission — all inside one tokio::spawn closure, all on the same runtime. Do not treat that as a fixed signal-to-landed latency promise: blockhash/RPC access, leader timing, priority fees, bundle acceptance and network conditions dominate the end-to-end number.
async fn on_signal(signal: CopyTradeSignal) -> anyhow::Result<()> {
let keypair = load_keypair()?;
let tx = build_buy_tx(&keypair, &signal.token_mint, signal.suggested_sol_amount).await?;
let sig = jito_submit(tx).await?;
tracing::info!(signal_id = signal.signal_id, signature = %sig, "landed");
Ok(())
}
Structured tracing via tracing + correlation on the signal_id field makes post-hoc analysis trivial. Every signal has a server-generated id (added to the WS payload as of v1.1) — pair that with your tx signature and you have the full audit trail.