-
Notifications
You must be signed in to change notification settings - Fork 0
Recipes
Practical, copyable patterns for @cryptohopper/sdk. Each one is a small, self-contained snippet you can drop into a TypeScript file and run with npx tsx <file>.ts. They use only the public SDK surface — no internals.
For end-to-end runnable scripts, see examples/ in the main repo.
- Wait for a backtest to finish
- Find every open position across all your hoppers
- Follow live ticker without WebSockets
- Detect new fills since the last poll
- Fail fast on auth errors, retry on transient ones
- Read your remaining quota for any rate bucket
- Run two SDK calls concurrently and merge results
- Use a custom fetch (proxies, instrumentation, mocking)
- Tighten timeouts for short-lived serverless invocations
- Disable the SDK's built-in retry and handle 429 yourself
- Debug an auth failure end-to-end
Backtests are async on the server side. create returns immediately with an ID; you poll get to see when it's done.
import { CryptohopperClient, type Backtest } from "@cryptohopper/sdk";
async function runBacktest(
ch: CryptohopperClient,
hopperId: number | string,
fromDate: string,
toDate: string,
): Promise<Backtest> {
const submitted = await ch.backtest.create({
hopper_id: hopperId,
start_date: fromDate,
end_date: toDate,
});
let bt = submitted;
while (bt.status !== "completed" && bt.status !== "failed") {
await new Promise((r) => setTimeout(r, 5000));
bt = await ch.backtest.get(submitted.id);
}
return bt;
}The backtest rate bucket is separate from the normal one (1 request per 2 seconds). Polling at 5-second intervals stays well clear; lower it if you have many concurrent backtests.
const hoppers = await ch.hoppers.list();
const all = await Promise.all(
hoppers.map(async (h) => ({
hopperId: h.id,
hopperName: h.name,
positions: await ch.hoppers.positions(h.id),
})),
);
for (const { hopperId, hopperName, positions } of all) {
for (const p of positions) {
console.log(`${hopperName} (#${hopperId}) holds ${p.amount} ${p.coin} @ ${p.rate}`);
}
}This fans out N requests in parallel. With many hoppers the SDK will start hitting the normal rate limit (30 req/min) — the built-in retry will smooth that out, but if you have 50+ hoppers consider chunking with Promise.all over batches of 10.
The SDK is HTTP-only, so there is no built-in WebSocket. For low-frequency monitoring (every few seconds), polling is fine.
async function pollTicker(
ch: CryptohopperClient,
exchange: string,
market: string,
intervalMs: number,
): Promise<void> {
while (true) {
const t = await ch.exchange.ticker({ exchange, market });
console.log(`${new Date().toISOString()} ${market} last=${t.last} bid=${t.bid} ask=${t.ask}`);
await new Promise((r) => setTimeout(r, intervalMs));
}
}
pollTicker(ch, "binance", "BTC/USDT", 3000);For sub-second updates, use Cryptohopper's WebSocket ticker portal directly — that's outside the SDK's scope.
const seen = new Set<string | number>();
async function pollFills(ch: CryptohopperClient, hopperId: number | string) {
const orders = await ch.hoppers.orders(hopperId);
for (const o of orders) {
if (o.id !== undefined && !seen.has(o.id) && o.status === "filled") {
seen.add(o.id);
console.log(`Fill: ${o.market} ${o.type} ${o.amount} @ ${o.price}`);
}
}
}
setInterval(() => pollFills(ch, 42).catch(console.error), 10000);For production-grade fill notifications, consider configuring the webhooks resource — push beats poll for event delivery.
The SDK auto-retries 429s. For 5xx and network errors, you usually want a tighter retry (or a hard fail) than the default — auth errors should never be retried.
import { CryptohopperClient, CryptohopperError } from "@cryptohopper/sdk";
async function withRetry<T>(fn: () => Promise<T>, maxAttempts = 3): Promise<T> {
let lastErr: unknown;
for (let i = 0; i < maxAttempts; i++) {
try {
return await fn();
} catch (e) {
lastErr = e;
if (e instanceof CryptohopperError) {
if (e.code === "UNAUTHORIZED" || e.code === "FORBIDDEN" || e.code === "NOT_FOUND" || e.code === "VALIDATION_ERROR") {
throw e; // never retry these
}
}
await new Promise((r) => setTimeout(r, 500 * Math.pow(2, i)));
}
}
throw lastErr;
}
const me = await withRetry(() => ch.user.get());Backtests have an explicit quota endpoint:
const limits = await ch.backtest.limits();
console.log(`Backtests remaining: ${limits.remaining} of ${limits.limit}`);For the normal and order buckets, the only quota signal is the Retry-After header on a 429. Watch err.retryAfterMs on a CryptohopperError with code === "RATE_LIMITED".
const [me, hoppers] = await Promise.all([
ch.user.get(),
ch.hoppers.list(),
]);
console.log(`${me.username} has ${hoppers.length} hoppers`);Both requests count against the normal bucket (30 req/min). Concurrency is fine until you start needing more than a handful of in-flight requests.
const ch = new CryptohopperClient({
apiKey: process.env.CRYPTOHOPPER_TOKEN!,
fetch: async (input, init) => {
const start = performance.now();
const res = await fetch(input, init);
console.log(`${init?.method ?? "GET"} ${input} -> ${res.status} (${(performance.now() - start).toFixed(0)}ms)`);
return res;
},
});The fetch option accepts anything that matches globalThis.fetch. Use it for OpenTelemetry spans, HTTP/HTTPS proxies (via undici.ProxyAgent), or MockFetch in tests.
The SDK's default timeoutMs is 30000. AWS Lambda's 15000 and Cloudflare Workers' 30000 mean the default can outlive your invocation, leading to confusing "function killed" errors instead of clean SDK timeouts.
const ch = new CryptohopperClient({
apiKey: process.env.CRYPTOHOPPER_TOKEN!,
timeoutMs: 8000, // ~half your function budget
maxRetries: 1, // leave room for one retry inside the function lifetime
});A TIMEOUT CryptohopperError is much easier to handle than a hard process kill.
const ch = new CryptohopperClient({
apiKey: process.env.CRYPTOHOPPER_TOKEN!,
maxRetries: 0,
});
try {
await ch.hoppers.list();
} catch (e) {
if (e instanceof CryptohopperError && e.code === "RATE_LIMITED") {
console.log(`Rate limited. Server says wait ${e.retryAfterMs}ms.`);
// your custom backoff / queue / circuit breaker
} else {
throw e;
}
}Useful when you have your own queue, want exact backoff control, or are running inside something that already does retries (a job runner, a workflow engine).
If the SDK is returning 401/403/UNAUTHORIZED/FORBIDDEN, this checklist tells you exactly why — most commonly: token typo, expired token, IP not on the OAuth-app allowlist, or wrong SDK version.
import { CryptohopperClient, CryptohopperError, CURRENT_VERSION } from "@cryptohopper/sdk";
async function debugAuth(): Promise<void> {
const token = process.env.CRYPTOHOPPER_TOKEN ?? "";
// 1. SDK version sanity check. Earlier alphas sent the wrong auth header
// (Authorization: Bearer instead of access-token); 0.4.0-alpha.2+ is fixed.
console.log(`SDK version: ${CURRENT_VERSION}`);
if (CURRENT_VERSION === "0.4.0-alpha.1") {
console.warn("⚠ You're on 0.4.0-alpha.1, which sent the wrong auth header. Upgrade to alpha.2+.");
}
// 2. Token shape sanity check — Cryptohopper bearer tokens are 40 chars.
console.log(`Token length: ${token.length} (expected 40)`);
if (token.length === 0) {
console.error("✗ CRYPTOHOPPER_TOKEN is empty. Set it from your Cryptohopper developer dashboard.");
return;
}
const ch = new CryptohopperClient({ apiKey: token, maxRetries: 0 });
// 3. The minimal authenticated probe.
try {
const me = await ch.user.get();
console.log(`✓ Authenticated. User: ${me.username ?? me.email ?? me.id}`);
} catch (e) {
if (!(e instanceof CryptohopperError)) throw e;
console.error(`✗ ${e.code} (HTTP ${e.status}): ${e.message}`);
if (e.ipAddress) {
console.error(` Cryptohopper saw your IP as: ${e.ipAddress}`);
console.error(` → If your OAuth app has IP allowlisting, add this IP to the allowlist.`);
}
if (e.code === "UNAUTHORIZED") {
console.error(` → Token rejected. Either it's expired, revoked, or never existed.`);
console.error(` → Re-issue at https://www.cryptohopper.com developer dashboard.`);
} else if (e.code === "FORBIDDEN") {
console.error(` → Token is valid but lacks the required scope, or your IP is blocked.`);
console.error(` → Check the OAuth app's scopes in the developer dashboard.`);
} else if (e.code === "UNKNOWN" && e.status === 405 && /Missing Authentication Token/.test(e.message)) {
console.error(` → AWS API Gateway rejected the request — likely the SDK is sending the wrong auth header.`);
console.error(` → Confirm SDK version is 0.4.0-alpha.2 or later.`);
}
}
}
await debugAuth();Pipe through npx tsx debug-auth.ts (or compile via tsc). The output gives you the next action in plain English. The full output looks like:
SDK version: 0.4.0-alpha.2
Token length: 40 (expected 40)
✓ Authenticated. User: alice
Or, on failure:
SDK version: 0.4.0-alpha.2
Token length: 40 (expected 40)
✗ FORBIDDEN (HTTP 403): IP address mismatch
Cryptohopper saw your IP as: 203.0.113.42
→ If your OAuth app has IP allowlisting, add this IP to the allowlist.
Save it as examples/debug-auth.ts once you have a setup that needs to be re-checked periodically.
Pages
Other SDKs
Resources