Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const me = await ch.user.get();
console.log(me.email);
```

For runnable scripts you can clone and tinker with, see [`examples/`](examples/) — covers public ticker, whoami, hopper listing, error handling, and a 30-day backtest.

## Resources

```ts
Expand Down
2 changes: 2 additions & 0 deletions examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
package-lock.json
38 changes: 38 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Examples

Runnable scripts that exercise the most common usage patterns of `@cryptohopper/sdk`. Each file is self-contained and reads its bearer token from `CRYPTOHOPPER_TOKEN`. The API gateway requires authentication on every endpoint, including market-data routes, so all examples need a real token.

## Setup

From this directory:

```bash
npm install # installs the SDK + tsx
export CRYPTOHOPPER_TOKEN=... # 40-char OAuth bearer
```

(On Windows PowerShell: `$env:CRYPTOHOPPER_TOKEN = "..."`.)

## Run an example

```bash
npx tsx ticker.ts # current BTC/USDT spot price on Binance
npx tsx whoami.ts # prints the authenticated user
npx tsx list-hoppers.ts # list every hopper on your account
npx tsx error-handling.ts <bad-hopper-id> # demonstrates the typed error surface
npx tsx start-backtest.ts <hopper-id> # kick off a 30-day backtest
```

## What each example shows

| File | Demonstrates |
|------|--------------|
| [`ticker.ts`](ticker.ts) | Single ticker fetch (current spot price) |
| [`whoami.ts`](whoami.ts) | Minimal authenticated request |
| [`list-hoppers.ts`](list-hoppers.ts) | Listing resource with optional filter, paginated table output |
| [`error-handling.ts`](error-handling.ts) | `CryptohopperError` codes, status, retry-after parsing |
| [`start-backtest.ts`](start-backtest.ts) | Long-running async resource — submit, poll, render result |

## Package layout

`package.json` here pins `@cryptohopper/sdk` to the workspace version and depends only on `tsx` for running TypeScript directly. Production users do **not** need `tsx` — these examples use it so the snippets stay readable. Compile and run with `tsc + node` in your own projects, or use TypeScript-aware bundlers / runtimes (Bun, Deno, ts-node).
41 changes: 41 additions & 0 deletions examples/error-handling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Demonstrate the typed error surface. Pass a bogus hopper ID to
// trigger a NOT_FOUND, and inspect every field on CryptohopperError.
//
// Run: npx tsx error-handling.ts <hopper-id-that-does-not-exist>
//
// The SDK auto-retries on 429 with backoff; if you want to see that path,
// hammer this in a loop and watch the retryAfterMs field on the final error.

import { CryptohopperClient, CryptohopperError } from "@cryptohopper/sdk";

const token = process.env.CRYPTOHOPPER_TOKEN;
if (!token) {
console.error("Set CRYPTOHOPPER_TOKEN to a 40-char OAuth bearer first.");
process.exit(1);
}

const hopperId = process.argv[2] ?? "999999999"; // unlikely to exist

const client = new CryptohopperClient({ apiKey: token });

try {
const hopper = await client.hoppers.get(hopperId);
console.log("Unexpectedly succeeded:", hopper);
} catch (e) {
if (e instanceof CryptohopperError) {
console.log("Caught CryptohopperError:");
console.log(` code : ${e.code}`);
console.log(` status : ${e.status}`);
console.log(` message : ${e.message}`);
console.log(` serverCode : ${e.serverCode ?? "(none)"}`);
console.log(` ipAddress : ${e.ipAddress ?? "(none)"}`);
console.log(` retryAfterMs : ${e.retryAfterMs ?? "(only set on 429)"}`);

// Codes are stable across every SDK — compare with `===`, never substring.
if (e.code === "NOT_FOUND") {
console.log("\nHandled NOT_FOUND specifically.");
}
} else {
throw e;
}
}
29 changes: 29 additions & 0 deletions examples/list-hoppers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// List every hopper on the account, optionally filtered by exchange.
//
// Run: npx tsx list-hoppers.ts (all hoppers)
// npx tsx list-hoppers.ts binance (only on Binance)

import { CryptohopperClient } from "@cryptohopper/sdk";

const token = process.env.CRYPTOHOPPER_TOKEN;
if (!token) {
console.error("Set CRYPTOHOPPER_TOKEN to a 40-char OAuth bearer first.");
process.exit(1);
}

const exchange = process.argv[2];

const client = new CryptohopperClient({ apiKey: token });

const hoppers = await client.hoppers.list(exchange ? { exchange } : undefined);

if (hoppers.length === 0) {
console.log(exchange ? `No hoppers on ${exchange}.` : "No hoppers on this account.");
process.exit(0);
}

console.log(`Found ${hoppers.length} hopper(s):`);
for (const h of hoppers) {
const enabled = h.enabled === 1 || h.enabled === true ? "on " : "off";
console.log(` [${enabled}] #${h.id} ${h.exchange ?? "?"} ${h.name ?? "(unnamed)"}`);
}
15 changes: 15 additions & 0 deletions examples/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "cryptohopper-sdk-examples",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Runnable examples for @cryptohopper/sdk",
"dependencies": {
"@cryptohopper/sdk": "*"
},
"devDependencies": {
"@types/node": "^25",
"tsx": "^4.19.0",
"typescript": "^6"
}
}
60 changes: 60 additions & 0 deletions examples/start-backtest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Submit a 30-day backtest, then poll the status until it finishes.
// Backtests are async on the server side — `create` returns immediately
// with a backtest ID; you poll `get` to see when it completes.
//
// Run: npx tsx start-backtest.ts <hopper-id>
//
// Backtests are subject to the `backtest` rate bucket (separate from the
// normal request bucket). The SDK retries automatically on 429; if you
// see RATE_LIMITED here you've hit your daily quota — check
// `client.backtest.limits()` to see how many you have left.

import { CryptohopperClient, type Backtest } from "@cryptohopper/sdk";

const token = process.env.CRYPTOHOPPER_TOKEN;
if (!token) {
console.error("Set CRYPTOHOPPER_TOKEN to a 40-char OAuth bearer first.");
process.exit(1);
}

const hopperId = process.argv[2];
if (!hopperId) {
console.error("Usage: npx tsx start-backtest.ts <hopper-id>");
process.exit(1);
}

const client = new CryptohopperClient({ apiKey: token });

const limits = await client.backtest.limits();
console.log(`Quota remaining: ${limits.remaining ?? "?"} of ${limits.limit ?? "?"}`);

const today = new Date();
const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
const fmt = (d: Date) => d.toISOString().slice(0, 10);

console.log(`Submitting backtest for hopper ${hopperId} from ${fmt(thirtyDaysAgo)} to ${fmt(today)}...`);

const submitted = await client.backtest.create({
hopper_id: hopperId,
start_date: fmt(thirtyDaysAgo),
end_date: fmt(today),
});

console.log(`Submitted backtest #${submitted.id}, polling for completion...`);

let backtest: Backtest = submitted;
const start = Date.now();
const TIMEOUT_MS = 5 * 60 * 1000;

while (backtest.status !== "completed" && backtest.status !== "failed") {
if (Date.now() - start > TIMEOUT_MS) {
console.error(`Timed out after ${TIMEOUT_MS / 1000}s — last status: ${backtest.status}`);
process.exit(1);
}
await new Promise((r) => setTimeout(r, 5000));
backtest = await client.backtest.get(submitted.id);
process.stdout.write(` status=${backtest.status}\r`);
}

console.log(`\nFinished with status: ${backtest.status}`);
console.log(JSON.stringify(backtest, null, 2));
28 changes: 28 additions & 0 deletions examples/ticker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Fetch the current BTC/USDT spot price on Binance.
//
// Even though the *data* returned is "public" (not tied to your account),
// the API gateway requires a real OAuth bearer on every call — there are
// no anonymous routes today. Set CRYPTOHOPPER_TOKEN before running.
//
// Run: npx tsx ticker.ts

import { CryptohopperClient } from "@cryptohopper/sdk";

const token = process.env.CRYPTOHOPPER_TOKEN;
if (!token) {
console.error("Set CRYPTOHOPPER_TOKEN to a 40-char OAuth bearer first.");
process.exit(1);
}

const client = new CryptohopperClient({ apiKey: token });

const ticker = await client.exchange.ticker({
exchange: "binance",
market: "BTC/USDT",
});

console.log(`BTC/USDT on Binance:`);
console.log(` last : ${ticker.last}`);
console.log(` bid : ${ticker.bid}`);
console.log(` ask : ${ticker.ask}`);
console.log(` vol : ${ticker.volume ?? "(unavailable)"}`);
15 changes: 15 additions & 0 deletions examples/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": false,
"resolveJsonModule": true,
"types": ["node"]
},
"include": ["*.ts"]
}
18 changes: 18 additions & 0 deletions examples/whoami.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Minimal authenticated request. Reads CRYPTOHOPPER_TOKEN from the env.
//
// Run: npx tsx whoami.ts

import { CryptohopperClient } from "@cryptohopper/sdk";

const token = process.env.CRYPTOHOPPER_TOKEN;
if (!token) {
console.error("Set CRYPTOHOPPER_TOKEN to a 40-char OAuth bearer first.");
process.exit(1);
}

const client = new CryptohopperClient({ apiKey: token });

const me = await client.user.get();
console.log(`User : ${me.username ?? me.email ?? me.id}`);
console.log(`Email : ${me.email ?? "(hidden)"}`);
console.log(`User ID : ${me.id}`);