From 9763228657f978f160c692bf6d071ba77e75ea60 Mon Sep 17 00:00:00 2001 From: Pim Feltkamp Date: Sat, 25 Apr 2026 23:23:38 +0200 Subject: [PATCH 1/2] Add examples/ folder with runnable usage scripts Five self-contained TypeScript examples covering the most common usage patterns: a public unauthenticated ticker fetch, an authenticated whoami, listing hoppers with optional filter, the typed CryptohopperError surface, and a 30-day backtest with polling. Each script is stand-alone and reads its bearer token from CRYPTOHOPPER_TOKEN. Includes a sub-package.json pinning @cryptohopper/sdk + tsx, a strict tsconfig (typechecked clean against the SDK source), and a short examples/README.md explaining how to run them. Main README now points at the folder from the Quickstart section. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 ++ examples/.gitignore | 2 ++ examples/README.md | 38 ++++++++++++++++++++++++ examples/error-handling.ts | 41 ++++++++++++++++++++++++++ examples/list-hoppers.ts | 29 ++++++++++++++++++ examples/package.json | 15 ++++++++++ examples/public-ticker.ts | 20 +++++++++++++ examples/start-backtest.ts | 60 ++++++++++++++++++++++++++++++++++++++ examples/tsconfig.json | 15 ++++++++++ examples/whoami.ts | 18 ++++++++++++ 10 files changed, 240 insertions(+) create mode 100644 examples/.gitignore create mode 100644 examples/README.md create mode 100644 examples/error-handling.ts create mode 100644 examples/list-hoppers.ts create mode 100644 examples/package.json create mode 100644 examples/public-ticker.ts create mode 100644 examples/start-backtest.ts create mode 100644 examples/tsconfig.json create mode 100644 examples/whoami.ts diff --git a/README.md b/README.md index a47de12..8090553 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..504afef --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..99aa52f --- /dev/null +++ b/examples/README.md @@ -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` (except [`public-ticker.ts`](public-ticker.ts), which calls a public endpoint and needs no auth). + +## 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 public-ticker.ts # no auth required +npx tsx whoami.ts # prints the authenticated user +npx tsx list-hoppers.ts # list every hopper on your account +npx tsx error-handling.ts # demonstrates the typed error surface +npx tsx start-backtest.ts # kick off a 30-day backtest +``` + +## What each example shows + +| File | Demonstrates | +|------|--------------| +| [`public-ticker.ts`](public-ticker.ts) | Public endpoint (no token), single ticker fetch | +| [`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). diff --git a/examples/error-handling.ts b/examples/error-handling.ts new file mode 100644 index 0000000..c3ece9e --- /dev/null +++ b/examples/error-handling.ts @@ -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 +// +// 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; + } +} diff --git a/examples/list-hoppers.ts b/examples/list-hoppers.ts new file mode 100644 index 0000000..49eb385 --- /dev/null +++ b/examples/list-hoppers.ts @@ -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)"}`); +} diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 0000000..f54aa2a --- /dev/null +++ b/examples/package.json @@ -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" + } +} diff --git a/examples/public-ticker.ts b/examples/public-ticker.ts new file mode 100644 index 0000000..883b6b9 --- /dev/null +++ b/examples/public-ticker.ts @@ -0,0 +1,20 @@ +// Fetch a public ticker. Public endpoints don't need a token, but the +// SDK still requires *something* in `apiKey` — pass an empty string and +// any unauthenticated route just works. +// +// Run: npx tsx public-ticker.ts + +import { CryptohopperClient } from "@cryptohopper/sdk"; + +const client = new CryptohopperClient({ apiKey: "" }); + +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)"}`); diff --git a/examples/start-backtest.ts b/examples/start-backtest.ts new file mode 100644 index 0000000..a635186 --- /dev/null +++ b/examples/start-backtest.ts @@ -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 +// +// 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 "); + 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)); diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 0000000..2a5cce5 --- /dev/null +++ b/examples/tsconfig.json @@ -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"] +} diff --git a/examples/whoami.ts b/examples/whoami.ts new file mode 100644 index 0000000..dd421ff --- /dev/null +++ b/examples/whoami.ts @@ -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}`); From 4ea256d1ee25fbd9af96b5601c852159aa278d0f Mon Sep 17 00:00:00 2001 From: Pim Feltkamp Date: Mon, 27 Apr 2026 17:24:07 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Rename=20public-ticker.ts=20=E2=86=92=20tic?= =?UTF-8?q?ker.ts;=20drop=20'no=20auth'=20claim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original example used apiKey: '' on the assumption that public market-data endpoints accept anonymous calls. Two issues: 1. The SDK throws TypeError on empty apiKey at construction. 2. Even with any token, the API gateway requires a real OAuth bearer on every call — there are no anonymous routes. So the example crashed at line 1 (SDK validation) before ever hitting the wire. Renames the file to drop the 'public-' prefix that no longer makes sense, and updates the example to read CRYPTOHOPPER_TOKEN like every other example in this folder. Same auth-claim cleanup that landed across SDK READMEs and wikis from cryptohopper-resources#9. --- examples/README.md | 6 +++--- examples/public-ticker.ts | 20 -------------------- examples/ticker.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 23 deletions(-) delete mode 100644 examples/public-ticker.ts create mode 100644 examples/ticker.ts diff --git a/examples/README.md b/examples/README.md index 99aa52f..2435a79 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # 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` (except [`public-ticker.ts`](public-ticker.ts), which calls a public endpoint and needs no auth). +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 @@ -16,7 +16,7 @@ export CRYPTOHOPPER_TOKEN=... # 40-char OAuth bearer ## Run an example ```bash -npx tsx public-ticker.ts # no auth required +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 # demonstrates the typed error surface @@ -27,7 +27,7 @@ npx tsx start-backtest.ts # kick off a 30-day backtest | File | Demonstrates | |------|--------------| -| [`public-ticker.ts`](public-ticker.ts) | Public endpoint (no token), single ticker fetch | +| [`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 | diff --git a/examples/public-ticker.ts b/examples/public-ticker.ts deleted file mode 100644 index 883b6b9..0000000 --- a/examples/public-ticker.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Fetch a public ticker. Public endpoints don't need a token, but the -// SDK still requires *something* in `apiKey` — pass an empty string and -// any unauthenticated route just works. -// -// Run: npx tsx public-ticker.ts - -import { CryptohopperClient } from "@cryptohopper/sdk"; - -const client = new CryptohopperClient({ apiKey: "" }); - -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)"}`); diff --git a/examples/ticker.ts b/examples/ticker.ts new file mode 100644 index 0000000..0cf6070 --- /dev/null +++ b/examples/ticker.ts @@ -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)"}`);