cargo add cryptohopperOr in your Cargo.toml:
[dependencies]
cryptohopper = "0.1.0-alpha.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }Requires Rust 1.74 or newer (for async fn in traits, used internally) and a tokio runtime.
use cryptohopper::Client;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let token = std::env::var("CRYPTOHOPPER_TOKEN")?;
let client = Client::new(token)?;
let me = client.user.get().await?;
println!("Logged in as: {}", me["email"]);
let ticker = client
.exchange
.ticker("binance", "BTC/USDT")
.await?;
println!("BTC/USDT: {}", ticker["last"]);
Ok(())
}Client is cheaply cloneable (Arc internally), so you can pass it to spawned tasks without wrapping it in another Arc.
Every request (except a handful of public endpoints like /exchange/ticker) needs an OAuth2 bearer token. Create one via Developer → Create App on cryptohopper.com and complete the consent flow. The token is a 40-character opaque string.
For local dev:
export CRYPTOHOPPER_TOKEN=<your-token>For production, load from your secret store at startup. dotenv works for dev, but in CI use the runner's secret-injection mechanism.
The SDK returns a typed cryptohopper::Error whose code field is a cryptohopper::ErrorCode enum. Match on it directly — no string-comparison gymnastics:
use cryptohopper::{Client, ErrorCode};
match client.hoppers.get("999999").await {
Ok(hopper) => println!("got: {hopper:?}"),
Err(err) => match err.code {
ErrorCode::NotFound => {
// Expected; ignore.
}
ErrorCode::Unauthorized => {
refresh_token().await?;
// retry
}
ErrorCode::RateLimited => {
// SDK already retried; back off harder
if let Some(ms) = err.retry_after_ms {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
}
}
ErrorCode::Forbidden => {
eprintln!("blocked from IP: {:?}", err.ip_address);
}
ErrorCode::Other(ref code) => {
// Server returned a code the SDK doesn't know about yet —
// pass through cleanly.
tracing::warn!("unknown cryptohopper code: {code}");
}
_ => return Err(err.into()),
},
}The Other(String) variant catches any new server-side codes the SDK predates. You don't need to bump SDK versions to handle new error types.
The SDK error implements std::error::Error, so ? works naturally with anyhow::Result or any custom error type built with thiserror:
use anyhow::Result;
async fn list_open_positions(client: &cryptohopper::Client, hopper_id: &str) -> Result<Vec<serde_json::Value>> {
let raw = client.hoppers.positions(hopper_id).await?;
Ok(raw.as_array().cloned().unwrap_or_default())
}For library code, define your own error type:
#[derive(thiserror::Error, Debug)]
pub enum MyError {
#[error("cryptohopper: {0}")]
Cryptohopper(#[from] cryptohopper::Error),
#[error("missing field: {0}")]
MissingField(&'static str),
}#[from] auto-converts the SDK error.
use std::time::Duration;
use cryptohopper::Client;
let client = Client::builder()
.api_key(std::env::var("CRYPTOHOPPER_TOKEN")?)
.app_key(std::env::var("CRYPTOHOPPER_APP_KEY").unwrap_or_default())
.base_url("https://api.staging.cryptohopper.com/v1")
.timeout(Duration::from_secs(60))
.max_retries(5)
.user_agent("my-app/1.0")
.build()?;The builder is the full-control entry point. Client::new(api_key) is a shortcut for Client::builder().api_key(...).build() when defaults are fine.
Client is Send + Sync + Clone. Spawn it across tasks:
use futures::stream::{FuturesUnordered, StreamExt};
let mut tasks = FuturesUnordered::new();
for id in hopper_ids {
let client = client.clone();
tasks.push(tokio::spawn(async move {
client.hoppers.get(&id).await
}));
}
while let Some(res) = tasks.next().await {
match res? {
Ok(hopper) => process(hopper),
Err(err) => tracing::warn!("hopper fetch failed: {err}"),
}
}See Rate Limits for guidance on capping concurrency at the API quota.
Error: Cryptohopper(Error { code: ValidationError, ... }) with message: "api_key must not be empty" — you passed an empty string. Most often: std::env::var("CRYPTOHOPPER_TOKEN").unwrap_or_default() returns "" when unset. Use ? to fail loudly:
let token = std::env::var("CRYPTOHOPPER_TOKEN")
.map_err(|_| anyhow::anyhow!("CRYPTOHOPPER_TOKEN is not set"))?;code: Unauthorized on every call — token is wrong, expired, or revoked. Visit the app page in the Cryptohopper dashboard to confirm.
code: Forbidden on endpoints that used to work — IP allowlisting on the OAuth app blocked your current IP. The error includes ip_address:
if let Err(err) = client.hoppers.list().await {
if matches!(err.code, ErrorCode::Forbidden) {
eprintln!("blocked from {:?}", err.ip_address);
}
}error[E0277]: ? operator can only be applied to values that implement Try — usually means you're inside a function that returns something other than Result. Either change the function signature to Result<T, E> or pattern-match on the SDK call's return.
Hangs in tests — your tests don't have a tokio runtime. Use #[tokio::test] instead of #[test]:
#[tokio::test]
async fn it_lists_hoppers() {
let client = Client::new("test-token").unwrap();
// ...
}Response shapes are returned as serde_json::Value because the Cryptohopper API hasn't been frozen into stable models yet. To layer typed parsing on top, use serde::Deserialize:
use serde::Deserialize;
#[derive(Deserialize)]
struct Hopper {
id: u64,
name: String,
exchange: String,
enabled: bool,
}
let raw = client.hoppers.get(&id).await?;
let hopper: Hopper = serde_json::from_value(raw)?;Future SDK versions may ship typed response structs as a feature flag — file an issue if you'd benefit.
- Authentication — bearer flow, app keys, IP whitelisting, custom HTTP clients
- Error Handling — every variant, pattern-match recipes, retry wrappers
- Rate Limits — auto-retry, customising back-off, concurrency caps