High-performance client SDK for BULK
HTTP and WebSocket clients for BULK, written in Rust and Python.
# Rust SDK
cargo add bulk-client
# CLI
cargo install bulk-cli
# Python
pip install bulk-client- WebSocket - Actor + Watch architecture for zero-cost ticker reads and low-latency order placement
- HTTP - Full REST API coverage (market data, account queries, signed trading)
- Batch transactions - Bundle multiple actions (orders, cancels, conditionals) into a single signed transaction
- Conditional orders - Stop, take-profit, OCO/range, trailing stop, trigger baskets, on-fill consequents
- Sub-accounts & multisig - First-class support for sub-account management and multisig smart accounts
- Ed25519 signing - Native signing with wincode binary serialization
BULK client is built for latency-sensitive trading. Here's what makes it fast:
The WebSocket client uses an actor + watch pattern. A single background actor owns the
socket and deserializes the stream into shared state. Consumers read via tokio::watch
channels - zero-copy, no lock contention, readers never block the writer. Getting the
latest ticker is a .borrow(), not an async round-trip.
┌──────────────┐ mpsc::channel ┌───────────────┐
│ BulkWsClient │ ───── Command ────────────────▶ │ Actor │
│ (handle) │ ◀──── watch::Receiver ──────── │ (owns state) │
└──────────────┘ └───────┬───────┘
│ │
│ oneshot for order responses │ tokio::select!
└─────────────────────────────────────────────────▶│◀── ws_read
Transactions are serialized to a compact little-endian binary format, not JSON.
Prices and sizes are fixed-point u64 (1e8 scale) - no floating-point ambiguity,
significantly faster than text serialization. Ed25519 signatures are computed over this
canonical binary representation.
Multiple actions go into a single signed transaction with one signature and one network round-trip. An OCO entry with a limit order + stop-loss + take-profit is 1 transaction, not 3.
The Python client is pure Python - no native compilation, no wheel matrix. Install
with pip install bulk-client on any platform. REST and WebSocket are I/O-bound anyway;
quants can read, fork, and extend the source directly.
use bulk_client::*;
#[tokio::main]
async fn main() -> eyre::Result<()> {
let client = BulkWsClient::connect(WSConfig {
url: "wss://exchange-wss.bulk.trade".into(),
symbols: vec!["BTC-USD".into()],
..Default::default()
}).await?;
// Zero-cost read - no lock, no async round-trip
if let Some(ticker) = client.get_ticker("BTC-USD") {
println!("BTC mark: {}", ticker.mark_price);
}
client.shutdown().await;
Ok(())
}use bulk_client::*;
#[tokio::main]
async fn main() -> eyre::Result<()> {
let client = BulkHttpClient::with_url(
"https://exchange-api.bulk.trade/api/v1",
Some("your_base58_private_key"),
)?;
let resp = client.place_limit_order(
"BTC-USD", Side::Buy, 95_000.0, 0.01,
TimeInForce::GTC, false, None, None,
).await?;
println!("order status: {}", resp.status);
Ok(())
}use std::sync::Arc;
use bulk_client::*;
#[tokio::main]
async fn main() -> eyre::Result<()> {
let client = BulkHttpClient::with_url(
"https://exchange-api.bulk.trade/api/v1",
Some("your_base58_private_key"),
)?;
let actions = vec![
Action::LimitOrder(LimitOrder {
symbol: Arc::from("BTC-USD"),
is_buy: true,
price: 94_000.0,
size: 0.01,
tif: TimeInForce::GTC,
reduce_only: false,
iso: false,
meta: Default::default(),
}),
Action::CancelAll(CancelAll {
symbols: vec!["ETH-USD".into()],
meta: Default::default(),
}),
];
let responses = client.place_tx(actions, None, None).await?;
for r in &responses {
println!("{}: {}", r.status, r.order_id.as_deref().unwrap_or("-"));
}
Ok(())
}use std::sync::Arc;
use bulk_client::*;
#[tokio::main]
async fn main() -> eyre::Result<()> {
let client = BulkHttpClient::with_url(
"https://exchange-api.bulk.trade/api/v1",
Some("your_base58_private_key"),
)?;
// Single transaction: collar stop at 90k, TP at 110k
let actions = vec![
Action::Range(Range {
symbol: Arc::from("BTC-USD"),
is_buy: true,
size: 0.1,
collar_min: 90_000.0,
collar_max: 110_000.0,
limit_min: Some(89_900.0),
limit_max: Some(110_100.0),
meta: Default::default(),
}),
];
let responses = client.place_tx(actions, None, None).await?;
println!("OCO placed: {}", responses[0].status);
Ok(())
}from bulk_client import BulkHttpClient
client = BulkHttpClient(base_url="https://exchange-api.bulk.trade/api/v1")
ticker = client.get_ticker("BTC-USD")
print(f"BTC last: {ticker['lastPrice']}")
book = client.get_orderbook("BTC-USD", nlevels=5)
print(f"Best bid: {book['levels'][0][0]}")from bulk_client import BulkHttpClient
from bulk_api.common import Side, TimeInForce
from bulk_api.messages import LimitOrder
client = BulkHttpClient(
base_url="https://exchange-api.bulk.trade/api/v1",
private_key="your_base58_private_key",
)
resp = client.place_orders([
LimitOrder(
symbol="BTC-USD",
side=Side.BUY,
price=95_000.0,
size=0.01,
time_in_force=TimeInForce.GTC,
)
])
print(f"Order result: {resp}")from bulk_api.messages import CancelAll
resp = client.place_orders([
CancelAll(symbols=["BTC-USD"])
])The bulk CLI wraps the Rust SDK for quick terminal trading:
# Place a limit order
bulk place Buy BTC-USD 0.01@95000 --tif GTC
# Place a market order
bulk place Sell ETH-USD 1.0
# Cancel a specific order
bulk cancel BTC-USD <order-id>
# Cancel all orders on a market
bulk cancel-all --instrument BTC-USD
# Conditional orders
bulk stop BTC-USD 0.1 90000 # stop-loss
bulk tp BTC-USD 0.1 110000 --above # take-profit
bulk range BTC-USD 0.1 90000 110000 --buy # OCO collar
bulk trail BTC-USD 0.1 --buy --trail-bps 200 # trailing stop
# Sub-accounts
bulk create-subaccount mybot --margin-symbol USDC --margin-amount 1000
bulk transfer <from> <to> USDC 500
# Multisig
bulk create-multisig <pk1>,<pk2> --threshold 2 --lock 120Set your key via environment variable or flag:
export BULK_PRIVATE_KEY="your_base58_private_key"
export BULK_API_URL="https://exchange-api.bulk.trade/api/v1"Licensed under the Apache License, Version 2.0.
