radion-sdk 0.9.0

Official, async-first, fully-typed Rust SDK for the Radion platform
Documentation

Radion Rust SDK

crates.io docs.rs license

Official, async-first, fully-typed Rust SDK for Radion. One client, one API key.

use futures_util::StreamExt;
use radion_sdk::realtime::{Channel, Payload, Subscription};
use radion_sdk::Radion;

#[tokio::main]
async fn main() -> eyre::Result<()> {
    let radion = Radion::builder().api_key(std::env::var("RADION_API_KEY")?).build()?;
    radion.realtime.connect().await?;

    let mut trades = radion
        .realtime
        .subscribe(Subscription::new("trading", Channel::Trading))
        .await?;

    while let Some(event) = trades.next().await {
        if let Payload::Trading(trade) = event.data {
            println!("{} {:?}", event.id, trade.kind);
        }
    }
    Ok(())
}

Features

  • Unified client — one Radion, one API key; product surfaces attach as fields.
  • Auto-reconnect — exponential backoff with jitter after unexpected drops.
  • Subscription restore — subscriptions are replayed automatically on reconnect.
  • Heartbeats — periodic pings with stale-connection detection.
  • Typed end-to-end — every frame, channel, filter, and payload is statically typed; events stream as a typed Payload enum you can match exhaustively.
  • Async-first — built on tokio; events are a Stream.
  • Webhook helpers — verify delivery signatures and parse bodies into the same typed Payload events (webhooks feature, no async transport).

Requirements

Rust 1.85 or later.

Install

cargo add radion-sdk

Cargo features:

Feature Default Description
realtime The WebSocket product surface.
rustls rustls TLS backend (no system OpenSSL).
native-tls Use the platform native TLS backend instead.
compression Inflate zlib-compressed realtime frames. Opt in per client.
tracing Emit tracing spans/events.
webhooks Webhook helpers: signature verification + typed body parsing. No async transport.

To drop the realtime transport (e.g. for a future REST-only build):

cargo add radion-sdk --no-default-features

Usage

Configuration

Build a client with [Radion::builder]:

Method Description
.api_key(key) Radion API key (required), sent as X-API-Key.
.base_url(url) Override the REST base URL. Defaults to https://api.radion.app.
.ws_url(url) Override the realtime endpoint. Defaults to wss://api.radion.app/ws.
.token(jwt) / .token_provider(p) User JWT for the pk_jwt_ flow (see below).
.auth_in_query(true) Send credentials in the WS URL query string.
.compression(true) Ask for zlib-compressed realtime frames (compression feature).
let radion = radion_sdk::Radion::builder().api_key("sk_...").build()?;
# Ok::<(), radion_sdk::RadionError>(())

Authentication

Two credential schemes, both keyed on the API key (sent as X-API-Key):

  • Secret key (sk_ / rk_): .api_key("sk_...").
  • Public JWT (pk_jwt_): pair the public key with a per-user JWT. Use .token_provider(...) so a fresh JWT is fetched on every (re)connect:
use radion_sdk::realtime::TokenProvider;

let radion = radion_sdk::Radion::builder()
    .api_key("pk_jwt_...")
    .token_provider(TokenProvider::new(|| async { Ok(fetch_user_jwt().await?) }))
    .build()?;
# Ok::<(), radion_sdk::RadionError>(())
# async fn fetch_user_jwt() -> radion_sdk::error::Result<String> { Ok(String::new()) }

Use .auth_in_query(true) to move credentials into the WS URL query string — for a proxy or gateway that strips the X-API-Key / Authorization headers.

Realtime client

Reached as radion.realtime, or constructed standalone with RealtimeClient::new(RealtimeOptions::new(api_key)).

Method Description
connect() Open the connection; resolves once established.
subscribe(sub) Subscribe and return a Stream of that subscription's events.
unsubscribe(id) Drop a subscription by id.
events() Firehose Stream across all subscriptions.
lifecycle() Stream of connection lifecycle events.
close(code, reason) Gracefully close; stops reconnecting.
connected() Whether the socket is currently open.

Subscriptions & filters

Subscription::new(id, channel) takes a client-defined id (echoed back on every event) and a channel. Subscriptions default to the confirmed on-chain feed; call .pending() (or .confirmed(false)) for the speculative pending (mempool) feed: Subscription::new("pending", Channel::Trading).pending(). Pending events arrive on the same bare channel name and are told apart by ChannelEvent.confirmed; their data is a decoded transaction (Payload::Mempool).

Every event also carries two envelope fields: seq, a per-connection monotonic counter (+1 per event across all subscriptions — a jump means frames were dropped), and sent_at_ms, the Unix-millisecond server send time, so server→client latency is your receive time minus sent_at_ms. Pending events keep data.seen_at_ms (block-detection time) for block→client latency.

Attach server-side filters with .with_filters(...). Some channels require a filter — wallets needs wallets, markets needs market_ids or token_ids. Requirements are validated before any frame is sent.

use radion_sdk::realtime::{Channel, ChannelFilters, Subscription};

let sub = Subscription::new("whales", Channel::Wallets).with_filters(ChannelFilters {
    wallets: Some(vec!["0xabc...".into()]),
    ..Default::default()
});

Channels

Channel enumerates every channel. Nine topic channels each carry a typed payload — Trading, Fees, Oracle, Resolution, Lifecycle, Positions, Combos, Transfers, Accounts — plus two cross-cutting filter channels, Wallets and Markets, that re-emit the matching topic payload. CHANNELS is the full array. Each channel's event data is the typed Payload enum — match on it for compile-time exhaustiveness. Unknown channels or event types are preserved as Payload::Other(serde_json::Value).

Filter high-volume order flow by size with min_usd on Trading (there is no separate large-trades channel). On the confirmed feed min_usd is the actual filled USD; on the pending feed it is the intended fill notional (call.notional_usd). Every topic channel also has a pending (mempool) feed, selected with .pending() on the subscription.

CLOB channels

The CLOB (central limit order book) family is a first-class, separate set of subscribable channels. ClobChannel enumerates the six — Book, Prices, LastTrade, Midpoint, TickSize, BestBidAsk (wire names clob.book, clob.prices, clob.last_trade, clob.midpoint, clob.tick_size, clob.best_bid_ask) — and CLOB_CHANNELS is the full array. Unlike topic channels, each CLOB channel requires a token_ids filter and has no pending feed. Each carries one fixed payload (Payload::ClobBook, ClobPrices, ClobLastTrade, ClobMidpoint, ClobTickSize, ClobBestBidAsk) with no event type discriminator.

use radion_sdk::realtime::{ChannelFilters, ClobChannel, Subscription};

let book = Subscription::new("book", ClobChannel::Book).with_filters(ChannelFilters {
    token_ids: Some(vec!["71321...".into()]),
    ..Default::default()
});

Lifecycle events

use futures_util::StreamExt;
use radion_sdk::realtime::LifecycleEvent;
# async fn run(radion: radion_sdk::Radion) {
let mut lifecycle = radion.realtime.lifecycle();
while let Some(event) = lifecycle.next().await {
    match event {
        LifecycleEvent::Open => {}
        LifecycleEvent::Close { code, reason } => {}
        LifecycleEvent::Reconnect { attempt, delay } => {}
        LifecycleEvent::Warning { code, message, .. } => eprintln!("warning {code}: {message}"),
        LifecycleEvent::Error(err) => eprintln!("{err}"),
        _ => {}
    }
}
# }

Reconnect & subscription restore

By default the client reconnects with exponential backoff (500ms initial, ×2, 30s max, 0.2 jitter) and replays every active subscription on reconnect. Tune via RealtimeOptions::reconnect(...) or disable with .disable_reconnect().

Heartbeats

A ping is sent every 15s; if no inbound traffic arrives within 10s the connection is treated as stale, torn down, and reconnected. Tune via RealtimeOptions::heartbeat(...) or disable with .disable_heartbeat().

Compression

Enable the compression feature and call .compression(true) to cut bandwidth on high-volume channels:

cargo add radion-sdk --features compression
let radion = radion_sdk::Radion::builder()
    .api_key("sk_...")
    .compression(true)
    .build()?;
# Ok::<(), radion_sdk::RadionError>(())

This adds compress=zlib to the connect URL. The server then sends event frames as binary zlib (RFC 1950), which the client inflates before parsing. Text frames still work, so a server may mix both on one connection. Pings and pongs are unaffected. A frame that fails to inflate is reported on the lifecycle() stream as LifecycleEvent::Error(RadionError::Decompression(_)) — it is never dropped in silence.

Compression is off by default: it trades CPU for bandwidth, which only pays off on busy subscriptions.

Webhooks

Radion webhooks POST the same event frames as the WebSocket, signed with your endpoint's secret. Enable the webhooks feature for standalone helpers — no client and no async transport, so they fit any HTTP server:

cargo add radion-sdk --features webhooks
use radion_sdk::webhooks::{WebhookDelivery, parse_webhook_event};

fn handle(raw_body: &[u8], signature: &str, timestamp: &str, secret: &str) {
    let delivery = WebhookDelivery {
        payload: raw_body,
        signature,
        timestamp,
    };
    if !delivery.verify(&[secret]) {
        return;
    }
    let raw = std::str::from_utf8(raw_body).unwrap_or_default();
    if let Some(event) = parse_webhook_event(raw) {
        println!("{} seq={} {:?}", event.channel, event.seq, event.data);
    }
}

payload is the raw request body exactly as received, signature the X-Radion-Signature header, and timestamp the X-Radion-Timestamp header. verify runs a constant-time HMAC-SHA256 check and rejects deliveries older than five minutes — tune the replay window with verify_with_tolerance, and pass both secrets during a rotation window. parse_webhook_event returns the same typed ChannelEvent the realtime client streams. Deliveries are retried and unordered, so deduplicate on (id, seq) or on fields of data.

Error handling

RadionError covers connection-lifecycle misuse (Connection), server-reported error frames (Server), transport failures (Transport), and frames that fail to inflate (Decompression). Errors during a live connection are surfaced on the lifecycle() stream as LifecycleEvent::Error.

License

MIT — see LICENSE.