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..2435a79 --- /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`. 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 # demonstrates the typed error surface +npx tsx start-backtest.ts # 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). 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/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/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)"}`); 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}`);