diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0593d542..d5ad9697 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,6 +14,7 @@ updates: - dependency-name: binance - dependency-name: '@siebly/kraken-api' - dependency-name: coinbase-api + - dependency-name: okx-api groups: sdk-releases: patterns: @@ -22,6 +23,7 @@ updates: - binance - '@siebly/kraken-api' - coinbase-api + - okx-api cooldown: exclude: - '@siebly/hyperliquid-api' @@ -29,3 +31,4 @@ updates: - binance - '@siebly/kraken-api' - coinbase-api + - okx-api diff --git a/compatibility/adapters/okx.mjs b/compatibility/adapters/okx.mjs new file mode 100644 index 00000000..f0f6b847 --- /dev/null +++ b/compatibility/adapters/okx.mjs @@ -0,0 +1,25 @@ +export const operations = { + rest: 'RestClient.getTicker()', + websocket: 'WebsocketClient.subscribe()', +}; +export function client(sdk) { + return new sdk.RestClient({}, { timeout: 15000 }); +} +export async function read(client) { + const tickers = await client.getTicker({ instId: 'BTC-USDT' }); + return tickers[0]?.last; +} +export function stream(sdk) { + return new sdk.WebsocketClient(); +} +export const event = 'update'; +export function value(message) { + return message.arg?.channel === 'tickers' ? message.data?.[0]?.last : undefined; +} +export async function subscribe(ws) { + await Promise.all(ws.subscribe({ channel: 'tickers', instId: 'BTC-USDT' })); +} +export const typeExample = `import { RestClient, WebsocketClient } from 'okx-api'; +new RestClient().getTicker({ instId: 'BTC-USDT' }); +new WebsocketClient().closeAll(); +`; diff --git a/compatibility/browser-config/okx.mjs b/compatibility/browser-config/okx.mjs new file mode 100644 index 00000000..3ddd40a1 --- /dev/null +++ b/compatibility/browser-config/okx.mjs @@ -0,0 +1,37 @@ +import { fileURLToPath } from 'node:url'; + +const browserAgent = '\0okx-browser-agent'; +const sdkRoot = fileURLToPath(new URL('./node_modules/okx-api/', import.meta.url)); + +export default { + // Keep the same SDK transform during development and production builds. + optimizeDeps: { + exclude: ['okx-api'], + include: ['events', 'okx-api > isomorphic-ws'], + }, + resolve: { + alias: { + events: fileURLToPath(new URL('./node_modules/events/events.js', import.meta.url)), + }, + }, + plugins: [{ + name: 'okx-browser-transport', + enforce: 'pre', + resolveId(source, importer) { + if (source === 'https' && importer?.startsWith(sdkRoot)) { + return browserAgent; + } + }, + load(id) { + if (id === browserAgent) { + // Browsers manage connections themselves. Reject an unsupported option + // rather than presenting a Node Agent as a working browser transport. + return `export class Agent { + constructor() { + throw new Error('keepAlive and HTTPS agents are Node.js-only. Omit keepAlive in a browser.'); + } +}`; + } + }, + }], +}; diff --git a/compatibility/native-config/okx.metro.cjs b/compatibility/native-config/okx.metro.cjs new file mode 100644 index 00000000..7ff8f3e4 --- /dev/null +++ b/compatibility/native-config/okx.metro.cjs @@ -0,0 +1,19 @@ +module.exports = function configureSdkMetro(config, projectRoot) { + const aliases = { + https: require.resolve('https-browserify', { paths: [projectRoot] }), + http: require.resolve('stream-http', { paths: [projectRoot] }), + url: require.resolve('url/', { paths: [projectRoot] }), + }; + const previousResolveRequest = config.resolver.resolveRequest; + + config.resolver.resolveRequest = (context, moduleName, platform) => { + if (aliases[moduleName]) { + return { type: 'sourceFile', filePath: aliases[moduleName] }; + } + + const resolveRequest = previousResolveRequest || context.resolveRequest; + return resolveRequest(context, moduleName, platform); + }; + + return config; +}; diff --git a/compatibility/native-config/recipe.mjs b/compatibility/native-config/recipe.mjs index 4cbd4564..a09f0b90 100644 --- a/compatibility/native-config/recipe.mjs +++ b/compatibility/native-config/recipe.mjs @@ -1,7 +1,7 @@ import { readFile } from 'node:fs/promises'; export function nativeMetroConfig(id) { - const filename = { bybit: 'bybit.metro.cjs', binance: 'metro.cjs', kraken: 'kraken.metro.cjs', coinbase: 'coinbase.metro.cjs' }[id]; + const filename = { bybit: 'bybit.metro.cjs', binance: 'metro.cjs', kraken: 'kraken.metro.cjs', coinbase: 'coinbase.metro.cjs', okx: 'okx.metro.cjs' }[id]; if (!filename) { throw new Error(`No Metro polyfill recipe for ${id}.`); } diff --git a/releases/package.json b/releases/package.json index c777c3e1..8003d578 100644 --- a/releases/package.json +++ b/releases/package.json @@ -6,6 +6,7 @@ "bybit-api": "4.8.5", "binance": "3.6.12", "@siebly/kraken-api": "1.1.10", - "coinbase-api": "1.3.1" + "coinbase-api": "1.3.1", + "okx-api": "3.2.9" } } diff --git a/scripts/check-public.mjs b/scripts/check-public.mjs index a4d766ff..619b5e0b 100644 --- a/scripts/check-public.mjs +++ b/scripts/check-public.mjs @@ -20,7 +20,7 @@ export async function checkPublic(directory) { const stat = await lstat(filename); if (stat.isDirectory()) continue; if (stat.isFile() && path.extname(relative) === '.gz') { - const match = /^(hyperliquid|bybit|binance|kraken|coinbase)\/starters\/(node|typescript|react|bun|deno|vite|next|expo)\.tar\.gz$/.exec(parts.join('/')); + const match = /^(hyperliquid|bybit|binance|kraken|coinbase|okx)\/starters\/(node|typescript|react|bun|deno|vite|next|expo)\.tar\.gz$/.exec(parts.join('/')); if (!match || stat.size > 131072) throw new Error(`Unexpected public archive: ${relative}`); const sdk = (await import(`../sdks/${match[1]}/sdk.config.mjs`)).default; await checkStarter(filename, await environmentFiles(match[1], sdk, match[2])); diff --git a/scripts/context.mjs b/scripts/context.mjs index 0cf5b4d0..1e0e87eb 100644 --- a/scripts/context.mjs +++ b/scripts/context.mjs @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'; import { readFile } from 'node:fs/promises'; export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -export const ids = ['hyperliquid', 'bybit', 'binance', 'kraken', 'coinbase']; +export const ids = ['hyperliquid', 'bybit', 'binance', 'kraken', 'coinbase', 'okx']; export async function context(id) { if (!ids.includes(id)) throw new Error(`Unknown SDK: ${id}. Choose ${ids.join(', ')}.`); diff --git a/scripts/environment-guides.mjs b/scripts/environment-guides.mjs index b147c052..dc045317 100644 --- a/scripts/environment-guides.mjs +++ b/scripts/environment-guides.mjs @@ -95,7 +95,7 @@ export function guideFields(id, sdk, environment, files) { app = orderedSteps(names, files); } - if (id === 'coinbase') { + if (id === 'coinbase' || id === 'okx') { streamFiles = streamFiles .replace('it closes the client after the first price or a failure.', 'it streams prices until you press Ctrl+C. Exceptions and reconnections are logged without stopping the client.') .replace('when you want a live update. Its promise resolves to a price string and its `finally` block closes the client.', 'to stream prices until Ctrl+C. Signal handlers close the client; exceptions and reconnections are logged without stopping it.') diff --git a/scripts/environments.mjs b/scripts/environments.mjs index 38ff4c9d..27c40d77 100644 --- a/scripts/environments.mjs +++ b/scripts/environments.mjs @@ -240,13 +240,15 @@ export async function generateEnvironments(ctx) { if (!(key in substitutions)) throw new Error(`Missing environment template field: ${key}`); return substitutions[key]; }); - if (ctx.id === 'coinbase') { + if (exchanges[ctx.id].continuous) { rendered = rendered .replace('The script prints the first update, closes its WebSocket and exits.', 'The script keeps streaming until Ctrl+C. Exceptions and reconnections are logged; signal handlers close the client.') .replace('The script receives one update, closes the client in `finally` and exits.', 'The script keeps streaming until Ctrl+C. Exceptions and reconnections are logged; signal handlers close the client.'); rendered = rendered .replace('A failed request or no update within 30 seconds needs a network check.', 'A failed request or a stream without updates needs a network check.') .replace('If a request fails or no update arrives within 30 seconds,', 'If a request fails or no stream updates arrive,'); + } + if (ctx.id === 'coinbase') { const note = browser || environment === 'expo' ? 'This is a public-market-data recipe only: the supplied configuration disables the SDK’s Node-only JWT signing helper. Keep the configuration and, for Expo, `coinbase-public-auth.cjs`. Run authenticated calls on your Node.js server. Browser REST access also depends on Coinbase CORS policy.' : 'This project uses Advanced Trade public market data. Keep credentialed account and trading calls in a server-side application.'; @@ -255,6 +257,15 @@ export async function generateEnvironments(ctx) { .replace('See Runtime compatibility for recorded verification of this configuration.', 'Keep the public-auth helper alongside the Metro configuration.') .replaceAll('[Runtime compatibility](/compatibility) for tested versions', '[Runtime setup](/compatibility) for the other project recipes'); } + if (ctx.id === 'okx') { + const note = browser + ? 'Keep the supplied Vite configuration: it resolves EventEmitter and rejects Node-only HTTPS agents. Browsers manage connections, so omit `keepAlive`. REST access depends on OKX CORS policy; use a server route if blocked. Keep credentials on your server.' + : 'These examples use public market data. Keep authenticated account and trading calls on your server. See [Runtime setup](/compatibility) for the scope of each recipe; a successful build does not verify live exchange access.'; + rendered = rendered + .replace('## Create the project', note + '\n\n## Create the project') + .replace('See Runtime compatibility for recorded verification of this configuration.', 'See Runtime setup for the scope and limits of this recipe.') + .replaceAll('[Runtime compatibility](/compatibility) for tested versions', '[Runtime setup](/compatibility) for setup details'); + } if (ctx.id === 'kraken') { const note = browser ? 'The public GET client below omits two SDK request headers that trigger a failing Kraken CORS preflight. This is an example-specific setup for public reads, not general browser REST support. See [Browser REST and CORS](/guides/troubleshooting#browser-rest-and-cors) for the limitation and a server-side alternative.' diff --git a/scripts/generate.mjs b/scripts/generate.mjs index 43854106..6b382b58 100644 --- a/scripts/generate.mjs +++ b/scripts/generate.mjs @@ -61,6 +61,13 @@ const app = await Application.bootstrapWithPlugins({ }); // Filter the documentation model before TypeDoc builds its groups and categories. app.converter.on(Converter.EVENT_RESOLVE_BEGIN, ({ project }) => { + for (const name of sdk.referenceExcludeMembers || []) { + const reflection = project.getChildByName(name.split('.')); + if (!reflection) { + throw new Error(`Missing excluded member: ${name}`); + } + project.removeReflection(reflection); + } for (const reflection of Object.values(project.reflections)) { if (reflection.kind === ReflectionKind.Property && sdk.referenceExcludeProperties?.includes(reflection.name)) { project.removeReflection(reflection); diff --git a/scripts/release-manifest.mjs b/scripts/release-manifest.mjs index f99df7db..473fcf90 100644 --- a/scripts/release-manifest.mjs +++ b/scripts/release-manifest.mjs @@ -1,7 +1,7 @@ import { readFile } from 'node:fs/promises'; export const releaseManifestPath = 'releases/package.json'; -export const sdkPackages = ['@siebly/hyperliquid-api', 'bybit-api', 'binance', '@siebly/kraken-api', 'coinbase-api']; +export const sdkPackages = ['@siebly/hyperliquid-api', 'bybit-api', 'binance', '@siebly/kraken-api', 'coinbase-api', 'okx-api']; export const stableVersion = (version) => typeof version === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version); export function releaseVersions(manifest) { diff --git a/scripts/release-refresh.test.mjs b/scripts/release-refresh.test.mjs index e2f9b02a..54d031b3 100644 --- a/scripts/release-refresh.test.mjs +++ b/scripts/release-refresh.test.mjs @@ -4,7 +4,7 @@ import { refreshReleases, updatedManifest, validateRefreshBranch, approveAndMerg import { requireMainDispatch, repository, refreshBranch, createGitHubApi } from './github-automation.mjs'; const base = 'a'.repeat(40), proposal = 'b'.repeat(40), newer = 'c'.repeat(40); -const manifest = (hl = '1.0.1') => ({ name: 'siebly-sdk-release-inputs', private: true, dependencies: { '@siebly/hyperliquid-api': hl, 'bybit-api': '4.8.4', binance: '3.6.11', '@siebly/kraken-api': '1.1.10', 'coinbase-api': '1.3.1' } }); +const manifest = (hl = '1.0.1') => ({ name: 'siebly-sdk-release-inputs', private: true, dependencies: { '@siebly/hyperliquid-api': hl, 'bybit-api': '4.8.4', binance: '3.6.11', '@siebly/kraken-api': '1.1.10', 'coinbase-api': '1.3.1', 'okx-api': '3.2.9' } }); const pr = (head = proposal) => ({ state: 'open', number: 7, html_url: `https://github.com/${repository}/pull/7`, user: { login: 'github-actions[bot]', type: 'Bot' }, head: { sha: head, ref: refreshBranch, repo: { full_name: repository } }, base: { ref: 'main', repo: { full_name: repository } } }); const comparison = () => ({ merge_base_commit: { sha: base }, total_commits: 1, files: [{ filename: 'releases/package.json', status: 'modified' }], commits: [{ author: { login: 'github-actions[bot]' } }] }); diff --git a/scripts/release-update.test.mjs b/scripts/release-update.test.mjs index fd28e03b..22f0c1a6 100644 --- a/scripts/release-update.test.mjs +++ b/scripts/release-update.test.mjs @@ -5,7 +5,7 @@ import { refreshBranch } from './github-automation.mjs'; import { readReleaseVersions, releaseVersions, validateReleaseUpdate } from './release-manifest.mjs'; const main = 'a'.repeat(40), head = 'b'.repeat(40); -const manifest = () => ({ name: 'siebly-sdk-release-inputs', private: true, dependencies: { '@siebly/hyperliquid-api': '1.0.1', 'bybit-api': '4.8.4', binance: '3.6.11', '@siebly/kraken-api': '1.1.10', 'coinbase-api': '1.3.1' } }); +const manifest = () => ({ name: 'siebly-sdk-release-inputs', private: true, dependencies: { '@siebly/hyperliquid-api': '1.0.1', 'bybit-api': '4.8.4', binance: '3.6.11', '@siebly/kraken-api': '1.1.10', 'coinbase-api': '1.3.1', 'okx-api': '3.2.9' } }); function candidate() { const before = manifest(), after = manifest(); after.dependencies['bybit-api'] = '4.8.5'; @@ -18,7 +18,7 @@ function candidate() { } test('the committed manifest is the exact configured-package build input', async () => { - assert.deepEqual(Object.keys(await readReleaseVersions()).sort(), ['@siebly/hyperliquid-api', '@siebly/kraken-api', 'binance', 'bybit-api', 'coinbase-api']); + assert.deepEqual(Object.keys(await readReleaseVersions()).sort(), ['@siebly/hyperliquid-api', '@siebly/kraken-api', 'binance', 'bybit-api', 'coinbase-api', 'okx-api']); for (const version of ['latest', '^4.8.5', '4.8.5-beta.1', '04.8.5']) { const value = manifest(); value.dependencies['bybit-api'] = version; assert.throws(() => releaseVersions(value)); diff --git a/scripts/starters.mjs b/scripts/starters.mjs index ce9b386f..6cbe00a0 100644 --- a/scripts/starters.mjs +++ b/scripts/starters.mjs @@ -18,7 +18,7 @@ function readme(sdk, environment) { ? `Install ${environmentTitles[environment]} first.` : 'Use Node.js 24 and npm.'; const native = environment === 'expo' ? '\nOpen the project in a matching Expo Go installation. Press **i** for an iOS simulator or **a** for an Android emulator.\n' : ''; const controls = ['react', 'vite', 'expo'].includes(environment) ? '\nUse **Stop stream** to close the price subscription.\n' : ''; - const optionalStream = streaming ? `\n${sdk.id === 'coinbase' ? 'To stream WebSocket prices until Ctrl+C:' : 'To receive one WebSocket price and close the connection:'}\n\n\`\`\`sh\n${stream}\n\`\`\`\n` : ''; + const optionalStream = streaming ? `\n${['coinbase', 'okx'].includes(sdk.id) ? 'To stream WebSocket prices until Ctrl+C:' : 'To receive one WebSocket price and close the connection:'}\n\n\`\`\`sh\n${stream}\n\`\`\`\n` : ''; const commonjs = environment === 'node' ? '\nFor CommonJS instead, run `node price.cjs` or `node stream.cjs`.\n' : ''; const browserNote = sdk.id === 'kraken' && ['react', 'vite'].includes(environment) ? '\nThe public GET client removes the SDK’s `UserAgent` and `locale` headers to avoid Kraken’s failing CORS preflight. Keep this configuration for these public reads only. Run authenticated REST calls on your server, never in a browser bundle. See the guide for the server-side alternative.\n' : ''; @@ -27,7 +27,9 @@ function readme(sdk, environment) { + (['react', 'vite'].includes(environment) ? '\nKeep `sdk.vite.config.js`: it excludes Node-only JWT signing from this public-data application. Browser REST access depends on Coinbase CORS policy.\n' : '') + (environment === 'expo' ? '\nRetain `coinbase-public-auth.cjs` and both Metro files: they supply the public-data configuration and exclude Node-only JWT signing.\n' : '') : ''; - return `# ${sdk.name}: ${environmentTitles[environment]}\n\nA runnable public-price example using \`${sdk.packageName}\`. No API key is required.\n\n${prerequisite}\n\n## Run\n\nFrom this folder:\n\n\`\`\`sh\n${install}\n${start}\n\`\`\`\n${native}${controls}${optionalStream}${commonjs}${browserNote}${coinbaseNote}\n[Follow the complete step-by-step guide](https://sieblyio.github.io/docs/${sdk.id}/environments/${environment}.html).\n`; + const okxNote = sdk.id === 'okx' + ? '\nKeep authenticated calls on your server. Browser projects must retain the Vite configuration and omit Node-only `keepAlive`; browser REST access depends on OKX CORS policy. See the guide for runtime setup limits.\n' : ''; + return `# ${sdk.name}: ${environmentTitles[environment]}\n\nA runnable public-price example using \`${sdk.packageName}\`. No API key is required.\n\n${prerequisite}\n\n## Run\n\nFrom this folder:\n\n\`\`\`sh\n${install}\n${start}\n\`\`\`\n${native}${controls}${optionalStream}${commonjs}${browserNote}${coinbaseNote}${okxNote}\n[Follow the complete step-by-step guide](https://sieblyio.github.io/docs/${sdk.id}/environments/${environment}.html).\n`; } export async function starterFiles(id, sdk, environment) { diff --git a/sdks/okx/docs/clients.md b/sdks/okx/docs/clients.md new file mode 100644 index 00000000..daf84965 --- /dev/null +++ b/sdks/okx/docs/clients.md @@ -0,0 +1,26 @@ +--- +title: "Choose an OKX SDK client" +description: "Choose RestClient, WebsocketClient or WebsocketAPIClient for OKX requests and streams." +--- + +# Choose a client + + +- [RestClient](/api/classes/RestClient): public market data, authenticated account reads and order commands over REST. +- [WebsocketClient](/api/classes/WebsocketClient): public and private subscriptions, connection lifecycle events and low-level WebSocket API requests. +- [WebsocketAPIClient](/api/classes/WebsocketAPIClient): promise-based order commands over WebSockets. + + +## REST and subscriptions work together + +Use one long-lived REST client for your account and region. Start with [`getTicker()`](/api/classes/RestClient#getticker), then configure credentials for [`getBalance()`](/api/classes/RestClient#getbalance). The same client covers Spot and derivatives: select the instrument and trading mode in each request rather than choosing a separate futures class. + +Use `WebsocketClient` for ongoing prices and account state. It selects the public, private or business connection from the channel, signs private logins and restores subscriptions after reconnects. Your application still owns recovery of missed state. + +## Awaitable WebSocket commands + + + +`WebsocketAPIClient` exposes methods such as `submitNewOrder()`, `amendOrder()` and `cancelOrder()`. It is not a replacement for the private orders subscription: command responses acknowledge requests, while order events describe subsequent execution. + +New applications should use `WebsocketClient`, not the exported legacy client. See [WebSocket API requests](/guides/websocket-api) for the request and response differences. diff --git a/sdks/okx/docs/compatibility.md b/sdks/okx/docs/compatibility.md new file mode 100644 index 00000000..4ea31388 --- /dev/null +++ b/sdks/okx/docs/compatibility.md @@ -0,0 +1,40 @@ +--- +title: "OKX SDK runtime setup" +description: "Choose a Node.js, browser, framework or mobile project and understand runtime verification limits." +--- + +# Runtime setup + +Choose a project for your application. Each guide starts from an empty directory and supplies installation commands, configuration and a public price example. A setup recipe is not a guarantee of exchange access from every network or region. + +## Server-side JavaScript and TypeScript + +- [Node.js / JavaScript](/environments/node): ES modules and CommonJS. +- [Node.js / TypeScript](/environments/typescript): typed imports with build and run commands. +- [Next.js](/environments/next): a Node.js server route that keeps SDK calls off the browser. +- [Bun](/environments/bun): npm dependencies and Bun scripts. +- [Deno](/environments/deno): npm dependencies and explicit runtime permissions. + +Keep all account credentials and authenticated requests server-side. Use the [authentication guide](/guides/authentication) for account reads. Regional routing and live/demo settings must match the account. + +## Browser applications + +The [React](/environments/react) and [Vite](/environments/vite) guides include browser configuration for EventEmitter and the SDK's Node-only HTTPS agent import. Keep that configuration with the examples. Node-only `keepAlive` is not available in these browser projects; the supplied adapter rejects its use rather than silently pretending to enable it. + +Browser examples use public market data only. Never put API keys, secrets or passphrases in a browser bundle. REST access still depends on OKX's CORS policy and regional restrictions. If a browser cannot call the exchange, use a server route with the SDK. A local development proxy is not a production backend. + +## React Native and Expo + +The [Expo guide](/environments/expo) supplies mobile setup and polyfills. Keep those files together and test on your target device. Browser bundle success does not establish native-device support. Private account operations still belong on a server, not in the mobile bundle. + +## Verification limits + +Type checking verifies compatibility with the documented SDK's exported types. Production browser builds verify bundling, not live REST or WebSocket connectivity. Neither establishes that authentication, order placement, recovery or account-specific permissions work. + +Bun, Deno, Next.js and mobile setup instructions must be validated in your deployment environment. Treat a public request timeout as an unresolved network or service outcome, not proof of SDK incompatibility or successful access. Before relying on any runtime, check a public REST request, a market-data subscription and clean connection shutdown from the network and region you will use. + +## Streaming lifecycle + +Command-line streams run until Ctrl+C. Browser and mobile views must stop connections when removed or when the user chooses Stop. Let the SDK reconnect after transient loss, but wait for fresh market data and reconcile account state before resuming dependent actions. + +[WebSocket subscriptions](/guides/websockets) · [Production usage](/guides/production) · [Troubleshooting](/guides/troubleshooting) diff --git a/sdks/okx/docs/getting-started.md b/sdks/okx/docs/getting-started.md new file mode 100644 index 00000000..a3c5d77c --- /dev/null +++ b/sdks/okx/docs/getting-started.md @@ -0,0 +1,40 @@ +--- +title: "OKX Node.js quickstart with JavaScript" +description: "Install okx-api and read a public BTC-USDT ticker with plain JavaScript." +--- + +# Node.js quickstart with JavaScript + +Read the live BTC-USDT last price. This public request needs no API key, secret or passphrase. + +## Installation + + + + + +## Make a request + +Save this as `prices.mjs`: + +<<< @/../examples/first-call.ts{js} + +```sh +node prices.mjs +``` + +## Example output {#verify-the-result} + +```text +BTC-USDT last price: 65000.1 +``` + +The price is illustrative. `getTicker()` returns an array of ticker objects, not the raw REST envelope. Numeric values such as `last` are decimal strings. + +If your account belongs to a regional OKX service, [select its market](/guides/configuration) before adding authenticated requests. + +## Module formats + +The `.mjs` extension selects ES modules. For CommonJS, save a `.cjs` file, replace the import with `const { RestClient } = require('okx-api')`, and keep the async function. TypeScript uses the same import with included types. + +[Market data](/guides/market-data) · [Streaming](/guides/websockets) · [Runtime setup](/compatibility) · [API reference](/reference) diff --git a/sdks/okx/docs/guides/authentication.md b/sdks/okx/docs/guides/authentication.md new file mode 100644 index 00000000..131a634a --- /dev/null +++ b/sdks/okx/docs/guides/authentication.md @@ -0,0 +1,53 @@ +--- +title: "OKX authentication and account balances" +description: "Configure server-side API credentials and read trading-account balances." +--- + +# Authentication and account balances + +OKX credentials comprise an API key, API secret and API passphrase. The passphrase is the value associated with the API key, not your account password. Use read permissions for account reads and enable trading only when your application needs order commands. Withdrawal permission is unnecessary for these examples. + +Keep credentials server-side. Never embed them in browser, React Native or Expo bundles, commit them, or expose them through public environment-variable prefixes. Use an IP allowlist where supported, and keep the machine's clock synchronized. + +## Configure credentials + +Store these in a private `.env` file excluded from version control: + +```dotenv +OKX_API_KEY=your_api_key +OKX_API_SECRET=your_api_secret +OKX_API_PASSPHRASE=your_api_passphrase +``` + +Save this helper as `authenticated-client.ts`: + +<<< @/../examples/authenticated-client.ts + +The SDK signs requests. Do not construct signature headers manually. Defaults use the live global service. Select the account's [region and environment](/guides/configuration) consistently for REST and WebSockets. + +## Read starting account state + +Save as `account-state.ts` beside the helper: + +<<< @/../examples/account-state.ts + +### Compile and run the examples + +In your project directory, install the SDK and compiler dependencies, then compile these `.ts` files explicitly: + +```sh +npm install okx-api +npm install --save-dev typescript @types/node +npx tsc --target ES2022 --module NodeNext --strict --skipLibCheck --outDir dist authenticated-client.ts account-state.ts +node --env-file=.env dist/account-state.js +``` + +Run with Node.js 24. Keep `.env` in the project directory and run the commands from there. Relative `.js` imports refer to the compiled files. This explicit file list does not depend on the [TypeScript starter](/environments/typescript)'s `.mts` include pattern. + +For the other private examples, replace `account-state.ts` in the compile command with `private-stream.ts`, `first-order.ts` or `ws-api.ts`, retaining `authenticated-client.ts`. Run the corresponding emitted `.js` file as shown in its guide. Compilation alone does not send requests. + +`getBalance()` reads the trading account, including per-currency `details`. It is different from `getBalances()`, which reads funding-account balances. An empty currency balance or no open orders is not automatically an authentication failure. This example prints one page of open Spot orders; production bootstrap must paginate when necessary. + +Use this read for startup or diagnosis, then keep the [private account stream](/guides/private-streams) connected. Do not repeat a full snapshot after every order. Avoid sending account balances or raw authenticated error objects to public logs. + +[getBalance reference](/api/classes/RestClient#getbalance) · [Order management](/guides/orders) diff --git a/sdks/okx/docs/guides/configuration.md b/sdks/okx/docs/guides/configuration.md new file mode 100644 index 00000000..0415e31f --- /dev/null +++ b/sdks/okx/docs/guides/configuration.md @@ -0,0 +1,34 @@ +--- +title: "OKX SDK configuration and environments" +description: "Configure regions, optional demo trading, timeouts and connection options." +--- + +# Configuration and environments + +`RestClient` accepts SDK options first and Axios request options second. `WebsocketClient` and `WebsocketAPIClient` accept WebSocket options, including credentials in an `accounts` array. + +<<< @/../examples/configure-rest.ts + +This example selects EEA live services. Remove the `market` option for the default global service, or select the region where your account is registered. No private API credentials are included in this public configuration example. + +## Regional services {#environments} + +The pinned SDK supports `market: 'GLOBAL'`, `'prod'` (the global default), `'OPENAPI_GLOBAL'`, `'EEA'` and `'US'`. `OPENAPI_GLOBAL` selects the alternative global REST host while using the same WebSocket routing as global. + +Use the SDK's `market` flag rather than copying API URLs. A region is not a way to bypass availability restrictions. Supported products, credentials and endpoints can differ by account registration and jurisdiction. Choose the same region for REST, subscriptions and WebSocket API commands. + +## Demo trading + +Set `demoTrading: true` on each REST and WebSocket client to opt into simulated trading. Use API keys created for OKX demo trading, not live keys. The SDK supplies the REST simulation header and selects demo WebSocket routes; do not add manual headers or endpoint URLs. + +The default is live trading. Changing just the REST client does not change an existing WebSocket client. Check demo support for the selected region and feature against OKX's current documentation; a flag in the SDK does not guarantee every product is available in demo. + +## Timeouts and connections + +Set Axios `timeout` in milliseconds in the second REST constructor argument. `keepAlive` belongs in the first argument; when enabled it creates an HTTPS agent. Leave it false when providing a custom proxy agent. + +WebSocket options include `pingInterval`, `pongTimeout`, `reconnectTimeout` and `wsOptions`. Keep heartbeat handling enabled. Avoid extremely short timeouts that turn transient network delays into reconnect storms. + +The REST `parse_exceptions` flag controls error post-processing; see [Error handling](/guides/errors). Regional routing, demo trading and proxy settings should come from one validated server configuration so clients cannot accidentally target different accounts or environments. + +[Proxies](/guides/proxies) · [RestClientOptions reference](/api/interfaces/RestClientOptions) diff --git a/sdks/okx/docs/guides/errors.md b/sdks/okx/docs/guides/errors.md new file mode 100644 index 00000000..7e42f99e --- /dev/null +++ b/sdks/okx/docs/guides/errors.md @@ -0,0 +1,31 @@ +--- +title: "OKX SDK error handling" +description: "Separate transport failures, API errors and per-order rejection codes." +--- + +# Error handling + +OKX has several layers of failure. Handling only HTTP status or promise rejection is insufficient for order commands. + +## REST responses + +On a successful top-level `code: '0'`, the SDK returns the nested `data` array. A nonzero top-level code rejects the request. Order and batch results can still contain item-level `sCode` and `sMsg`; inspect each result even when the promise resolves. + +With default `parse_exceptions: true`, API errors are generally thrown as the response body, while some network failures are thrown as strings. Do not assume every caught value is an `Error` with `.message`. Use an `unknown` catch value and narrow the fields your application needs. + +Setting `parse_exceptions: false` preserves the underlying exception or wrapper. It does not convert rejected requests into successful responses. Raw Axios objects can contain signed headers and request configuration, so redact them before writing diagnostics. + +## WebSocket responses + +Listen for `exception` as well as subscription `response` and data `update` events. WebSocket API commands can reject or return an envelope whose top-level `code` and per-item `sCode` require inspection. See the [checked cancellation example](/guides/websocket-api). + +## Decide whether a retry is safe + +- Correct missing permissions, wrong region/environment, invalid increments or insufficient funds before retrying. +- Rate-limit requests and honor exchange retry guidance. Back off with jitter for transient reads rather than retrying every client simultaneously. +- A timed-out order or cancellation has an uncertain outcome. Use saved IDs and private events, followed by scoped REST reconciliation, before deciding what to do next. +- Batch operations are not necessarily atomic. Record accepted and rejected items separately; do not replay the whole batch blindly. + +Keep sanitized operation names, instrument IDs, client/exchange order IDs, timestamps and API codes in diagnostics. Keep secrets, signatures and full account payloads out of shared logs. + +[Order recovery](/guides/orders#recover-an-uncertain-outcome) · [Troubleshooting](/guides/troubleshooting) diff --git a/sdks/okx/docs/guides/market-data.md b/sdks/okx/docs/guides/market-data.md new file mode 100644 index 00000000..40dc2cbb --- /dev/null +++ b/sdks/okx/docs/guides/market-data.md @@ -0,0 +1,22 @@ +--- +title: "OKX public market data" +description: "Read instruments, order books and Spot or swap tickers through the SDK." +--- + +# Public market data + +Use `RestClient` without credentials for public data. An instrument ID includes separators: `BTC-USDT` is Spot and `BTC-USDT-SWAP` is a perpetual swap. Availability depends on region and instrument status. + +<<< @/../examples/market-data.ts + +Each method returns its `data` array. Inspect instrument metadata before choosing order values: `tickSz` is the price increment, `lotSz` the size increment, and `minSz` the minimum size. Also check the current instrument state and contract fields. Spot limit sizes are in base currency; derivative sizes generally represent contracts and must be interpreted using the instrument metadata. + +Keep price and size values as decimal strings when sending requests. Do not assume JavaScript floating-point arithmetic preserves valid increments. + +## Order books + +`getOrderBook()` is a REST snapshot. Book levels include price and size along with additional fields defined by OKX. A single snapshot is not a continuously maintained order book. + +For ongoing data use [WebSocket subscriptions](/guides/websockets). Snapshot channels such as `books5` simplify a small displayed book. Incremental book channels require the exchange's snapshot, sequence and checksum rules, plus recovery after gaps. The SDK transports messages; it does not maintain a verified local order book for you. + +[getInstruments reference](/api/classes/RestClient#getinstruments) · [getOrderBook reference](/api/classes/RestClient#getorderbook) · [OKX public data](https://www.okx.com/docs-v5/en/#public-data-rest-api-get-instruments) diff --git a/sdks/okx/docs/guides/orders.md b/sdks/okx/docs/guides/orders.md new file mode 100644 index 00000000..593b49ee --- /dev/null +++ b/sdks/okx/docs/guides/orders.md @@ -0,0 +1,72 @@ +--- +title: "OKX order management" +description: "Place, observe, amend and cancel a Spot order using REST and private account events." +--- + +# Order management + +This walkthrough sends real Spot order commands to the default live global service. Each CLI command is explicit; running `place` can spend funds. For simulated trading, configure [demo trading](/guides/configuration#demo-trading) on every client and use separate demo credentials before running anything. + +## Before submitting + +1. Configure [authenticated account access](/guides/authentication) with read and trading permissions. +2. Check `BTC-USDT` instrument metadata using [`getInstruments()`](/api/classes/RestClient#getinstruments). Choose price and size strings satisfying its increments, minimums and current exchange limits. This example uses `tdMode: 'cash'`, so it buys Spot without changing leverage or position mode. +3. Start the [private stream](/guides/private-streams) with the same account, region and environment. Wait for authentication and a successful `orders` subscription acknowledgement before submitting. +4. Record a unique alphanumeric `clOrdId` of at most 32 characters before sending. Keep it for recovery even if the command never returns an exchange order ID. + +Add your own values to `.env`: + +```dotenv +OKX_CLIENT_ORDER_ID=spotExample20261111001 +OKX_LIMIT_PRICE=60000 +OKX_ORDER_SIZE=0.0001 +OKX_NEW_PRICE=59000 +``` + +Prices and sizes are illustrative, not recommendations or guaranteed-valid orders. Choose a buy price below the current best bid. A `post_only` order that would immediately take liquidity is rejected or cancelled according to the exchange's rules; a resting order can still fill before you cancel it. + +## Place and observe + +Save `first-order.ts` beside the [credential helper](/guides/authentication), follow the [compile instructions](/guides/authentication#compile-and-run-the-examples) with `first-order.ts` as the entry file, then run the emitted file: + +<<< @/../examples/first-order.ts + +```sh +node --env-file=.env dist/first-order.js place +``` + +Save the returned `ordId`. REST removes the top-level response envelope, but each result still has `sCode` and `sMsg`. Only `sCode: '0'` indicates that item was accepted. The sample checks each returned result. + +Follow the matching `ordId` or `clOrdId` on the private `orders` channel. Typical states include `live`, `partially_filled`, `filled` and `canceled`. Read `accFillSz` for cumulative filled quantity; use fill identifiers and trade history when recording individual executions. Acceptance is not evidence of a fill. + +## Amend an open order + +```sh +node --env-file=.env dist/first-order.js amend +``` + +This changes the price using `newPx` and explicitly sets `cxlOnFail: false`. It does not request cancellation if the amendment fails. Check the item response and subsequent order events; orders can fill while an amendment is in flight. If amending size with `newSz`, it is the new total size, not an additional quantity. + +## Cancel remaining quantity + +```sh +node --env-file=.env dist/first-order.js cancel +``` + +Cancellation requests do not reverse fills. Follow the order stream until the resulting state is known, accounting for fills that race with cancellation. A filled Spot buy leaves an asset balance even if any remainder is cancelled. + +## Recover an uncertain outcome + +The sample never retries order commands automatically. A timeout can occur after OKX accepts a request. Check matching private events first, then use a scoped read if the outcome is still unknown: + +```sh +node --env-file=.env dist/first-order.js status +``` + +This uses [`getOrderDetails()`](/api/classes/RestClient#getorderdetails) with the saved client ID. For records beyond that endpoint's scope, use [`getOrderHistory()`](/api/classes/RestClient#getorderhistory), [`getOrderHistoryArchive()`](/api/classes/RestClient#getorderhistoryarchive) and [`getFills()`](/api/classes/RestClient#getfills), with pagination and appropriate filters. Empty results or delayed history are not proof the order failed. + +Treat client IDs as correlation identifiers, not a guarantee of indefinite idempotency. Do not reuse one for a new intended order or resubmit an uncertain request merely because its response was lost. Use REST for recovery, not after every successful command. + +Closing a process or WebSocket does not cancel open orders. + +[Private account updates](/guides/private-streams) · [Error handling](/guides/errors) · [OKX order contract](https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order) diff --git a/sdks/okx/docs/guides/private-streams.md b/sdks/okx/docs/guides/private-streams.md new file mode 100644 index 00000000..57726238 --- /dev/null +++ b/sdks/okx/docs/guides/private-streams.md @@ -0,0 +1,45 @@ +--- +title: "OKX private account updates" +description: "Observe orders, balances and positions with REST bootstrap and gap recovery." +--- + +# Private account updates + +Private subscriptions keep account state current without polling after every order. The example below only observes changes: it does not place, modify or cancel orders. + +Save it as `private-stream.ts` beside the [credential helper](/guides/authentication), then use the [compile instructions](/guides/authentication#compile-and-run-the-examples) with `private-stream.ts` as the entry file. It runs until Ctrl+C. + +<<< @/../examples/private-stream.ts + +```sh +node --env-file=.env dist/private-stream.js +``` + +## What each channel provides + +- `orders` with `instType: 'SPOT'` reports Spot order changes. It does **not** send an initial snapshot of all open orders. +- `account` supplies trading-account balance information. Apply currency updates to your existing store; do not discard currencies absent from a partial update. +- `positions` with `instType: 'ANY'` covers available position types for the account. Spot cash holdings live in balances, not derivative position rows. + +For a derivatives workflow, subscribe to the corresponding order instrument type as well. Access and available channels depend on your account and region. Authentication success alone does not mean every requested channel was accepted: inspect each subscription acknowledgement. + +The example prints events for inspection. It is not a complete account-state reducer or a reconciliation service. Keep private data out of shared logs. + +## Initialize without losing changes + +Connect and confirm the needed subscriptions, buffering incoming updates while loading REST starting state. Use [`getBalance()`](/api/classes/RestClient#getbalance), [`getOrderList()`](/api/classes/RestClient#getorderlist) and [`getPositions()`](/api/classes/RestClient#getpositions). Paginate open orders where necessary. The [account-state example](/guides/authentication#read-starting-account-state) shows these reads but deliberately does not merge concurrent events. + +Combine snapshots with buffered updates using order IDs, update times and execution identifiers. Preserve terminal orders and cumulative fills so that an older REST row cannot overwrite a newer event. Do not treat the separate REST responses as one atomic snapshot. Deduplicate repeated events before triggering downstream work. + +## Recover after a gap + +On `reconnect`, mark affected state stale and pause decisions that depend on it. On `reconnected`, confirm authentication and subscriptions again, buffer incoming events and reconcile: + +1. Refresh balances, positions and all relevant open orders. +2. Query recent order history and fills covering the outage, with overlap and pagination. An order missing from the open-order list may have filled or been cancelled while disconnected. +3. Resolve uncertain commands by saved IDs. Merge and deduplicate buffered events and history before marking state current. +4. Resume stream-driven updates. Keep recovery scoped and rate-limited rather than polling every order continuously. + +If private streaming is temporarily unavailable, use a bounded, rate-limited REST fallback appropriate to the account's limits and mark the reduced freshness explicitly. Once streams are restored, reconcile before turning polling off. Avoid acting on stale exposure merely because the socket is open. + +[Production usage](/guides/production) · [Order management](/guides/orders) · [OKX orders channel](https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-order-channel) diff --git a/sdks/okx/docs/guides/production.md b/sdks/okx/docs/guides/production.md new file mode 100644 index 00000000..82698de3 --- /dev/null +++ b/sdks/okx/docs/guides/production.md @@ -0,0 +1,33 @@ +--- +title: "OKX production usage" +description: "Manage connection health, account-state recovery, request policy and upgrades." +--- + +# Production usage + +Use long-lived clients with a consistent region and environment. Keep credentials on the server, use the minimum permissions and an IP allowlist, synchronize time and rotate keys according to your operational policy. + + + +OKX limits vary by endpoint, instrument, account and operation. Some trading limits are shared between REST and WebSocket requests. Moving commands to WebSockets does not remove those limits. Paginate history and open-order reads and budget reconciliation traffic separately from routine requests. + + + +## OKX recovery events + +Use `reconnect` and unexpected `close` events to invalidate affected state and `reconnected` to start recovery, not to mark it complete. Confirm private authentication and all required subscription acknowledgements. The `orders` channel does not initialize open orders, so combine REST bootstrap and history/fill repair with buffered stream events. See [Private account updates](/guides/private-streams). + +For derivatives, key state by account, instrument and position side where applicable. Derivative contract sizes and Spot quantities are not interchangeable. Keep balance, order and position events separate rather than assuming they arrive together. + +## Shut down cleanly + +Stop accepting new work, resolve or record in-flight command outcomes, persist order identifiers and recovery checkpoints, then call `closeAll()` on subscription clients and `getWSClient().closeAll()` on WebSocket API clients. Remove timers and listeners owned by your application. Closing a connection does not cancel an order; cancellation is a separate trading action. + +## Logging {#observe-failures-without-exposing-credentials} + +Record operation names, sanitized response codes, duration, retries and reconnects. Select fields using the SDK's [error format](/guides/errors); whole request and error objects can contain credentials. + + + + +Recheck regional and demo support, request/response types and order error handling when upgrading. Public connectivity checks do not validate private permissions or trading behavior. diff --git a/sdks/okx/docs/guides/proxies.md b/sdks/okx/docs/guides/proxies.md new file mode 100644 index 00000000..46874fff --- /dev/null +++ b/sdks/okx/docs/guides/proxies.md @@ -0,0 +1,40 @@ +--- +title: "OKX SDK proxy configuration" +description: "Route REST and WebSocket traffic through HTTP, HTTPS or SOCKS proxies." +--- + +# Use a proxy + + + +Allow the proxy's egress IP when your API key uses an IP allowlist. Do not log proxy credentials. + +## HTTP or HTTPS proxy for REST + +Pass Axios proxy options in the REST client's second constructor argument: + +<<< @/../examples/proxy-rest.ts + + + +## Proxy agents for REST and WebSockets + + + +<<< @/../examples/proxy-agent.ts + +`proxy: false` disables Axios proxy handling, including environment-variable proxy routing, so the custom agent controls the route. Keep `keepAlive: false`; enabling it replaces the custom HTTPS agent. + +REST configuration does not route WebSocket traffic. Set `wsOptions.agent` separately, including on `WebsocketAPIClient` when using WebSocket commands. Unlike token-based exchange SDKs, OKX private WebSockets authenticate with signed login messages rather than fetching a REST token. + +## SOCKS5 + + + +Use the SOCKS agent for both `httpsAgent` and `wsOptions.agent`, retaining `proxy: false` and `keepAlive: false` for REST. + +## Verify both transports + +Make a public `getTicker({ instId: 'BTC-USDT' })` request and a public ticker subscription using the configured clients. Confirm REST and WebSocket connections separately in proxy logs. A successful REST request alone does not verify the WebSocket path. + + diff --git a/sdks/okx/docs/guides/troubleshooting.md b/sdks/okx/docs/guides/troubleshooting.md new file mode 100644 index 00000000..eb629eb7 --- /dev/null +++ b/sdks/okx/docs/guides/troubleshooting.md @@ -0,0 +1,36 @@ +--- +title: "Troubleshoot the OKX SDK" +description: "Diagnose authentication, regions, missing updates and uncertain orders." +--- + +# Troubleshooting + +## Public request works but account read fails + +Check all three credential fields: `apiKey`, `apiSecret` and `apiPass`. Confirm the key's read permissions, IP allowlist and the server's clock. Match the account's regional `market` and live/demo environment. Never paste credentials into an issue or diagnostic screenshot. + +## Successful response but order rejected + +REST returns the unwrapped `data` array. Check each order result's `sCode` and `sMsg`, not a nonexistent top-level `response.code`. For WebSocket API commands, check both the envelope code and item codes. Verify instrument metadata, trading mode, available balance and current exchange limits. + +## No private order updates after connecting + +An open connection is not an authenticated, subscribed connection. Check login and subscription responses and the exact instrument type. The orders channel sends changes, not an initial open-order snapshot. An account with no new activity can legitimately be quiet. Initialize orders through REST without polling after each submission. + +## Reconnected but state is wrong + +The SDK restores connections and subscriptions, not missed fills or your local store. Reconcile open orders, recent history, fills, balances and positions, accounting for events arriving during those reads. Do not overwrite a newer terminal order with an older snapshot. + +## Timeout during an order command + +Treat the outcome as unknown. Look for matching private events, then query by the saved order/client ID. Do not submit a replacement until the original outcome is resolved. History can lag and empty results are inconclusive. + +## Browser or mobile request fails + +Keep private requests on a server. Browser CORS, mobile network policy and framework bundling are separate concerns from whether a package can be imported. Follow the relevant [runtime guide](/compatibility); do not expose secrets or disable transport security to make a client-side request work. + +## Proxy or connection problem + +Verify REST and WebSocket routes independently. Check the proxy agent, TLS trust, outbound rules and IP allowlist. Keep SDK `keepAlive` disabled with a custom agent. See [Proxies](/guides/proxies). + + diff --git a/sdks/okx/docs/guides/websocket-api.md b/sdks/okx/docs/guides/websocket-api.md new file mode 100644 index 00000000..099655b4 --- /dev/null +++ b/sdks/okx/docs/guides/websocket-api.md @@ -0,0 +1,35 @@ +--- +title: "OKX WebSocket API requests" +description: "Use promise-based trading commands over the authenticated WebSocket connection." +--- + +# WebSocket API requests + +Use `WebsocketAPIClient` for request/response order commands over a persistent authenticated connection. Use `WebsocketClient` subscriptions separately to follow the resulting account changes. + + + +## Cancel an existing order + +This example cancels remaining quantity on a real `BTC-USDT` order. Set `OKX_CLIENT_ORDER_ID` to an existing order you intend to cancel. Save the example as `ws-api.ts` beside the [credential helper](/guides/authentication). Follow the [compile instructions](/guides/authentication#compile-and-run-the-examples) with `ws-api.ts` as the entry file and use the same account, region and live/demo environment as the original order: + +<<< @/../examples/ws-api.ts + +```sh +node --env-file=.env dist/ws-api.js +``` + +Unlike the REST client, the WebSocket wrapper returns a response envelope containing `code`, `msg` and `data`. Inspect the top-level result and each item's `sCode` and `sMsg`. Transport or request errors may also reject the promise. Batch responses require checking every item; one successful item does not establish that all requests succeeded. + +## Available commands + +- [`submitNewOrder()`](/api/classes/WebsocketAPIClient#submitneworder): submit an order. +- [`amendOrder()`](/api/classes/WebsocketAPIClient#amendorder): amend an existing order. +- [`cancelOrder()`](/api/classes/WebsocketAPIClient#cancelorder): cancel remaining quantity. +- [`getWSClient()`](/api/classes/WebsocketAPIClient#getwsclient): access lifecycle events and close the underlying client. + +The wrapper opens and authenticates connections as needed. A long-running service should retain its client instead of creating and closing it for every order; this single-command example closes on completion. + +For lower-level use, [`sendWSAPIRequest()`](/api/classes/WebsocketClient#sendwsapirequest) takes a connection key, operation and parameters. Obtain private routing with `getMarketWsKey('private')` rather than hardcoding a connection key or URL. Keep `market` and `demoTrading` consistent across clients. + +An acknowledged cancellation is not proof no fill occurred. Observe the private order stream and reconcile unknown outcomes as described in [Order management](/guides/orders#recover-an-uncertain-outcome). Never replay a timed-out trade automatically. diff --git a/sdks/okx/docs/guides/websockets.md b/sdks/okx/docs/guides/websockets.md new file mode 100644 index 00000000..2d1d367a --- /dev/null +++ b/sdks/okx/docs/guides/websockets.md @@ -0,0 +1,28 @@ +--- +title: "OKX WebSocket subscriptions" +description: "Subscribe to public market updates and handle connection recovery." +--- + +# WebSocket subscriptions + +`WebsocketClient` opens connections as needed, maintains heartbeats and resubscribes after reconnects. This public example runs until Ctrl+C: + +<<< @/../examples/stream.ts + +Use `node stream.ts` on Node.js 24 after installing `okx-api`, or compile with your TypeScript project. + +## Subscription acknowledgements and data + +`subscribe()` returns an array of promises because channels can use different connections. Use `Promise.all(ws.subscribe(...))`, rather than awaiting the array itself. Observe `response` for subscription acknowledgements and `exception` for failures. An open socket does not prove a subscription succeeded. + +The `update` event carries channel messages. Read `arg.channel` and instrument identifiers before dispatching the data to your handlers. A quiet private stream can be healthy when nothing changes. + +Subscribe to multiple channels with an array. Use `unsubscribe()` with the same channel arguments when a view or strategy no longer needs them. Its returned promise array can also be awaited with `Promise.all`. + +## Connection recovery + +Treat data as stale on `reconnect` and unexpected closure. On `reconnected`, wait for restored subscriptions and fresh data. For incremental books, rebuild according to the channel contract; for accounts, [reconcile missed state](/guides/private-streams#recover-after-a-gap). + +Call `closeAll()` during deliberate shutdown to stop connections. This does not cancel orders. + +[Private account updates](/guides/private-streams) · [WebsocketClient reference](/api/classes/WebsocketClient) diff --git a/sdks/okx/docs/index.md b/sdks/okx/docs/index.md new file mode 100644 index 00000000..dd05e517 --- /dev/null +++ b/sdks/okx/docs/index.md @@ -0,0 +1,34 @@ +--- +title: "OKX API SDK for Node.js, JavaScript and TypeScript" +description: "Use okx-api for OKX REST, market streams, private account updates and WebSocket trading requests." +--- + + + +# OKX API SDK + +[`okx-api`](https://www.npmjs.com/package/okx-api) provides JavaScript and TypeScript clients for OKX Spot, margin and derivatives APIs. Read prices, observe account changes and manage orders through typed REST and WebSocket methods. + +
JavaScript & TypeScript · v{{ sdk.version }} · {{ sdk.license }} license
+ +## Installation + + + +## First request {#usage} + +Follow the [Node.js quickstart](/getting-started) to read a public Bitcoin price without credentials. Examples use live services by default; [demo trading](/guides/configuration#demo-trading) is an optional separate environment. + +## Choose a client + + + +## Next steps + +- [Read market data](/guides/market-data) or [stream tickers](/guides/websockets). +- [Read account balances](/guides/authentication) and [observe private account changes](/guides/private-streams). +- [Place, amend and cancel orders](/guides/orders). +- [Send WebSocket API requests](/guides/websocket-api). +- [Configure regional services](/guides/configuration), [prepare for production](/guides/production) or [browse the reference](/reference). diff --git a/sdks/okx/docs/reference.md b/sdks/okx/docs/reference.md new file mode 100644 index 00000000..4f9ca994 --- /dev/null +++ b/sdks/okx/docs/reference.md @@ -0,0 +1,34 @@ +--- +title: "OKX SDK API reference" +description: "Find REST and WebSocket clients, methods and typed requests for okx-api." +--- + +# API reference + +Generated reference for the documented `okx-api` release. JavaScript and TypeScript use the same methods; TypeScript additionally checks request and response shapes. + +## Clients + +- [RestClient](/api/classes/RestClient) +- [WebsocketClient](/api/classes/WebsocketClient) +- [WebsocketAPIClient](/api/classes/WebsocketAPIClient) + +## Common methods + +| Task | Method | +| --- | --- | +| Public ticker | [getTicker](/api/classes/RestClient#getticker) | +| Instrument metadata | [getInstruments](/api/classes/RestClient#getinstruments) | +| Order-book snapshot | [getOrderBook](/api/classes/RestClient#getorderbook) | +| Trading-account balances | [getBalance](/api/classes/RestClient#getbalance) | +| Funding-account balances | [getBalances](/api/classes/RestClient#getbalances) | +| Submit / amend / cancel | [submitOrder](/api/classes/RestClient#submitorder), [amendOrder](/api/classes/RestClient#amendorder), [cancelOrder](/api/classes/RestClient#cancelorder) | +| Recover one order | [getOrderDetails](/api/classes/RestClient#getorderdetails) | +| Open orders and positions | [getOrderList](/api/classes/RestClient#getorderlist), [getPositions](/api/classes/RestClient#getpositions) | +| Order and execution history | [getOrderHistory](/api/classes/RestClient#getorderhistory), [getFills](/api/classes/RestClient#getfills) | +| Subscribe to updates | [subscribe](/api/classes/WebsocketClient#subscribe) | +| WebSocket order command | [submitNewOrder](/api/classes/WebsocketAPIClient#submitneworder) | + +Search methods and types with **⌘K / Ctrl+K**. Use the [authentication](/guides/authentication), [order lifecycle](/guides/orders) and [private stream](/guides/private-streams) guides for complete workflows. + +[Reference index](/api/) · [OKX API documentation](https://www.okx.com/docs-v5/en/) diff --git a/sdks/okx/examples/account-state.ts b/sdks/okx/examples/account-state.ts new file mode 100644 index 00000000..54e3b43c --- /dev/null +++ b/sdks/okx/examples/account-state.ts @@ -0,0 +1,17 @@ +import { client } from './authenticated-client.js'; + +async function main() { + const [balances, orders, positions] = await Promise.all([ + client.getBalance(), + client.getOrderList({ instType: 'SPOT' }), + client.getPositions(), + ]); + console.log('Trading account balances:', balances); + console.log('Open Spot orders (one page):', orders); + console.log('Positions:', positions); +} + +main().catch((error) => { + console.error('Account read failed:', error); + process.exitCode = 1; +}); diff --git a/sdks/okx/examples/authenticated-client.ts b/sdks/okx/examples/authenticated-client.ts new file mode 100644 index 00000000..09459bb2 --- /dev/null +++ b/sdks/okx/examples/authenticated-client.ts @@ -0,0 +1,17 @@ +import { RestClient } from 'okx-api'; + +function requiredEnv(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name} in your server environment.`); + } + return value; +} + +export const credentials = { + apiKey: requiredEnv('OKX_API_KEY'), + apiSecret: requiredEnv('OKX_API_SECRET'), + apiPass: requiredEnv('OKX_API_PASSPHRASE'), +}; + +export const client = new RestClient(credentials); diff --git a/sdks/okx/examples/configure-rest.ts b/sdks/okx/examples/configure-rest.ts new file mode 100644 index 00000000..dd2001ab --- /dev/null +++ b/sdks/okx/examples/configure-rest.ts @@ -0,0 +1,8 @@ +import { RestClient, WebsocketClient } from 'okx-api'; + +// Select the region where your account is registered on both transports. +export const client = new RestClient( + { market: 'EEA', demoTrading: false, keepAlive: true }, + { timeout: 20_000 }, +); +export const ws = new WebsocketClient({ market: 'EEA', demoTrading: false }); diff --git a/sdks/okx/examples/first-call.ts b/sdks/okx/examples/first-call.ts new file mode 100644 index 00000000..3f3d8b1c --- /dev/null +++ b/sdks/okx/examples/first-call.ts @@ -0,0 +1,15 @@ +import { RestClient } from 'okx-api'; + +const client = new RestClient(); + +async function main() { + const tickers = await client.getTicker({ instId: 'BTC-USDT' }); + for (const ticker of tickers) { + console.log(`${ticker.instId} last price: ${ticker.last}`); + } +} + +main().catch((error) => { + console.error('Ticker request failed:', error); + process.exitCode = 1; +}); diff --git a/sdks/okx/examples/first-order.ts b/sdks/okx/examples/first-order.ts new file mode 100644 index 00000000..2603fcf4 --- /dev/null +++ b/sdks/okx/examples/first-order.ts @@ -0,0 +1,66 @@ +import { client } from './authenticated-client.js'; + +function requiredEnv(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name} before running this command.`); + } + return value; +} + +function reportResults(results: { sCode: string; sMsg: string; ordId: string }[]) { + for (const result of results) { + console.log(result); + if (result.sCode !== '0') { + throw new Error(`Order request rejected: ${result.sCode} ${result.sMsg}`); + } + } +} + +async function main() { + const command = process.argv[2]; + const instId = 'BTC-USDT'; + const clOrdId = requiredEnv('OKX_CLIENT_ORDER_ID'); + + switch (command) { + case 'place': { + // Persist this ID before sending. Do not automatically retry an uncertain request. + console.log('Submitting client order ID:', clOrdId); + reportResults(await client.submitOrder({ + instId, + clOrdId, + tdMode: 'cash', + side: 'buy', + ordType: 'post_only', + px: requiredEnv('OKX_LIMIT_PRICE'), + sz: requiredEnv('OKX_ORDER_SIZE'), + })); + break; + } + case 'status': { + console.log(await client.getOrderDetails({ instId, clOrdId })); + break; + } + case 'amend': { + reportResults(await client.amendOrder({ + instId, + clOrdId, + newPx: requiredEnv('OKX_NEW_PRICE'), + cxlOnFail: false, + })); + break; + } + case 'cancel': { + reportResults(await client.cancelOrder({ instId, clOrdId })); + break; + } + default: { + throw new Error('Choose place, status, amend or cancel. These commands use live funds.'); + } + } +} + +main().catch((error) => { + console.error('Order command failed; resolve the outcome before retrying:', error); + process.exitCode = 1; +}); diff --git a/sdks/okx/examples/market-data.ts b/sdks/okx/examples/market-data.ts new file mode 100644 index 00000000..a79925a6 --- /dev/null +++ b/sdks/okx/examples/market-data.ts @@ -0,0 +1,17 @@ +import { RestClient } from 'okx-api'; + +const client = new RestClient(); + +async function main() { + const instruments = await client.getInstruments({ instType: 'SPOT', instId: 'BTC-USDT' }); + const book = await client.getOrderBook({ instId: 'BTC-USDT', sz: '5' }); + const swaps = await client.getTickers({ instType: 'SWAP' }); + console.log('Spot instrument rules:', instruments); + console.log('Five book levels:', book); + console.log('Swap tickers:', swaps); +} + +main().catch((error) => { + console.error('Market data request failed:', error); + process.exitCode = 1; +}); diff --git a/sdks/okx/examples/private-stream.ts b/sdks/okx/examples/private-stream.ts new file mode 100644 index 00000000..8ff605ae --- /dev/null +++ b/sdks/okx/examples/private-stream.ts @@ -0,0 +1,36 @@ +import { WebsocketClient } from 'okx-api'; +import { credentials } from './authenticated-client.js'; + +const ws = new WebsocketClient({ accounts: [credentials] }); +ws.on('authenticated', () => { + console.log('Private connection authenticated.'); +}); +ws.on('response', (message) => { + console.log('Subscription response:', message); +}); +ws.on('update', (message) => { + console.log('Account update:', message); +}); +ws.on('exception', (error) => { + console.error('Private stream exception:', error); +}); +ws.on('reconnect', () => { + console.log('Mark account state stale; pause dependent actions.'); +}); +ws.on('reconnected', () => { + console.log('Confirm subscriptions, then reconcile missing state through REST.'); +}); + +Promise.all(ws.subscribe([ + { channel: 'orders', instType: 'SPOT' }, + { channel: 'account' }, + { channel: 'positions', instType: 'ANY' }, +])).catch((error) => { + console.error('Private subscription failed:', error); + ws.closeAll(); + process.exitCode = 1; +}); + +process.once('SIGINT', () => { + ws.closeAll(); +}); diff --git a/sdks/okx/examples/proxy-agent.ts b/sdks/okx/examples/proxy-agent.ts new file mode 100644 index 00000000..3099ff8f --- /dev/null +++ b/sdks/okx/examples/proxy-agent.ts @@ -0,0 +1,15 @@ +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { RestClient, WebsocketClient } from 'okx-api'; + +const proxyUrl = process.env.PROXY_URL; +if (!proxyUrl) { + throw new Error('Set PROXY_URL to your HTTP or HTTPS proxy URL.'); +} +const agent = new HttpsProxyAgent(proxyUrl); + +export const client = new RestClient({ keepAlive: false }, { + httpsAgent: agent, + proxy: false, + timeout: 20_000, +}); +export const ws = new WebsocketClient({ wsOptions: { agent } }); diff --git a/sdks/okx/examples/proxy-rest.ts b/sdks/okx/examples/proxy-rest.ts new file mode 100644 index 00000000..516f1a23 --- /dev/null +++ b/sdks/okx/examples/proxy-rest.ts @@ -0,0 +1,6 @@ +import { RestClient } from 'okx-api'; + +export const client = new RestClient({}, { + timeout: 20_000, + proxy: { protocol: 'http', host: '127.0.0.1', port: 8080 }, +}); diff --git a/sdks/okx/examples/stream.ts b/sdks/okx/examples/stream.ts new file mode 100644 index 00000000..05c7ee0b --- /dev/null +++ b/sdks/okx/examples/stream.ts @@ -0,0 +1,28 @@ +import { WebsocketClient } from 'okx-api'; + +const ws = new WebsocketClient(); +ws.on('update', (message) => { + console.log('Market update:', message); +}); +ws.on('response', (message) => { + console.log('Subscription response:', message); +}); +ws.on('exception', (error) => { + console.error('WebSocket exception:', error); +}); +ws.on('reconnect', () => { + console.log('Reconnecting; market data may be stale.'); +}); +ws.on('reconnected', () => { + console.log('Connection restored; wait for fresh market data.'); +}); + +Promise.all(ws.subscribe({ channel: 'tickers', instId: 'BTC-USDT' })).catch((error) => { + console.error('Subscription failed:', error); + ws.closeAll(); + process.exitCode = 1; +}); + +process.once('SIGINT', () => { + ws.closeAll(); +}); diff --git a/sdks/okx/examples/ws-api.ts b/sdks/okx/examples/ws-api.ts new file mode 100644 index 00000000..0461babf --- /dev/null +++ b/sdks/okx/examples/ws-api.ts @@ -0,0 +1,28 @@ +import { WebsocketAPIClient } from 'okx-api'; +import { credentials } from './authenticated-client.js'; + +const api = new WebsocketAPIClient({ accounts: [credentials] }); + +async function main() { + const clOrdId = process.env.OKX_CLIENT_ORDER_ID; + if (!clOrdId) { + throw new Error('Set OKX_CLIENT_ORDER_ID to the existing BTC-USDT order to cancel.'); + } + const response = await api.cancelOrder({ instId: 'BTC-USDT', clOrdId }); + console.log('Cancellation response:', response); + if (response.code !== '0') { + throw new Error(`Cancellation failed: ${response.code}`); + } + for (const result of response.data) { + if (result.sCode !== '0') { + throw new Error(`Cancellation rejected: ${result.sCode} ${result.sMsg}`); + } + } +} + +main().catch((error) => { + console.error('WS API request failed; reconcile before retrying:', error); + process.exitCode = 1; +}).finally(() => { + api.getWSClient().closeAll(); +}); diff --git a/sdks/okx/sdk.config.mjs b/sdks/okx/sdk.config.mjs new file mode 100644 index 00000000..6b63a5d0 --- /dev/null +++ b/sdks/okx/sdk.config.mjs @@ -0,0 +1,46 @@ +export default { + name: 'OKX', + sourceDirectory: 'okx-api', + packageName: 'okx-api', + entryPoint: 'src/index.ts', + tsconfig: 'tsconfig.json', + compatibilityPage: 'authored', + clients: ['RestClient', 'WebsocketClient', 'WebsocketAPIClient'], + website: 'https://siebly.io/sdk/okx/javascript', + tutorial: 'https://siebly.io/sdk/okx/javascript/tutorial', + repository: 'https://github.com/sieblyio/okx-api', + referenceHelp: [ + { client: 'RestClient', method: 'getTicker', guide: '/getting-started', title: 'Read a public ticker' }, + { client: 'RestClient', method: 'getOrderBook', guide: '/guides/market-data', title: 'Read an order book' }, + { client: 'RestClient', method: 'getBalance', guide: '/guides/authentication', title: 'Read trading account balances' }, + ...['submitOrder', 'amendOrder', 'cancelOrder', 'getOrderDetails', 'getOrderList', 'getOrderHistory', 'getFills'].map(method => ({ + client: 'RestClient', method, guide: '/guides/orders', title: 'Order management', + })), + { client: 'WebsocketClient', method: 'subscribe', guide: '/guides/websockets', title: 'Subscribe to public market data' }, + { client: 'WebsocketClient', method: 'closeAll', guide: '/guides/production#shut-down-cleanly', title: 'Shut down cleanly' }, + { client: 'WebsocketAPIClient', method: 'submitNewOrder', guide: '/guides/websocket-api', title: 'WebSocket API requests' }, + ], + guides: [ + { text: 'Order management', link: '/guides/orders' }, + { text: 'Private account updates (WebSocket)', link: '/guides/private-streams' }, + { text: 'WebSocket API requests', link: '/guides/websocket-api' }, + ], + referenceExclude: [ + 'src/types/rest/request/affiliate.ts', + 'src/types/rest/response/private-affiliate.ts', + ], + referenceExcludeMembers: [ + 'ASSET_BILL_TYPE.AFFILIATE_COMMISSION', + 'ASSET_BILL_TYPE.REFERRAL_REFERRED_CUSTOMER', + 'ASSET_BILL_TYPE.REFERRAL_REWARD', + 'ASSET_BILL_TYPE.REFERRAL_WAS_REFERRED', + 'ASSET_BILL_TYPE.SUB_AFFILIATE_COMMISSION', + 'RestClient.getAffiliatePerformanceSummary', + 'RestClient.getInviteeDetail', + 'RestClient.getAffiliateInviteeList', + 'RestClient.getAffiliateLinkList', + 'RestClient.getAffiliateCoInviterLinkList', + 'RestClient.getAffiliateSubAffiliateList', + 'RestClient.getAffiliateRebateInfo', + ], +}; diff --git a/shared/environments/direct/exchanges.mjs b/shared/environments/direct/exchanges.mjs index e7af8edf..f549eee7 100644 --- a/shared/environments/direct/exchanges.mjs +++ b/shared/environments/direct/exchanges.mjs @@ -1,4 +1,22 @@ export const exchanges = { + okx: { + restClient: 'RestClient', + symbol: 'BTC-USDT', + continuous: true, + cliSubscription: "Promise.all(ws.subscribe({ channel: 'tickers', instId: 'BTC-USDT' })).catch((error) => {\n console.error('WebSocket subscription failed:', error);\n})", + request: `const client = new RestClient(); +const tickers = await client.getTicker({ instId: 'BTC-USDT' }); +const price = tickers[0]?.last;`, + event: 'update', + update: `if (message.arg?.channel !== 'tickers') { + console.log('WebSocket update:', message); + return; +} + +const price = message.data?.[0]?.last;`, + websocket: 'new WebsocketClient()', + subscription: "Promise.all(ws.subscribe({ channel: 'tickers', instId: 'BTC-USDT' }))", + }, coinbase: { restClient: 'CBAdvancedTradeClient', symbol: 'BTC-USD', diff --git a/starters/README.md b/starters/README.md index 0c62531e..063dd811 100644 --- a/starters/README.md +++ b/starters/README.md @@ -57,6 +57,17 @@ Each folder is a complete project. Open its README for prerequisites and run com - [Bun](./coinbase/bun/) - [Deno](./coinbase/deno/) +## OKX + +- [Node.js / JavaScript](./okx/node/) +- [Node.js / TypeScript](./okx/typescript/) +- [React](./okx/react/) +- [Vite (plain JavaScript)](./okx/vite/) +- [Next.js](./okx/next/) +- [React Native / Expo](./okx/expo/) +- [Bun](./okx/bun/) +- [Deno](./okx/deno/) + ## Maintaining these examples Edit the maintained sources in `shared/environments/`, then run `npm run starters:generate` from the repository root. Run `npm run check:starters` to check without writing files. Regeneration updates known generated files and never deletes extra files or local build output. The check reports unexpected source files and stale folders for review. diff --git a/starters/okx/bun/.gitignore b/starters/okx/bun/.gitignore new file mode 100644 index 00000000..314171b4 --- /dev/null +++ b/starters/okx/bun/.gitignore @@ -0,0 +1,23 @@ +node_modules/ +dist/ +.next/ +.expo/ +.vite/ +.turbo/ +.metro-cache/ +.metro-file-map/ +native-dist-ios/ +native-dist-android/ +.git/ +package-lock.json +npm-shrinkwrap.json +bun.lock +bun.lockb +deno.lock +yarn.lock +pnpm-lock.yaml +.npmrc +next-env.d.ts +.env +.env.* +.DS_Store diff --git a/starters/okx/bun/README.md b/starters/okx/bun/README.md new file mode 100644 index 00000000..5a262a05 --- /dev/null +++ b/starters/okx/bun/README.md @@ -0,0 +1,24 @@ +# OKX: Bun + +A runnable public-price example using `okx-api`. No API key is required. + +Install Bun first. + +## Run + +From this folder: + +```sh +bun install +bun run start +``` + +To stream WebSocket prices until Ctrl+C: + +```sh +bun run stream +``` + +Keep authenticated calls on your server. Browser projects must retain the Vite configuration and omit Node-only `keepAlive`; browser REST access depends on OKX CORS policy. See the guide for runtime setup limits. + +[Follow the complete step-by-step guide](https://sieblyio.github.io/docs/okx/environments/bun.html). diff --git a/starters/okx/bun/package.json b/starters/okx/bun/package.json new file mode 100644 index 00000000..fdd1d72a --- /dev/null +++ b/starters/okx/bun/package.json @@ -0,0 +1,12 @@ +{ + "name": "okx-bun-quickstart", + "private": true, + "type": "module", + "scripts": { + "start": "bun price.js", + "stream": "bun stream.js" + }, + "dependencies": { + "okx-api": "latest" + } +} diff --git a/starters/okx/bun/price.js b/starters/okx/bun/price.js new file mode 100644 index 00000000..7d0ac59b --- /dev/null +++ b/starters/okx/bun/price.js @@ -0,0 +1,11 @@ +import { RestClient } from 'okx-api'; + +const client = new RestClient(); +const tickers = await client.getTicker({ instId: 'BTC-USDT' }); +const price = tickers[0]?.last; + +if (!price) { + throw new Error('No BTC-USDT price was returned.'); +} + +console.log('BTC-USDT REST price:', price); diff --git a/starters/okx/bun/stream.js b/starters/okx/bun/stream.js new file mode 100644 index 00000000..ee9ee3bf --- /dev/null +++ b/starters/okx/bun/stream.js @@ -0,0 +1,42 @@ +import { WebsocketClient } from 'okx-api'; +import process from 'node:process'; + +const ws = new WebsocketClient(); + +ws.on('update', (message) => { + if (message.arg?.channel !== 'tickers') { + console.log('WebSocket update:', message); + return; + } + + const price = message.data?.[0]?.last; + + if (price) { + console.log('BTC-USDT WebSocket price:', String(price)); + } +}); + +ws.on('response', (message) => { + console.log('WebSocket response:', message); +}); +ws.on('exception', (error) => { + console.error('WebSocket exception:', error); +}); +ws.on('reconnect', (event) => { + console.log('WebSocket reconnecting:', event); +}); +ws.on('reconnected', (event) => { + console.log('WebSocket reconnected:', event); +}); + +function stop() { + ws.closeAll(); + console.log('WebSocket closed.'); +} + +process.once('SIGINT', stop); +process.once('SIGTERM', stop); + +Promise.all(ws.subscribe({ channel: 'tickers', instId: 'BTC-USDT' })).catch((error) => { + console.error('WebSocket subscription failed:', error); +}); diff --git a/starters/okx/deno/.gitignore b/starters/okx/deno/.gitignore new file mode 100644 index 00000000..314171b4 --- /dev/null +++ b/starters/okx/deno/.gitignore @@ -0,0 +1,23 @@ +node_modules/ +dist/ +.next/ +.expo/ +.vite/ +.turbo/ +.metro-cache/ +.metro-file-map/ +native-dist-ios/ +native-dist-android/ +.git/ +package-lock.json +npm-shrinkwrap.json +bun.lock +bun.lockb +deno.lock +yarn.lock +pnpm-lock.yaml +.npmrc +next-env.d.ts +.env +.env.* +.DS_Store diff --git a/starters/okx/deno/README.md b/starters/okx/deno/README.md new file mode 100644 index 00000000..61f8116f --- /dev/null +++ b/starters/okx/deno/README.md @@ -0,0 +1,24 @@ +# OKX: Deno + +A runnable public-price example using `okx-api`. No API key is required. + +Install Deno first. + +## Run + +From this folder: + +```sh +deno install +deno task start +``` + +To stream WebSocket prices until Ctrl+C: + +```sh +deno task stream +``` + +Keep authenticated calls on your server. Browser projects must retain the Vite configuration and omit Node-only `keepAlive`; browser REST access depends on OKX CORS policy. See the guide for runtime setup limits. + +[Follow the complete step-by-step guide](https://sieblyio.github.io/docs/okx/environments/deno.html). diff --git a/starters/okx/deno/deno.json b/starters/okx/deno/deno.json new file mode 100644 index 00000000..e107f01f --- /dev/null +++ b/starters/okx/deno/deno.json @@ -0,0 +1,7 @@ +{ + "nodeModulesDir": "auto", + "tasks": { + "start": "deno run --allow-net --allow-env --allow-read price.js", + "stream": "deno run --allow-net --allow-env --allow-read stream.js" + } +} diff --git a/starters/okx/deno/package.json b/starters/okx/deno/package.json new file mode 100644 index 00000000..5d23cf18 --- /dev/null +++ b/starters/okx/deno/package.json @@ -0,0 +1,8 @@ +{ + "name": "okx-deno-quickstart", + "private": true, + "type": "module", + "dependencies": { + "okx-api": "latest" + } +} diff --git a/starters/okx/deno/price.js b/starters/okx/deno/price.js new file mode 100644 index 00000000..7d0ac59b --- /dev/null +++ b/starters/okx/deno/price.js @@ -0,0 +1,11 @@ +import { RestClient } from 'okx-api'; + +const client = new RestClient(); +const tickers = await client.getTicker({ instId: 'BTC-USDT' }); +const price = tickers[0]?.last; + +if (!price) { + throw new Error('No BTC-USDT price was returned.'); +} + +console.log('BTC-USDT REST price:', price); diff --git a/starters/okx/deno/stream.js b/starters/okx/deno/stream.js new file mode 100644 index 00000000..ee9ee3bf --- /dev/null +++ b/starters/okx/deno/stream.js @@ -0,0 +1,42 @@ +import { WebsocketClient } from 'okx-api'; +import process from 'node:process'; + +const ws = new WebsocketClient(); + +ws.on('update', (message) => { + if (message.arg?.channel !== 'tickers') { + console.log('WebSocket update:', message); + return; + } + + const price = message.data?.[0]?.last; + + if (price) { + console.log('BTC-USDT WebSocket price:', String(price)); + } +}); + +ws.on('response', (message) => { + console.log('WebSocket response:', message); +}); +ws.on('exception', (error) => { + console.error('WebSocket exception:', error); +}); +ws.on('reconnect', (event) => { + console.log('WebSocket reconnecting:', event); +}); +ws.on('reconnected', (event) => { + console.log('WebSocket reconnected:', event); +}); + +function stop() { + ws.closeAll(); + console.log('WebSocket closed.'); +} + +process.once('SIGINT', stop); +process.once('SIGTERM', stop); + +Promise.all(ws.subscribe({ channel: 'tickers', instId: 'BTC-USDT' })).catch((error) => { + console.error('WebSocket subscription failed:', error); +}); diff --git a/starters/okx/expo/.gitignore b/starters/okx/expo/.gitignore new file mode 100644 index 00000000..314171b4 --- /dev/null +++ b/starters/okx/expo/.gitignore @@ -0,0 +1,23 @@ +node_modules/ +dist/ +.next/ +.expo/ +.vite/ +.turbo/ +.metro-cache/ +.metro-file-map/ +native-dist-ios/ +native-dist-android/ +.git/ +package-lock.json +npm-shrinkwrap.json +bun.lock +bun.lockb +deno.lock +yarn.lock +pnpm-lock.yaml +.npmrc +next-env.d.ts +.env +.env.* +.DS_Store diff --git a/starters/okx/expo/App.js b/starters/okx/expo/App.js new file mode 100644 index 00000000..59a95959 --- /dev/null +++ b/starters/okx/expo/App.js @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react'; +import { Button, SafeAreaView, Text, View } from 'react-native'; +import { RestClient, WebsocketClient } from 'okx-api'; + +function Prices() { + const [restPrice, setRestPrice] = useState('Loading'); + const [livePrice, setLivePrice] = useState('Connecting'); + const [error, setError] = useState(''); + + useEffect(() => { + let mounted = true; + const ws = new WebsocketClient(); + + function showError(error) { + console.error(error); + if (mounted) { + setError(error instanceof Error ? error.message : String(error)); + } + } + + async function loadPrice() { + const client = new RestClient(); + const tickers = await client.getTicker({ instId: 'BTC-USDT' }); + const price = tickers[0]?.last; + + if (!price) { + throw new Error('No price was returned.'); + } + + if (mounted) { + setRestPrice(String(price)); + } + } + + loadPrice().catch(showError); + + const timeout = setTimeout(() => { + showError(new Error('No BTC-USDT update within 30 seconds.')); + }, 30_000); + + ws.on('update', (message) => { + if (message.arg?.channel !== 'tickers') { + console.log('WebSocket update:', message); + return; + } + + const price = message.data?.[0]?.last; + + if (mounted && price) { + clearTimeout(timeout); + setLivePrice(String(price)); + } + }); + ws.on('exception', showError); + + // React can clean up an effect before its subscription starts. + Promise.resolve().then(async () => { + if (!mounted) { + return; + } + + try { + await Promise.all(ws.subscribe({ channel: 'tickers', instId: 'BTC-USDT' })); + } finally { + // Close again if pending setup completed after unmount. + if (!mounted) { + ws.closeAll(); + } + } + }).catch(showError); + + return () => { + mounted = false; + clearTimeout(timeout); + ws.closeAll(); + }; + }, []); + + return ( +BTC-USDT REST price:
+BTC-USDT WebSocket price:
+{error}
+Stream stopped. WebSocket closed.
} + +REST:
+WebSocket:
+ + + + + diff --git a/starters/okx/vite/main.js b/starters/okx/vite/main.js new file mode 100644 index 00000000..c945b1f6 --- /dev/null +++ b/starters/okx/vite/main.js @@ -0,0 +1,114 @@ +import { RestClient, WebsocketClient } from 'okx-api'; + +const restOutput = document.querySelector('#rest'); +const liveOutput = document.querySelector('#live'); +const errorOutput = document.querySelector('#error'); +const stopButton = document.querySelector('#stop'); +let stopStream; +let userStopped = false; + +function startStream() { + const ws = new WebsocketClient(); + let stopped = false; + liveOutput.textContent = 'Connecting'; + errorOutput.textContent = ''; + + function showError(error) { + console.error(error); + if (!stopped) { + errorOutput.textContent = error instanceof Error ? error.message : String(error); + } + } + + async function loadPrice() { + const client = new RestClient(); + const tickers = await client.getTicker({ instId: 'BTC-USDT' }); + const price = tickers[0]?.last; + + if (!stopped) { + restOutput.textContent = String(price); + } + } + + loadPrice().catch(showError); + + const timeout = setTimeout(() => { + showError(new Error('No BTC-USDT update within 30 seconds.')); + }, 30_000); + + ws.on('update', (message) => { + if (message.arg?.channel !== 'tickers') { + console.log('WebSocket update:', message); + return; + } + + const price = message.data?.[0]?.last; + + if (!stopped && price) { + clearTimeout(timeout); + liveOutput.textContent = String(price); + } + }); + ws.on('exception', showError); + + Promise.resolve().then(async () => { + if (stopped) { + return; + } + + try { + await Promise.all(ws.subscribe({ channel: 'tickers', instId: 'BTC-USDT' })); + } finally { + if (stopped) { + ws.closeAll(); + } + } + }).catch(showError); + + return function stop() { + if (stopped) { + return; + } + + stopped = true; + clearTimeout(timeout); + ws.closeAll(); + liveOutput.textContent = 'Stopped'; + }; +} + +function start() { + if (!stopStream && !userStopped) { + stopStream = startStream(); + } +} + +function stop() { + stopStream?.(); + stopStream = undefined; +} + +function stopByUser() { + userStopped = true; + stop(); +} + +function restore(event) { + if (event.persisted) { + start(); + } +} + +stopButton.addEventListener('click', stopByUser); +window.addEventListener('pagehide', stop); +window.addEventListener('pageshow', restore); +start(); + +if (import.meta.hot) { + import.meta.hot.dispose(() => { + stopButton.removeEventListener('click', stopByUser); + window.removeEventListener('pagehide', stop); + window.removeEventListener('pageshow', restore); + stop(); + }); +} diff --git a/starters/okx/vite/package.json b/starters/okx/vite/package.json new file mode 100644 index 00000000..8272b9e9 --- /dev/null +++ b/starters/okx/vite/package.json @@ -0,0 +1,17 @@ +{ + "name": "okx-vite-quickstart", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vite build", + "preview": "vite preview --host 127.0.0.1" + }, + "dependencies": { + "okx-api": "latest", + "events": "3.3.0" + }, + "devDependencies": { + "vite": "5.4.21" + } +} diff --git a/starters/okx/vite/vite.config.js b/starters/okx/vite/vite.config.js new file mode 100644 index 00000000..3ddd40a1 --- /dev/null +++ b/starters/okx/vite/vite.config.js @@ -0,0 +1,37 @@ +import { fileURLToPath } from 'node:url'; + +const browserAgent = '\0okx-browser-agent'; +const sdkRoot = fileURLToPath(new URL('./node_modules/okx-api/', import.meta.url)); + +export default { + // Keep the same SDK transform during development and production builds. + optimizeDeps: { + exclude: ['okx-api'], + include: ['events', 'okx-api > isomorphic-ws'], + }, + resolve: { + alias: { + events: fileURLToPath(new URL('./node_modules/events/events.js', import.meta.url)), + }, + }, + plugins: [{ + name: 'okx-browser-transport', + enforce: 'pre', + resolveId(source, importer) { + if (source === 'https' && importer?.startsWith(sdkRoot)) { + return browserAgent; + } + }, + load(id) { + if (id === browserAgent) { + // Browsers manage connections themselves. Reject an unsupported option + // rather than presenting a Node Agent as a working browser transport. + return `export class Agent { + constructor() { + throw new Error('keepAlive and HTTPS agents are Node.js-only. Omit keepAlive in a browser.'); + } +}`; + } + }, + }], +};