Every SDK request (except a handful of public endpoints) requires an OAuth2 bearer token:
Authorization: Bearer <40-char token>
- Log in to cryptohopper.com.
- Developer → Create App — gives you a
client_id+client_secret. - Complete the OAuth consent flow for your app, which returns a bearer token.
Options to automate step 3:
- The official CLI:
cryptohopper loginopens the consent page, runs a loopback listener, and persists the token to~/.cryptohopper/config.json. Read it from your Rust binary. - Your own code: call the server's
/oauth2/authorize+/oauth2/tokenendpoints directly. The CLI's implementation is short (~300 lines of TypeScript) and a reasonable reference.
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.cryptohopper.com/v1")
.timeout(Duration::from_secs(30))
.max_retries(3)
.user_agent("my-app/1.0")
.build()?;Client::new(api_key) is a shortcut for the most common case (defaults for everything else). Use the builder when you need to set anything beyond the token.
Cryptohopper lets OAuth apps identify themselves on every request via the x-api-app-key header (value = your OAuth client_id). When set, the SDK adds the header automatically. Reasons to set it:
- Shows up in Cryptohopper's server-side telemetry — you can attribute your own traffic.
- Drives per-app rate limits — if two apps share a token, they get independent quotas.
- Harmless to omit; the server accepts unattributed requests.
Empty strings are treated as "not set," so passing unwrap_or_default() from a missing env var is safe.
Override for staging or a local dev server. The default is https://api.cryptohopper.com/v1. The trailing /v1 is part of the base; resource paths are relative to it.
Bring your own reqwest::Client for proxies, custom CA bundles, connection-pool tuning, or tower-based middleware:
let custom = reqwest::Client::builder()
.proxy(reqwest::Proxy::http("http://corporate-proxy.internal:3128")?)
.danger_accept_invalid_certs(false) // keep TLS verification ON; supply a custom root if needed
.timeout(Duration::from_secs(30))
.build()?;
let client = Client::builder()
.api_key(token)
.http_client(custom)
.build()?;When you supply your own reqwest::Client, the .timeout(...) builder option on the Cryptohopper client is overridden — your reqwest::Client controls the per-request connect/read/write timeout. The body-read timeout the SDK applies on top of resp.text() still uses the value passed via .timeout(...) on the builder; pair the two so the body-read timeout isn't tighter than your reqwest timeout.
For rustls vs OpenSSL: the SDK's default-built reqwest client uses rustls + webpki-roots (mozilla CA bundle). To use your OS cert store, supply your own client built with rustls-native-certs:
let custom = reqwest::Client::builder()
// ... build with rustls-native-certs feature enabled ...
.build()?;.timeout(Duration) — per-request total timeout. Applied to the connect + headers phase by reqwest, AND wrapped around the body read by the SDK (reqwest::Client::timeout only covers up to response headers; the body read needs a separate tokio::time::timeout to honour the same deadline). Defaults to 30 seconds.
.max_retries(u32) — automatic retries on HTTP 429. Default 3. Set to 0 to disable. See Rate Limits for details.
If your Cryptohopper app has IP allowlisting enabled, requests from unlisted IPs return 403 FORBIDDEN. The SDK surfaces this as cryptohopper::Error with code == ErrorCode::Forbidden and ip_address populated:
use cryptohopper::ErrorCode;
if let Err(err) = client.hoppers.list().await {
if matches!(err.code, ErrorCode::Forbidden) {
eprintln!("blocked: caller IP was {:?}", err.ip_address);
}
}For CI where the runner IP isn't stable, either disable IP allowlisting for that app or route outbound traffic through a stable IP (NAT gateway, VPN, dedicated proxy).
Cryptohopper bearer tokens are long-lived but can be revoked:
- Manually from the dashboard.
- When the user revokes consent.
The SDK surfaces revocation as Unauthorized on the next call. There is no automatic refresh-token handling in the SDK today — if your app uses refresh tokens, handle the Unauthorized branch by exchanging your refresh token for a new access token and constructing a fresh client. Use a tokio::sync::RwLock (or arc_swap::ArcSwap for higher concurrency) to swap the client atomically:
use std::sync::Arc;
use tokio::sync::RwLock;
use cryptohopper::{Client, ErrorCode};
#[derive(Clone)]
pub struct AutoRefresh {
inner: Arc<RwLock<Client>>,
}
impl AutoRefresh {
pub async fn call<T, F, Fut>(&self, f: F) -> Result<T, cryptohopper::Error>
where
F: Fn(Client) -> Fut,
Fut: std::future::Future<Output = Result<T, cryptohopper::Error>>,
{
let snapshot = self.inner.read().await.clone();
match f(snapshot).await {
Ok(v) => Ok(v),
Err(e) if matches!(e.code, ErrorCode::Unauthorized) => {
let new_token = refresh_token().await?;
let new_client = Client::new(new_token)?;
{
let mut w = self.inner.write().await;
*w = new_client.clone();
}
f(new_client).await
}
Err(e) => Err(e),
}
}
}Client is cheaply cloneable, so swapping is safe. In-flight requests on the old token complete with Unauthorized and trigger their own refresh on retry — there's no shared state to invalidate.
Client is Send + Sync + Clone. One client serving many tokio tasks is fine. The underlying reqwest::Client has its own connection pool which is also thread-safe.
use futures::stream::{FuturesUnordered, StreamExt};
let mut futures = FuturesUnordered::new();
for id in hopper_ids {
let c = client.clone();
futures.push(async move { c.hoppers.get(&id).await });
}
while let Some(res) = futures.next().await {
handle(res);
}See Rate Limits for guidance on capping concurrency.
A handful of endpoints accept anonymous calls:
/market/*— marketplace browse/platform/*— i18n, country list, blog feed/exchange/ticker,/exchange/candle,/exchange/orderbook,/exchange/markets,/exchange/exchanges,/exchange/forex-rates— public market data
The SDK still requires a non-empty api_key at construction; pass any placeholder if you only intend to hit public endpoints. The server ignores the bearer header on whitelisted routes.
let client = Client::new("anonymous")?;
let btc = client.exchange.ticker("binance", "BTC/USDT").await?;