diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5a9dab46..123ce1fb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,6 +12,7 @@ updates: - dependency-name: bybit-api - dependency-name: binance - dependency-name: '@siebly/kraken-api' + - dependency-name: coinbase-api groups: sdk-releases: patterns: @@ -19,9 +20,11 @@ updates: - bybit-api - binance - '@siebly/kraken-api' + - coinbase-api cooldown: exclude: - '@siebly/hyperliquid-api' - bybit-api - binance - '@siebly/kraken-api' + - coinbase-api diff --git a/compatibility/adapters/coinbase.mjs b/compatibility/adapters/coinbase.mjs new file mode 100644 index 00000000..a6a18088 --- /dev/null +++ b/compatibility/adapters/coinbase.mjs @@ -0,0 +1,25 @@ +export const operations = { + rest: 'CBAdvancedTradeClient.getPublicProduct()', + websocket: 'WebsocketClient.subscribe()', +}; +export function client(sdk) { + return new sdk.CBAdvancedTradeClient({}, { timeout: 15000 }); +} +export async function read(client) { + const ticker = await client.getPublicProduct({ product_id: 'BTC-USD' }); + return ticker.price; +} +export function stream(sdk) { + return new sdk.WebsocketClient(); +} +export const event = 'update'; +export function value(message) { + return message.channel === 'ticker' ? message.events?.flatMap(event => event.tickers || []).find(ticker => ticker.product_id === 'BTC-USD')?.price : undefined; +} +export async function subscribe(ws) { + ws.subscribe({ topic: 'ticker', payload: { product_ids: ['BTC-USD'] } }, 'advTradeMarketData'); +} +export const typeExample = `import { CBAdvancedTradeClient, WebsocketClient } from 'coinbase-api'; +new CBAdvancedTradeClient().getPublicProduct({ product_id: 'BTC-USD' }); +new WebsocketClient().closeAll(); +`; diff --git a/compatibility/browser-config/coinbase.mjs b/compatibility/browser-config/coinbase.mjs new file mode 100644 index 00000000..1516aac1 --- /dev/null +++ b/compatibility/browser-config/coinbase.mjs @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url'; + +// Public market data only. Coinbase's JWT helper imports Node crypto eagerly. +// Keep credentialed calls on a Node.js server rather than bundling private keys. +const publicAuth = '\0coinbase-public-auth'; +const sdkRoot = fileURLToPath(new URL('./node_modules/coinbase-api/', import.meta.url)); +export default { + // Keep SDK modules in Vite's transform pipeline so the JWT replacement also + // applies during development. Prebundle its CommonJS browser dependencies. + optimizeDeps: { + exclude: ['coinbase-api'], + include: ['events', 'coinbase-api > isomorphic-ws'], + }, + resolve: { + alias: { + events: fileURLToPath(new URL('./node_modules/events/events.js', import.meta.url)), + }, + }, + plugins: [{ + name: 'coinbase-public-market-data', + enforce: 'pre', + resolveId(source, importer) { + if (source.endsWith('/jwtNode.js') && importer?.startsWith(sdkRoot)) { + return publicAuth; + } + }, + load(id) { + if (id === publicAuth) { + return `export function signJWT() { throw new Error('JWT signing is server-only in this public market-data starter.'); } +export const signWSJWT = signJWT;`; + } + }, + }], +}; diff --git a/compatibility/browser.mjs b/compatibility/browser.mjs index 6601d84a..1304e912 100644 --- a/compatibility/browser.mjs +++ b/compatibility/browser.mjs @@ -85,7 +85,7 @@ document.getElementById('status').textContent=response.ok?'Complete. Results sav await writeFile(path.join(evidence,`${evidenceName}.json`),JSON.stringify({report,warnings,profile},null,2)); const passed=completeBrowserResult(report.checks); const version=/Chrome\/([\d.]+)/.exec(report.userAgent)?.[1]; - const description=profile==='vite'?'The default Vite consumer':profile==='configured'?'The Vite consumer with an events polyfill'+(id==='binance'?' and the SDK browser-signing alias':''):'The standalone browser bundle shipped in this npm package'; + const description=profile==='vite'?'The default Vite consumer':profile==='configured'?'The Vite consumer with an events polyfill'+(id==='binance'?' and the SDK browser-signing alias':id==='coinbase'?' and public-only JWT rejection':''):'The standalone browser bundle shipped in this npm package'; return {status:passed?'passed':'failed',runtimeVersion:version?`Chromium ${version}`:report.userAgent,checks:[{id:'bundle',status:'passed'},...report.checks],reason:passed?`${description} passed execution checks in the browser shown here, against the live exchange REST and WebSocket APIs.`:`${description} failed browser execution checks. See the observed failures below.`,userAgent:report.userAgent,secureContext:report.secureContext,warnings,profile}; }finally{ clearTimeout(timer); diff --git a/compatibility/native-config/coinbase-public-auth.cjs b/compatibility/native-config/coinbase-public-auth.cjs new file mode 100644 index 00000000..0270b291 --- /dev/null +++ b/compatibility/native-config/coinbase-public-auth.cjs @@ -0,0 +1,5 @@ +// This consumer is for public data only. Keep API credentials on your server. +function signJWT() { + throw new Error('JWT signing is server-only in this public market-data starter.'); +} +module.exports = { signJWT, signWSJWT: signJWT }; diff --git a/compatibility/native-config/coinbase.metro.cjs b/compatibility/native-config/coinbase.metro.cjs new file mode 100644 index 00000000..4fe24cd0 --- /dev/null +++ b/compatibility/native-config/coinbase.metro.cjs @@ -0,0 +1,26 @@ +const path = require('node:path'); + +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) => { + // Replace only Coinbase's Node-only JWT implementation for public consumers. + if (moduleName.endsWith('/jwtNode.js') && + context.originModulePath.startsWith(path.join(projectRoot, 'node_modules/coinbase-api') + path.sep)) { + return { type: 'sourceFile', filePath: path.join(projectRoot, 'coinbase-public-auth.cjs') }; + } + 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 b5ada08b..4cbd4564 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' }[id]; + const filename = { bybit: 'bybit.metro.cjs', binance: 'metro.cjs', kraken: 'kraken.metro.cjs', coinbase: 'coinbase.metro.cjs' }[id]; if (!filename) { throw new Error(`No Metro polyfill recipe for ${id}.`); } diff --git a/compatibility/native.mjs b/compatibility/native.mjs index 5f789e9f..2ed53bc5 100644 --- a/compatibility/native.mjs +++ b/compatibility/native.mjs @@ -38,6 +38,7 @@ export async function nativeCheck({id,consumer,evidence,env,packageName,packageV const dependencies=JSON.parse(await readFile(path.join(root,'compatibility/native-config/dependencies.json'))); await npm(['install','--ignore-scripts','--save-exact',...Object.entries(dependencies).map(([name,version])=>`${name}@${version}`)],{cwd:consumer,env,timeout:120000,log:path.join(evidence,`${evidenceId}-polyfills-install.log`)}); await writeFile(path.join(consumer,'siebly-sdk.metro.cjs'),await nativeMetroConfig(id)); + if(id==='coinbase') await writeFile(path.join(consumer,'coinbase-public-auth.cjs'),await readFile(path.join(root,'compatibility/native-config/coinbase-public-auth.cjs'))); result.consumerDependencies=dependencies; } const pkg=JSON.parse(await readFile(path.join(consumer,'package.json')));pkg.main='index.js'; diff --git a/releases/package.json b/releases/package.json index 4726aa2c..c777c3e1 100644 --- a/releases/package.json +++ b/releases/package.json @@ -5,6 +5,7 @@ "@siebly/hyperliquid-api": "1.0.3", "bybit-api": "4.8.5", "binance": "3.6.12", - "@siebly/kraken-api": "1.1.10" + "@siebly/kraken-api": "1.1.10", + "coinbase-api": "1.3.1" } } diff --git a/scripts/check-public.mjs b/scripts/check-public.mjs index e497abae..a4d766ff 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)\/starters\/(node|typescript|react|bun|deno|vite|next|expo)\.tar\.gz$/.exec(parts.join('/')); + const match = /^(hyperliquid|bybit|binance|kraken|coinbase)\/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 4af6fc36..0cf5b4d0 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']; +export const ids = ['hyperliquid', 'bybit', 'binance', 'kraken', 'coinbase']; 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 bf726278..b147c052 100644 --- a/scripts/environment-guides.mjs +++ b/scripts/environment-guides.mjs @@ -10,6 +10,7 @@ export function codeBlock(filename, content) { const command = value => `\`\`\`sh\n${value}\n\`\`\``; const purposes = { + 'coinbase-public-auth.cjs': 'Reject JWT signing in this public-data consumer. Keep credentialed API calls on a Node.js server.', 'package.json': 'Declare the SDK dependency and the commands used by this project.', 'tsconfig.json': 'Enable strict type checking and compile the TypeScript programs into `dist/`.', 'deno.json': 'Tell Deno to install npm dependencies locally and define the price and stream tasks.', @@ -90,10 +91,17 @@ export function guideFields(id, sdk, environment, files) { ? ['package.json', 'install', 'vite.config.js', 'main.js', 'index.html'] : environment === 'next' ? ['package.json', 'install', 'app/layout.js', 'app/api/price/route.js', 'app/page.js'] - : ['package.json', 'app.json', 'install', 'siebly-sdk.metro.cjs', 'metro.config.cjs', 'App.js', 'index.js']; + : ['package.json', 'app.json', 'install', 'coinbase-public-auth.cjs', 'siebly-sdk.metro.cjs', 'metro.config.cjs', 'App.js', 'index.js']; app = orderedSteps(names, files); } + if (id === 'coinbase') { + 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.') + .replace('to receive one WebSocket price and close the connection.', 'to stream WebSocket prices until Ctrl+C.'); + } + return { PROJECT_SETUP: setup, CONFIG_FILES: config, diff --git a/scripts/environments.mjs b/scripts/environments.mjs index 9c962f27..38ff4c9d 100644 --- a/scripts/environments.mjs +++ b/scripts/environments.mjs @@ -154,6 +154,9 @@ export async function environmentFiles(id, sdk, environment) { if (id !== 'hyperliquid') { Object.assign(pkg.dependencies, JSON.parse(await source('compatibility/native-config/dependencies.json'))); files.set('siebly-sdk.metro.cjs', await nativeMetroConfig(id)); + if (id === 'coinbase') { + files.set('coinbase-public-auth.cjs', await source('compatibility/native-config/coinbase-public-auth.cjs')); + } files.set('metro.config.cjs', "const { getDefaultConfig } = require('expo/metro-config');\nconst configureSdkMetro = require('./siebly-sdk.metro.cjs');\n\nconst config = getDefaultConfig(__dirname);\n\nmodule.exports = configureSdkMetro(config, __dirname);\n"); } } @@ -237,6 +240,21 @@ export async function generateEnvironments(ctx) { if (!(key in substitutions)) throw new Error(`Missing environment template field: ${key}`); return substitutions[key]; }); + if (ctx.id === 'coinbase') { + 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,'); + 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.'; + rendered = rendered + .replace('## Create the project', note + '\n\n## Create the project') + .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 === '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 41d15323..43854106 100644 --- a/scripts/generate.mjs +++ b/scripts/generate.mjs @@ -1,6 +1,6 @@ import { readFile, writeFile, mkdir, readdir, cp, rm } from 'node:fs/promises'; import path from 'node:path'; -import { Application, Comment, CommentTag, ReflectionKind } from 'typedoc'; +import { Application, Comment, CommentTag, Converter, ReflectionKind } from 'typedoc'; import { context, root, catalogue } from './context.mjs'; import { compilerConfig } from './compiler.mjs'; import { generateCompatibility } from '../compatibility/report.mjs'; @@ -12,9 +12,18 @@ await rm(projectRoot, { recursive: true, force: true }); await mkdir(projectRoot, { recursive: true }); await cp(path.join(ctx.content, 'docs'), ctx.docs, { recursive: true }); await copySharedContent(root, ctx.docs); +if (sdk.compatibilityPage === 'authored') { + // Reuse the shared setup table while pointing to this SDK's setup-only page. + const setupFragment = path.join(ctx.docs, '.vitepress/shared/getting-started/environments.md'); + const content = await readFile(setupFragment, 'utf8'); + await writeFile(setupFragment, content.replace( + 'See [Runtime compatibility](/compatibility) for tested versions, required configuration and check scope.', + 'See [Runtime setup](/compatibility) for project recipes and required configuration.', + )); +} await cp(path.join(ctx.content, 'examples'), path.join(projectRoot, 'examples'), { recursive: true }); await cp(path.join(root, 'shared/public'), path.join(ctx.docs, 'public'), { recursive: true }); -await generateCompatibility(ctx, root); +if (sdk.compatibilityPage !== 'authored') await generateCompatibility(ctx, root); await generateEnvironments(ctx); const pkg = JSON.parse(await readFile(path.join(sdkRoot, 'package.json'), 'utf8')); @@ -50,10 +59,24 @@ const app = await Application.bootstrapWithPlugins({ typeAliasPropertiesFormat: 'table', validation: { notExported: true, invalidLink: true, notDocumented: false }, }); +// Filter the documentation model before TypeDoc builds its groups and categories. +app.converter.on(Converter.EVENT_RESOLVE_BEGIN, ({ project }) => { + for (const reflection of Object.values(project.reflections)) { + if (reflection.kind === ReflectionKind.Property && sdk.referenceExcludeProperties?.includes(reflection.name)) { + project.removeReflection(reflection); + } + } +}); const project = await app.convert(); if (!project || app.logger.hasErrors()) throw new Error('TypeDoc conversion failed'); // Enrich the documentation model, never the SDK or generated Markdown. +for (const [name, description] of Object.entries(sdk.referenceDescriptions || {})) { + const reflection = project.getChildByName(name.split('.')); + if (!reflection) throw new Error(`Missing documented export: ${name}`); + reflection.comment = new Comment([{ kind: 'text', text: description }]); +} + // Exact member checks make renamed or removed methods fail the docs build. for (const help of sdk.referenceHelp || []) { const client = project.children.find((child) => child.name === help.client); diff --git a/scripts/release-manifest.mjs b/scripts/release-manifest.mjs index 975f191c..f99df7db 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']; +export const sdkPackages = ['@siebly/hyperliquid-api', 'bybit-api', 'binance', '@siebly/kraken-api', 'coinbase-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 b2bba8c4..e2f9b02a 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' } }); +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 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 53605213..fd28e03b 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' } }); +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' } }); 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']); + assert.deepEqual(Object.keys(await readReleaseVersions()).sort(), ['@siebly/hyperliquid-api', '@siebly/kraken-api', 'binance', 'bybit-api', 'coinbase-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/shared-content.test.mjs b/scripts/shared-content.test.mjs index a7b3fb47..786f0eb7 100644 --- a/scripts/shared-content.test.mjs +++ b/scripts/shared-content.test.mjs @@ -29,3 +29,26 @@ test('shared includes resolve privately and fail on missing or circular dependen await rm(work, { recursive: true, force: true }); } }); + +test('Coinbase navigation keeps references scoped and its runtime setups expanded', async () => { + const { default: siteConfig } = await import('../shared/config.mjs'); + const { catalogue } = await import('./context.mjs'); + const sdks = await catalogue(); + for (const sdk of sdks) { + const config = siteConfig({ sdk, catalogue: sdks, siteUrl: `http://localhost/${sdk.id}/`, originUrl: 'http://localhost/', indexable: false }, '.'); + const sidebar = config.themeConfig.sidebar; + const groups = Array.isArray(sidebar) ? sidebar : sidebar['/']; + if (!sdk.productGroups) continue; + assert.equal(groups.find(group => group.text.startsWith('Frameworks and runtimes')).collapsed, undefined); + const defaultReferences = groups.find(group => group.text === 'API reference').items; + for (const product of sdk.productGroups) { + const productGroups = sidebar[`/products/${product.slug}`]; + const references = productGroups.find(group => group.text === 'API reference').items; + for (const client of product.clients) { + assert(!defaultReferences.some(item => item.link === `/api/classes/${client}`)); + assert(references.some(item => item.link === `/api/classes/${client}`)); + assert.equal(sidebar[`/api/classes/${client}`], productGroups); + } + } + } +}); diff --git a/scripts/starters.mjs b/scripts/starters.mjs index d0da1bf7..ce9b386f 100644 --- a/scripts/starters.mjs +++ b/scripts/starters.mjs @@ -18,11 +18,16 @@ 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 ? `\nTo receive one WebSocket price and close the connection:\n\n\`\`\`sh\n${stream}\n\`\`\`\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 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' : ''; - 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}\n[Follow the complete step-by-step guide](https://sieblyio.github.io/docs/${sdk.id}/environments/${environment}.html).\n`; + const coinbaseNote = sdk.id === 'coinbase' + ? '\nThese examples use Advanced Trade public market data. Keep credentialed calls on your Node.js server.\n' + + (['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`; } export async function starterFiles(id, sdk, environment) { diff --git a/sdks/coinbase/docs/clients.md b/sdks/coinbase/docs/clients.md new file mode 100644 index 00000000..25f37acc --- /dev/null +++ b/sdks/coinbase/docs/clients.md @@ -0,0 +1,29 @@ +--- +title: "Coinbase Advanced Trade SDK clients" +description: "Choose REST requests or streaming subscriptions for Advanced Trade." +--- + +# Advanced Trade clients + +Use these two clients for Advanced Trade brokerage APIs. Looking for Global derivatives, Exchange, INTX, Prime, App or Commerce? [Choose your Coinbase API](/products). + + +| Client | Purpose | +| --- | --- | +| [`CBAdvancedTradeClient`](/api/classes/CBAdvancedTradeClient) | Advanced Trade REST: market data, accounts, orders and supported derivatives endpoints. | +| [`WebsocketClient`](/api/classes/WebsocketClient) | Advanced Trade market and user subscriptions, selected with `advTradeMarketData` or `advTradeUserData`. | + + +## REST requests + +<<< @/../examples/first-call.ts + +Methods return response bodies directly, not Axios response wrappers. Public market-data methods have `Public` in their name. Account and order methods require [Advanced Trade credentials](/guides/authentication). + +## WebSocket subscriptions + +Register listeners before calling `subscribe()`. Pass the correct Advanced Trade `WS_KEY_MAP` value and receive parsed data on `update`. See [WebSocket subscriptions](/guides/websockets) and [private account updates](/guides/private-streams). + +`WebsocketClient` also supports other Coinbase products, but their channels and credentials belong to their own product guides. `WebsocketAPIClient` is for [Global derivatives](/products/global), not Advanced Trade spot order entry. + +[Advanced Trade reference](/reference) · [Choose another API](/products) diff --git a/sdks/coinbase/docs/compatibility.md b/sdks/coinbase/docs/compatibility.md new file mode 100644 index 00000000..12b91ae5 --- /dev/null +++ b/sdks/coinbase/docs/compatibility.md @@ -0,0 +1,34 @@ +--- +title: "Coinbase SDK runtime setup" +description: "Choose a Node.js, browser, framework or mobile project for Coinbase Advanced Trade." +--- + +# Runtime setup + +Choose a project for your application. Each guide starts from an empty directory and includes the files, installation commands and a public Advanced Trade price example. + +## Server-side JavaScript and TypeScript + +- [Node.js / JavaScript](/environments/node): ES modules and CommonJS. +- [Node.js / TypeScript](/environments/typescript): a typed project with build and run commands. +- [Next.js](/environments/next): a Node.js Route Handler that keeps SDK calls on the server. +- [Bun](/environments/bun): npm dependencies and Bun scripts. +- [Deno](/environments/deno): npm dependencies and explicit runtime permissions. + +Keep account credentials and authenticated requests on your server. The [authentication guide](/guides/authentication) explains CDP keys and account reads. + +## Browser applications + +The [React](/environments/react) and [Vite](/environments/vite) projects use public market data. Their configuration supplies EventEmitter and excludes the SDK's Node-only JWT signer. Keep that configuration with the examples. Never put private keys in a browser bundle. + +REST access in a browser depends on Coinbase's CORS policy. If a request is blocked by CORS, call the SDK through a server route instead. A Vite development proxy is not a production server route. + +## React Native and Expo + +The [Expo guide](/environments/expo) includes the Metro resolver, public-auth helper and required polyfills. Keep those files together. The project uses public market data; authenticated operations belong on your server. + +## Streaming lifecycle + +Command-line streaming examples run until Ctrl+C. Browser and mobile examples expose a Stop control and clean up when their view is removed. Let the SDK reconnect after a connection loss; a reopened connection does not by itself restore application account state. + +[WebSocket subscriptions](/guides/websockets) · [Private account updates](/guides/private-streams) · [Production usage](/guides/production) diff --git a/sdks/coinbase/docs/getting-started.md b/sdks/coinbase/docs/getting-started.md new file mode 100644 index 00000000..2fb98d7f --- /dev/null +++ b/sdks/coinbase/docs/getting-started.md @@ -0,0 +1,40 @@ +--- +title: "Coinbase Node.js quickstart with JavaScript" +description: "Install coinbase-api and read a public Advanced Trade Bitcoin price without credentials." +--- + +# Node.js quickstart with JavaScript + +Read the public Advanced Trade Bitcoin/USD product with `CBAdvancedTradeClient`. This example uses plain JavaScript, with no compiler or API credentials. + +## Installation + + + +Use Node.js 24 for this quickstart. For other runtimes and required configuration, see [Runtime setup](/compatibility). + +## Make a request + +Save this as `prices.mjs`: + +<<< @/../examples/first-call.ts{js} + +```sh +node prices.mjs +``` + +## Example output {#verify-the-result} + +```text +BTC-USD price: 65000 +``` + +The price is illustrative. The method returns the product directly, not an Axios response wrapper. Public REST market data can be cached; use [WebSockets](/guides/websockets) for streaming prices. + +## Module formats + +The `.mjs` extension selects ES modules. For CommonJS, save a `.cjs` file and replace the import with `const { CBAdvancedTradeClient } = require('coinbase-api');`. Keep the async `main()` function. TypeScript uses the same imports with the package's included types. + +The other guides use TypeScript. For their small examples, Node.js 24 can run `.ts` files directly. When copying an example that imports `./authenticated-client.js`, compile both TypeScript files with TypeScript first, or change that local import to `./authenticated-client.ts` for Node.js type stripping. + +[Choose a client](/clients) · [Market data](/guides/market-data) · [Troubleshooting](/guides/troubleshooting) diff --git a/sdks/coinbase/docs/guides/authentication.md b/sdks/coinbase/docs/guides/authentication.md new file mode 100644 index 00000000..f80d101a --- /dev/null +++ b/sdks/coinbase/docs/guides/authentication.md @@ -0,0 +1,57 @@ +--- +title: "Coinbase SDK authentication" +description: "Configure CDP keys for Advanced Trade and distinguish other Coinbase credential types." +--- + +# Authentication + +The SDK signs requests for you. Keep secrets on your server, grant only the permissions the application needs, and use IP restrictions where available. Never put private keys into browser bundles, source control or logs. + +## Advanced Trade credentials + +Use a CDP API key name and its private key, not an account password. An ECDSA key is the straightforward choice for Advanced Trade. Preserve the complete PEM, including its header, footer and newlines. + +Set `COINBASE_API_KEY` and `COINBASE_API_SECRET` in your environment, then save this as `authenticated-client.ts`: + +<<< @/../examples/authenticated-client.ts + +The REST client also accepts `cdpApiKey: { name, privateKey }`, matching the exported CDP JSON fields. This is a REST option; `WebsocketClient` expects `apiKey` and `apiSecret` separately. The REST client normalizes literal `\n` sequences in the secret. The WebSocket client does not; the private-stream example normalizes these explicitly before passing the key. + +The signer supports ECDSA PEM and supported base64 Ed25519 formats. That does not establish that every Coinbase product or endpoint accepts both key types. Use the key type required by the API family; do not infer support from successful local signing. + +## Run authenticated examples + +Create a private `.env` file, exclude it from source control, and add your key name and complete private key: + +```dotenv +COINBASE_API_KEY=organizations/your-organization/apiKeys/your-key +COINBASE_API_SECRET="your-full-PEM-private-key-with-\n-line-breaks" +``` + +Use actual credentials in your local file, never in shared code. The values above are placeholders. In the actual local value, retain the complete original PEM header, footer and line breaks, using escaped `\n` where needed. For examples with the local `authenticated-client.js` import, install TypeScript and compile both files into a separate output directory: + +```sh +npm install --save-dev typescript @types/node +npx tsc authenticated-client.ts account-state.ts --module nodenext --target es2022 --outDir compiled +node --env-file=.env compiled/account-state.js +``` + +For the preview, include `preview-order.ts` in that compile command and run `compiled/preview-order.js`. Standalone stream examples can run directly with Node.js 24 and `node --env-file=.env private-stream.ts`. + +## Verify read access + +Save this as `account-state.ts` alongside `authenticated-client.ts`, then use the compile and run commands above: + +<<< @/../examples/account-state.ts + +Check the returned permissions before enabling trading. `getAccounts()` is paginated: follow `cursor` while `has_next` is true when loading all accounts. Never assume one page represents the whole account. + +## Keep product credentials separate + +This setup is for Advanced Trade. For another product's keys and account identifiers, follow its [own authentication guide](/products). An API passphrase is not needed by this client. + +## Troubleshooting signing + +Check the full key name, private-key format, permissions, environment and system clock. Advanced Trade JWT lifetime defaults to 120 seconds; increasing it is not a substitute for correcting clock drift. If credentials are accidentally exposed, revoke and replace them. + +[Configuration](/guides/configuration) · [Private streams](/guides/private-streams) · [API families](/products) diff --git a/sdks/coinbase/docs/guides/configuration.md b/sdks/coinbase/docs/guides/configuration.md new file mode 100644 index 00000000..1581fc22 --- /dev/null +++ b/sdks/coinbase/docs/guides/configuration.md @@ -0,0 +1,38 @@ +--- +title: "Coinbase SDK configuration" +description: "Configure REST timeouts, environments and WebSocket options." +--- + +# Configuration + +## REST options + +The first constructor argument configures SDK behavior and credentials. The second is Axios request configuration: + +<<< @/../examples/configure-rest.ts + +- `keepAlive` enables Node.js HTTPS connection reuse. +- `strictParamValidation` rejects undefined values during parameter serialization; it is not a complete validator for exchange order rules. +- `jwtExpiresSeconds` controls JWT expiry, defaulting to 120 seconds. +- `parseExceptions` defaults to `true`; see [Errors](/guides/errors). +- `baseUrl` overrides the REST origin. Only use a trusted destination because authenticated requests can carry credentials or tokens. + +The SDK's default REST timeout is five minutes. Choose a shorter application-specific timeout where appropriate. A timeout does not establish that a state-changing request failed. + +## Environments {#environments} + +Set `useSandbox: true` on `CBAdvancedTradeClient` to use the static REST sandbox at `api-sandbox.coinbase.com`. It supplies mocked accounts/orders, not a matching engine or live account behavior. This flag changes the host, not the client's signing and request-building behavior. Do not use this static service to validate live authentication or execution. + +There is **no Advanced Trade WebSocket sandbox**. Do not set `useSandbox` on the Advanced Trade stream clients. Leave it unset for the mainnet quickstart. + +Other products have different environments. Follow their [product guides](/products). + +## WebSocket options + +`WebsocketClient` accepts credentials, `pingInterval`, `pongTimeout`, `reconnectTimeout`, `wsOptions`, `wsUrl` and `useSandbox`. Defaults manage connection health and recovery; keep the default logger enabled while integrating. + +For the separate request-response options used by Global derivatives, see its [product guide](/products/global). + +A `wsUrl` override applies to connections created by that client. Avoid reusing one override across unrelated feeds. + +[Proxies](/guides/proxies) · [Production usage](/guides/production) diff --git a/sdks/coinbase/docs/guides/errors.md b/sdks/coinbase/docs/guides/errors.md new file mode 100644 index 00000000..c4486995 --- /dev/null +++ b/sdks/coinbase/docs/guides/errors.md @@ -0,0 +1,32 @@ +--- +title: "Coinbase SDK errors" +description: "Handle Advanced Trade REST failures, business rejections and stream errors." +--- + +# Errors + +Distinguish transport errors, rejected API requests and business-level failures in successful HTTP responses. + +## REST failures + +With `parseExceptions: true` (the default), HTTP failures are thrown as objects containing `code` (HTTP status), `message`, `body`, `headers`, `requestOptions` and `requestParams`. Network errors can retain the underlying error shape; request-setup failures can be thrown as a string. Catch values are not guaranteed to be `Error` instances. + +Set `parseExceptions: false` in the first constructor argument if your application needs the underlying transport error. Request-setup and network failures can still differ in shape. + +The SDK redacts configured credentials from its normalized options, but request parameters, bodies and headers can still contain private data. Avoid dumping full errors into public logs or issue reports. + +## Successful HTTP responses can still fail + +- Advanced Trade order submission: inspect `success` and `error_response`. +- Order preview: inspect `errs` and `warning`. +- Batch cancellation: inspect each item in `results`. + +## WebSocket failures + +Attach an `exception` listener before subscribing. Inspect `response` for subscription acknowledgements and keep the default SDK logger. An active socket is not proof that a requested channel was accepted. + +## Retry policy + +Retry transient read failures with bounded backoff and jitter, respecting rate-limit responses. Correct invalid parameters or credentials rather than repeatedly retrying them. For writes, first resolve any uncertain outcome using private events and targeted REST reads. + +[Production usage](/guides/production) · [Troubleshooting](/guides/troubleshooting) diff --git a/sdks/coinbase/docs/guides/market-data.md b/sdks/coinbase/docs/guides/market-data.md new file mode 100644 index 00000000..5eccd17f --- /dev/null +++ b/sdks/coinbase/docs/guides/market-data.md @@ -0,0 +1,30 @@ +--- +title: "Coinbase market data" +description: "Read public Advanced Trade products, books, candles and trades." +--- + +# Market data + +Advanced Trade has separate public and authenticated market-data methods. For requests without credentials, choose methods with `Public` in the name. `getProduct()`, `getProductBook()`, `getProducts()` and `getBestBidAsk()` use private routes even though they return market data. + +## Product and price + +<<< @/../examples/first-call.ts + +Use `getPublicProducts()` to discover products. Product metadata includes price and size increments, minimum sizes and trading restrictions. Keep decimal quantities as strings and validate against the current product rules before building an order. + +## Order book + +<<< @/../examples/order-book.ts + +The response contains `pricebook`, with bids, asks, product ID and time. This is a snapshot, not a maintained local book. Use the `level2` [WebSocket channel](/guides/websockets) for ongoing updates, following its snapshot and sequence rules. + +## Candles and trades + +- `getPublicProductCandles()` takes `product_id`, Unix-second `start` and `end` strings, and a supported `granularity` such as `ONE_MINUTE`. +- `getPublicMarketTrades()` takes `product_id` and `limit`, with optional time bounds. +- `getServerTime()` returns the server's ISO and epoch timestamps. + +Public endpoints can return cached data. Do not treat a successful response as proof that the price is fresh enough for trading. Check timestamps and use streams for time-sensitive changes. + +[Advanced Trade client reference](/api/classes/CBAdvancedTradeClient) · [Other API families](/products) diff --git a/sdks/coinbase/docs/guides/orders.md b/sdks/coinbase/docs/guides/orders.md new file mode 100644 index 00000000..e03dad7c --- /dev/null +++ b/sdks/coinbase/docs/guides/orders.md @@ -0,0 +1,50 @@ +--- +title: "Coinbase order management" +description: "Preview Advanced Trade orders and understand submission, status and cancellation." +--- + +# Order management + +Use authenticated Advanced Trade requests to read balances, prepare orders, submit them deliberately and manage their lifecycle. Private WebSocket updates keep order state current after submission. The runnable order example previews only; the submission and cancellation steps explain live operations. + +## Check account access and balances + +Configure [Advanced Trade credentials](/guides/authentication) and call `getApiKeyPermissions()` to inspect access. Use `getAccounts()` to read accounts and their available and held balances, following `cursor` while `has_next` is true. Each account has its own UUID; use `getAccount({ account_id })` for a targeted read. A balance is not itself proof that an order meets current product, permission or margin requirements. + +<<< @/../examples/account-state.ts + +## 1. Prepare and preview {#preview} + +Read current product increments, minimums and trading restrictions. Keep amounts as decimal strings. Save this as `preview-order.ts` alongside the [credential module](/guides/authentication), then use that guide's compile and run commands: + +<<< @/../examples/preview-order.ts + +The example previews a market buy with a USD quote size of `10`. This is an illustrative amount, not a recommendation or a guarantee that the order meets current minimums. Inspect `errs`, `warning`, commissions and totals. A successful preview does not reserve funds or guarantee later execution. + +## 2. Submit deliberately {#submit} + +For a real order, call [`submitOrder()`](/api/classes/CBAdvancedTradeClient#submitorder) with the reviewed `product_id`, `side` and `order_configuration`, plus `preview_id` when using the preview result. This sends a **live trading request** with the default mainnet client. + +Generate a client order ID with `client.generateNewOrderId()` and persist it before submission. Keep the exchange's returned `order_id` and `client_order_id` for recovery. Inspect `success`: a resolved HTTP request can contain `success: false` and `error_response`. With success, read `success_response` and save its identifiers. + +Connect and confirm the [private user stream](/guides/private-streams) before submitting. Use subsequent order events to track fills and terminal states; do not query REST simply because submission returned. + +### Application functions + +These functions are building blocks, not a script that trades when started. Call them only after reviewing the live action. Generate and persist the client order ID before passing the request to `submitReviewedOrder()`. Handle rejected promises at your application's boundary; a transport failure may leave the result unknown. + +<<< @/../examples/manage-orders.ts + +## 3. Check an unresolved outcome {#check-order} + +Use [`getOrder({ order_id })`](/api/classes/CBAdvancedTradeClient#getorder) when the response or stream leaves a specific order unresolved. `getOrders()` and `getFills()` help reconcile gaps or discover the matching order when no exchange ID was received. Match saved client ID, product and request context, and follow pagination. + +A request timeout is an unknown outcome. Do not submit a new order with a new ID simply because the first reply was lost. Resolve the original request before deciding whether another order is needed. + +## 4. Cancel when needed {#cancel} + +[`cancelOrders({ order_ids: [...] })`](/api/classes/CBAdvancedTradeClient#cancelorders) initiates cancellation for up to 100 IDs per request. Inspect every item in `results`: each has its own `success` and `failure_reason`. A successful cancellation request does not rule out a fill racing with cancellation. Continue applying private order events and reconcile through REST only if the final state remains unclear. + +`updateOrderPreview()` previews an edit; `updateOrder()` changes the live order. Changes can affect queue priority. Refer to the method and exchange rules before enabling amendments. + +[Production usage](/guides/production) · [Errors](/guides/errors) diff --git a/sdks/coinbase/docs/guides/private-streams.md b/sdks/coinbase/docs/guides/private-streams.md new file mode 100644 index 00000000..006f2abc --- /dev/null +++ b/sdks/coinbase/docs/guides/private-streams.md @@ -0,0 +1,34 @@ +--- +title: "Coinbase private account updates" +description: "Observe Advanced Trade user order updates and recover passive account state." +--- + +# Private account updates + +Use the Advanced Trade `user` channel to observe order changes without making trading requests. It reports user orders and their execution state; do not assume it is a complete ledger of every balance or position change. + +## Subscribe to user orders + +Configure [Advanced Trade credentials](/guides/authentication), save this as `private-stream.ts`, then run `node --env-file=.env private-stream.ts` with Node.js 24: + +<<< @/../examples/private-stream.ts + +The example runs until you stop it. Omitting product IDs requests user updates across available products. The SDK signs private subscription messages automatically. No manual JWT refresh loop is needed for this feed. + +## Apply account changes passively + +The channel sends an initial order snapshot followed by updates. Initial open orders may arrive in batches; do not mark the snapshot complete after the first message. Follow the [user channel schema](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/websocket/websocket-channels) for batching, order status and sequence fields. + +Maintain local order state keyed by order ID and account context. Use status and cumulative filled quantities to update execution state without counting the same fill twice. An order acknowledgement is not a fill, and a subscription reply is not an order acknowledgement. + +Load account balances and any positions or order history the stream does not supply through REST. Apply stream events received during that load. For account fields this feed does not cover, refresh at an explicit workflow boundary or to repair a known gap. Do not claim a complete balance or position store from the `user` channel alone. + +Once established, let events drive the state they cover. Do not query REST after every order or add a polling loop to check for fills. See [Account State](https://siebly.io/reference/glossary#accountstate). + +## Recover after a gap + +On `reconnect`, mark affected state stale. On `reconnected`, confirm subscription recovery and process fresh snapshots; use `getOrders()`, `getFills()` and `getAccounts()` to repair missing state where needed. Follow pagination and account for events arriving during those reads. Resume dependent decisions only when their required state is current. + +These examples print events for inspection; they do not implement a complete account-state database. Protect private event logs as account data. + +[Production usage](/guides/production) · [WebsocketClient reference](/api/classes/WebsocketClient) diff --git a/sdks/coinbase/docs/guides/production.md b/sdks/coinbase/docs/guides/production.md new file mode 100644 index 00000000..ab65b5d8 --- /dev/null +++ b/sdks/coinbase/docs/guides/production.md @@ -0,0 +1,34 @@ +--- +title: "Coinbase SDK production usage" +description: "Manage Coinbase account state, reconnection, uncertain orders and operational safety." +--- + +# Production usage + + + +For Advanced Trade, the `user` feed principally supplies order state. Load balances, positions and any other fields not covered by the selected feed separately. Only apply position-triggered logic from a position source that actually covers your product. + + + +## Resolve uncertain order outcomes + +Persist the client order ID before submission and the exchange order ID when returned. Inspect the response for business errors. Then let the private stream track order changes. If a timeout or disconnect leaves the outcome unresolved, reconcile the original request with `getOrder()`, `getOrders()` and `getFills()` as appropriate; do not blindly recreate it. + +Follow pagination. An empty first page or a delayed stream message does not prove that an order was never accepted. + +## Recover streaming state + +`reconnect` signals recovery starting; mark affected state stale. `reconnected` signals a reopened socket and can trigger reconciliation, but does not establish that subscriptions, authentication or account recovery have completed. + +The SDK reconnects and resubscribes cached topics. Confirm required subscriptions and snapshots, repair gaps with REST, and account for events received while that repair runs. Resume dependent actions only after the relevant state is current. Never assume missed updates are replayed in full. + +Use channel-specific sequence and snapshot rules for market data. Heartbeats show activity, not the correctness of a reconstructed book. A quiet order feed may simply mean no orders changed. + +## Account and rate-limit boundaries + +Apply request budgets across workers sharing an account or key. Coinbase limits vary by API family and endpoint. Keep system clocks synchronized for signing, validate permissions on startup, and separate credentials for different products and environments. + + + +[SDK releases](https://github.com/tiagosiebler/coinbase-api/releases) · [Coinbase developer documentation](https://docs.cdp.coinbase.com/) diff --git a/sdks/coinbase/docs/guides/proxies.md b/sdks/coinbase/docs/guides/proxies.md new file mode 100644 index 00000000..f1a246e8 --- /dev/null +++ b/sdks/coinbase/docs/guides/proxies.md @@ -0,0 +1,40 @@ +--- +title: "Coinbase SDK proxies" +description: "Route Coinbase REST and WebSocket connections through HTTP, HTTPS or SOCKS proxies." +--- + +# Use a proxy + + + +If a Coinbase API key uses an IP allowlist, allow the proxy's egress IP. + +## HTTP or HTTPS proxy for REST + +Pass Axios configuration 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 routing, including proxy environment variables, so the agent controls the REST route. Keep the SDK's `keepAlive` option false: enabling it replaces your custom agent with a standard HTTPS agent. Configure connection reuse on the custom agent itself if needed. + +`wsOptions.agent` configures the Node.js WebSocket transport independently. The same setting is accepted by `WebsocketAPIClient`. Coinbase WebSocket authentication is performed through signing or the socket session; these clients do not have the separate REST token request options used by some other exchange SDKs. + +## SOCKS5 + + + +Pass the SOCKS agent as both REST `httpsAgent` and `wsOptions.agent`, with REST `proxy: false` and SDK `keepAlive: false`. + +## Verify the route + +Use a public `getPublicProduct()` request and an Advanced Trade ticker subscription with the configured clients. Confirm **both** connections in the proxy logs. Successful REST routing alone does not prove WebSocket routing. + + diff --git a/sdks/coinbase/docs/guides/troubleshooting.md b/sdks/coinbase/docs/guides/troubleshooting.md new file mode 100644 index 00000000..78234c43 --- /dev/null +++ b/sdks/coinbase/docs/guides/troubleshooting.md @@ -0,0 +1,32 @@ +--- +title: "Coinbase SDK troubleshooting" +description: "Diagnose Coinbase authentication, subscription, response and proxy issues." +--- + +# Troubleshooting + +## Public request asks for credentials + +Use `getPublicProduct()`, `getPublicProducts()` or `getPublicProductBook()` for unauthenticated Advanced Trade data. The similarly named methods without `Public` use authenticated routes. + +## Authentication fails + +Confirm API family, permissions, environment, key name, private-key format and clock. Advanced Trade uses CDP key credentials. For another API family, follow its [product guide](/products). Preserve PEM newlines. A signer accepting a key does not prove the target product supports its algorithm. + +## WebSocket opens but no data arrives + +Listen to `response` and `exception`. Check the `WS_KEY_MAP` value, exact channel and product IDs. Topic parameters belong in `payload`. Parsed data arrives on **`update`**, not the `message` event used by some other SDKs. Add `heartbeats` to Advanced Trade subscriptions and distinguish a quiet market from a rejected channel. + +## Sandbox URL fails + +Advanced Trade has a static REST sandbox but no WebSocket sandbox. Check [environment availability](/guides/configuration#environments) rather than assuming `useSandbox` applies everywhere. + +## Order request resolves but no order appears + +Inspect `success` and `error_response`, or preview `errs`. A preview never submits an order. For submitted orders, check the private stream and resolve remaining ambiguity with a targeted read. Never repeat an uncertain submission blindly. + +## Proxy works for REST only + +REST Axios options do not configure WebSocket transport. Set `wsOptions.agent` as well. Disable SDK `keepAlive` when supplying a custom HTTPS agent. See [Proxies](/guides/proxies). + + diff --git a/sdks/coinbase/docs/guides/websockets.md b/sdks/coinbase/docs/guides/websockets.md new file mode 100644 index 00000000..f39901c0 --- /dev/null +++ b/sdks/coinbase/docs/guides/websockets.md @@ -0,0 +1,40 @@ +--- +title: "Coinbase WebSocket subscriptions" +description: "Subscribe to Advanced Trade ticker and heartbeat updates with automatic reconnect." +--- + +# WebSocket subscriptions + +`WebsocketClient` manages connections, signing and resubscriptions. Choose the correct `WS_KEY_MAP` feed; channel names and payloads differ across Coinbase products. + +## Stream Advanced Trade prices + +Save this as `stream.ts` and run `node stream.ts` with Node.js 24: + +<<< @/../examples/stream.ts + +The example runs until you press Ctrl+C. `heartbeats` helps keep subscriptions active during quiet periods. The SDK's connection-health handling is separate from your application's price freshness checks. + +The topic object uses **`payload`**, not `params`. For Advanced Trade, `payload.product_ids` supplies product IDs to the channel. Subscribe to `level2` instead of `ticker` when maintaining an order book; its data arrives on the `l2_data` channel. Implement the exchange's snapshot, update and sequence semantics rather than treating each update as a complete book. + +## Events + +| Event | Meaning | +| --- | --- | +| `open` | Initial socket open. | +| `update` | Parsed channel data, with the connection's `wsKey`. | +| `response` | Reply to a subscription or another command. | +| `exception` | SDK, exchange or listener exception. | +| `reconnect` | Connection recovery has started. | +| `reconnected` | Socket reopened after a disconnect. | +| `authenticated` | Session authentication confirmed where that protocol has a separate authentication step. | + +`subscribe()` is not an awaitable server acknowledgement. Inspect `response` and `exception`, and confirm the channels you require before relying on them. Advanced Trade signs subscription requests rather than establishing the Global-style authenticated session. + +## Unsubscribe and shutdown + +Pass the same topic/payload and feed key to `unsubscribe()`. It removes that subscription from the recovery cache. Call `closeAll()` for intentional shutdown; do not add a competing reconnect loop. + +Reconnect restores transport and cached subscriptions, not missed data. See [production recovery](/guides/production#recover-streaming-state) and [private streams](/guides/private-streams). + +[WebsocketClient reference](/api/classes/WebsocketClient) · [Coinbase WebSocket documentation](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/websocket/websocket-overview) diff --git a/sdks/coinbase/docs/index.md b/sdks/coinbase/docs/index.md new file mode 100644 index 00000000..8cf1c331 --- /dev/null +++ b/sdks/coinbase/docs/index.md @@ -0,0 +1,36 @@ +--- +title: "Coinbase API SDK for Node.js, JavaScript and TypeScript" +description: "Use coinbase-api for Advanced Trade, Global derivatives and other Coinbase REST and WebSocket APIs." +--- + + + +# Coinbase API SDK + +[`coinbase-api`](https://www.npmjs.com/package/coinbase-api) provides typed JavaScript and TypeScript clients for Coinbase REST APIs and WebSockets. Start with Advanced Trade market data, then add account access and order management. + +
JavaScript & TypeScript · v{{ sdk.version }} · {{ sdk.license }} license
+ +## Installation + + + +## First request {#usage} + +The [Node.js quickstart](/getting-started) reads a public Bitcoin price without credentials. + +## Choose a client + + + +Coinbase's API families have different accounts, credentials and protocols. These guides focus on **Advanced Trade** unless stated otherwise. + +## Next steps + +- [Read market data](/guides/market-data) or [subscribe to updates](/guides/websockets). +- [Configure credentials](/guides/authentication) and [manage orders](/guides/orders). +- [Observe private account updates](/guides/private-streams). +- [Choose another Coinbase API](/products). +- [Prepare for production](/guides/production) or [browse the reference](/reference). diff --git a/sdks/coinbase/docs/products.md b/sdks/coinbase/docs/products.md new file mode 100644 index 00000000..4c1b228d --- /dev/null +++ b/sdks/coinbase/docs/products.md @@ -0,0 +1,22 @@ +--- +title: "Choose a Coinbase API" +description: "Choose Advanced Trade, Global derivatives, Exchange, INTX, Prime, App or Commerce documentation." +--- + +# Choose a Coinbase API + +Start with [Advanced Trade](/getting-started) for the main market-data, account and order walkthroughs. + +Choose one product group and keep its clients, keys and account identifiers together. A Coinbase login or a shared hostname does not make API access interchangeable. + +| Product | Start here | +| --- | --- | +| Advanced Trade | [Quickstart](/getting-started), [clients](/clients), [reference](/reference). | +| Global derivatives | [Global guide](/products/global): derivatives JSON-RPC and a dedicated WebSocket API. | +| Exchange | [Exchange guide](/products/exchange): Exchange REST and market feeds. | +| International Exchange | [INTX guide](/products/international): international derivatives instruments and portfolios. | +| Prime | [Prime guide](/products/prime): institutional trading, portfolios and custody. | +| Coinbase App | [App guide](/products/app): wallet accounts and transactions. | +| Commerce | [Commerce guide](/products/commerce): charges and checkouts. | + +The product guides describe their own authentication, environments and reference entry points. The main tutorials remain focused on Advanced Trade. diff --git a/sdks/coinbase/docs/products/app.md b/sdks/coinbase/docs/products/app.md new file mode 100644 index 00000000..ca2c8baa --- /dev/null +++ b/sdks/coinbase/docs/products/app.md @@ -0,0 +1,61 @@ +--- +title: "Coinbase App SDK" +description: "Read Coinbase App prices, wallet balances and transactions with App-specific accounts and credentials." +--- + +# Coinbase App + +`CBAppClient` provides Coinbase App wallet/account and transaction endpoints on `api.coinbase.com`. It is a wallet API, not an exchange order client. + +## Setup {#setup} + +Install the package with `npm install coinbase-api`. Save each example with the shown filename and run it with Node.js 24. Public examples need no credentials; for authenticated examples, add the named variables to a private `.env` file excluded from Git, then use `node --env-file=.env filename.ts`. Keep account output private. + +## Public prices {#market-data} + +Save as `app-market-data.ts` and run `node app-market-data.ts`: + +<<< @/../examples/app-market-data.ts + +`getSpotPrice()` returns a `data` object. Use `getBuyPrice()` or `getSellPrice()` for the corresponding price endpoint, and `getExchangeRates()` for currency conversion data. A displayed price is not an executable order or a reserved quote. + +## Authenticate wallet reads {#authentication} + +Use the CDP key name and private key accepted by the App endpoints you need: + +```dotenv +COINBASE_APP_API_KEY=organizations/your-organization/apiKeys/your-key +COINBASE_APP_API_SECRET="your-full-PEM-private-key-with-\n-line-breaks" +``` + +Retain the complete original private-key header, footer and newlines in your local value. The REST client normalizes escaped newline sequences. Grant only the required account permissions; signing a request does not grant access to every App endpoint. + +Save as `app-read.ts`: + +<<< @/../examples/app-read.ts + +### Account balances and pagination {#account-state} + +The response contains `data` (accounts) and `pagination`. Each account has its own ID and balance. Use `getAccount({ account_id })` for one account. When loading the whole account list, follow the returned pagination metadata using `getAccounts({ paginationURL })` rather than assuming the first page is complete. Use only pagination URLs returned by the trusted Coinbase response. + +## Inspect transactions {#transactions} + +Set `COINBASE_APP_ACCOUNT_ID` to an account ID returned above. Save as `app-transactions.ts`: + +<<< @/../examples/app-transactions.ts + +`getTransactions()` also returns `data` and `pagination`. For a specific unresolved transaction, call `getTransaction({ account_id, transactionId })`; the parameter is `transactionId`, not an exchange `order_id`. + +Use transaction status and the account balance together when reconciling wallet activity. Keep original transaction identifiers, and do not assume that a successful HTTP response means a transfer has completed. + +## Money movement {#transfers} + +`sendMoney()` sends supported assets to a recipient, while `transferMoney()` moves supported assets between the user's eligible accounts. Both change account state. `depositFunds()` / `withdrawFunds()` and their commit methods have their own payment-method and confirmation requirements. Do not invoke them just to verify credentials. + +Review the recipient or destination account, currency, amount, fees and permissions before sending. If a response is lost, resolve the original transaction with targeted reads before creating another transfer. + +## Transport and environments {#environments} + +There is no mapped App sandbox, App WebSocket feed or App WebSocket request API in this SDK. Load wallet state through REST at the workflow points that need it. If your application receives account events through another authorized integration, apply those passively and use targeted reads for gaps; exchange market streams do not cover App wallet transactions. + +[App client reference](/api/classes/CBAppClient) · [Choose another API](/products) diff --git a/sdks/coinbase/docs/products/commerce.md b/sdks/coinbase/docs/products/commerce.md new file mode 100644 index 00000000..e8bf2365 --- /dev/null +++ b/sdks/coinbase/docs/products/commerce.md @@ -0,0 +1,60 @@ +--- +title: "Coinbase Commerce SDK" +description: "Read Commerce charges and checkouts, inspect payment state, and keep payment identifiers separate." +--- + +# Commerce + +`CBCommerceClient` targets `api.commerce.coinbase.com`. Its resources are payment charges and checkouts, not exchange accounts or orders. + +## Setup {#setup} + +Install the package with `npm install coinbase-api`. Save each example with the shown filename and run it with Node.js 24. Public examples need no credentials; for authenticated examples, add the named variables to a private `.env` file excluded from Git, then use `node --env-file=.env filename.ts`. Keep account output private. + +## Configure your Commerce key {#authentication} + +Set a Commerce API key in your private environment: + +```dotenv +COINBASE_COMMERCE_API_KEY=your-commerce-api-key +``` + +No API secret or passphrase is required. For these GET requests, supply `X-CC-Api-Key` explicitly through the REST client's second constructor argument. Do not invent a secret to force a signing path. + +## List charges {#charges} + +Save as `commerce-read.ts`: + +<<< @/../examples/commerce-read.ts + +Inspect the charge resources and returned pagination metadata. Preserve the charge ID/code used by your application; do not substitute a checkout ID or an on-chain transaction hash. + +## Read one payment {#payment-state} + +Set `COINBASE_COMMERCE_CHARGE_ID` to a charge ID or code from the list, then save as `commerce-charge.ts`: + +<<< @/../examples/commerce-charge.ts + +Inspect the charge's current payment state and timeline according to the Commerce schema before treating an invoice as paid. A successful charge lookup proves that the resource was retrieved, not that payment has completed. + +Use this targeted read for a specific unresolved payment or to recover missing state. If a webhook integration supplies payment events, validate those events according to Commerce's webhook requirements, deduplicate them and update local payment state passively. Do not mistake a duplicate event for an additional payment. + +## Checkouts and events {#events} + +- `getAllCheckouts()` lists checkout resources. +- `getCheckout({ checkout_id })` retrieves a specific checkout. +- `listEvents()` and `showEvent()` expose event resources; their signatures require an `X-CC-Version` argument. Use the Commerce API version selected for your integration. + +Charges, checkouts and events have different identifiers. Store their relationships explicitly so a payment event updates the right invoice or purchase. Handle pagination and API errors without assuming the broad response types validate payment state for you. + +## Creating payment resources {#payments} + +The reference includes `createCharge()` and `createCheckout()`. They create payment resources rather than submit exchange orders. Review currency, amount, pricing mode, metadata and redirect/cancel destinations before exposing a checkout to a customer. A created charge is not a completed payment. + +## Transport and environments {#environments} + +This SDK has no Commerce WebSocket feed, Commerce WebSocket request API or mapped Commerce sandbox. A webhook receiver is a separate part of your application; `WebsocketClient` does not manage it. + +Keep API keys and customer/payment data out of public logs. Use durable payment identifiers and targeted recovery reads when a response or event leaves the outcome unclear. + +[Commerce client reference](/api/classes/CBCommerceClient) · [Choose another API](/products) diff --git a/sdks/coinbase/docs/products/exchange.md b/sdks/coinbase/docs/products/exchange.md new file mode 100644 index 00000000..f7df4ac5 --- /dev/null +++ b/sdks/coinbase/docs/products/exchange.md @@ -0,0 +1,75 @@ +--- +title: "Coinbase Exchange SDK" +description: "Read Exchange prices, stream public and private updates, and manage Exchange accounts and orders." +--- + +# Exchange + +`CBExchangeClient` targets Coinbase Exchange at `api.exchange.coinbase.com`. Use Exchange credentials, accounts and product IDs throughout this guide. + +## Setup {#setup} + +Install the package with `npm install coinbase-api`. Save each example with the shown filename and run it with Node.js 24. Public examples need no credentials; for authenticated examples, add the named variables to a private `.env` file excluded from Git, then use `node --env-file=.env filename.ts`. Keep account output private. + +## Public REST {#market-data} + +Save as `exchange-market-data.ts` and run `node exchange-market-data.ts`: + +<<< @/../examples/exchange-market-data.ts + +Use `getAllTradingPairs()` to discover products, `getProduct()` for size/price increments and trading restrictions, and `getProductBook()` for a book snapshot. Keep price and size strings at the exchange's supported precision. + +## Public WebSocket prices {#public-streams} + +Save as `exchange-stream.ts` and run `node exchange-stream.ts`: + +<<< @/../examples/exchange-stream.ts + +The stream runs until Ctrl+C. `ticker` updates arrive on `update`; inspect `response` for subscription confirmation and `exception` for errors. To maintain a book, use the Exchange `level2` channel and apply its snapshot/update rules. The singular `heartbeat` channel is available for connection activity. + +## Authentication and account reads {#authentication} + +Create an Exchange API key with the permissions needed by your application. Set: + +```dotenv +COINBASE_EXCHANGE_API_KEY=your-exchange-key +COINBASE_EXCHANGE_API_SECRET=your-exchange-secret +COINBASE_EXCHANGE_API_PASSPHRASE=your-api-passphrase +``` + +The passphrase belongs to the API key, not your account password. Save as `exchange-account.ts`: + +<<< @/../examples/exchange-account.ts + +### Balances and current orders {#account-state} + +`getAccounts()` supplies balances and holds. `getOrders()` defaults to open or unsettled orders; a settled order disappearing from this list does not mean it never existed. Use `getOrder({ order_id })` or `getFills()` for the specific history you need. Follow the endpoint's pagination rather than assuming one response is complete. + +## Order management {#orders} + +1. Read product rules and available balances, then build the Exchange request: `product_id`, `side`, `type`, and the applicable `price`, `size` or `funds` fields. These are flat fields, not an order-configuration object. +2. Generate and persist a client identifier with `client.generateNewOrderId()`, pass it as `client_oid`, and call `submitOrder()` only when ready to trade. This is a live operation. +3. Save the returned exchange order ID and follow private events. Use `getOrder({ order_id })` for a specific unresolved outcome, not as a routine follow-up to every submission. +4. Call `cancelOrder({ order_id })` to cancel an individual order. Cancellation can race with fills; reconcile the final state before submitting a replacement. + +A timeout does not prove a submission failed. Match the original client/exchange identifiers against events and targeted reads before retrying. + +## Private order updates {#private-streams} + +Save as `exchange-private-stream.ts` and run with the Exchange environment variables: + +<<< @/../examples/exchange-private-stream.ts + +This subscribes to the authenticated Exchange `user` channel through `exchangeDirectMarketData`. The SDK signs the subscription using the Exchange key, secret and passphrase. `exchangeMarketData` is the public route; supplying credentials alone does not turn that route into this authenticated subscription. + +Load starting orders and account state, then apply user order events passively. For balance updates, the authenticated `balance` channel takes `payload.account_ids` containing Exchange account IDs. Do not assume the `user` channel itself is a full balance ledger. + +Mark state stale on `reconnect`. On `reconnected`, confirm subscription recovery and reconcile missing orders/fills/balances through targeted REST reads, accounting for events received during recovery. Resume dependent actions only when state is current. See [Account State](https://siebly.io/reference/glossary#accountstate). + +## Environments and reference {#environments} + +`useSandbox: true` selects Exchange sandbox REST and WebSocket hosts. Use sandbox credentials for private requests. Keep separate client instances for live and sandbox accounts. + +This SDK's `WebsocketAPIClient` does not provide Exchange order entry. Use REST for order commands and subscriptions for updates. + +[Exchange client reference](/api/classes/CBExchangeClient) · [Exchange WebSocket channels](https://docs.cdp.coinbase.com/exchange/docs/websocket-channels) · [Choose another API](/products) diff --git a/sdks/coinbase/docs/products/global.md b/sdks/coinbase/docs/products/global.md new file mode 100644 index 00000000..e1afb63f --- /dev/null +++ b/sdks/coinbase/docs/products/global.md @@ -0,0 +1,72 @@ +--- +title: "Coinbase Global derivatives SDK" +description: "Use Global derivatives HTTP JSON-RPC, WebSocket API methods and subscriptions." +--- + +# Global derivatives + +Use this section for the Coinbase Global derivatives gateway, not Advanced Trade spot trading. Access requires product eligibility and the appropriate account permissions. + +## Setup {#setup} + +Install the package with `npm install coinbase-api`. Save each example with the filename shown below and run it with Node.js 24. For private examples, put the named credentials in a private `.env` file excluded from Git and use `node --env-file=.env filename.ts`. Keep account output private. + +## Public REST {#market-data} + +`CBAdvancedTradeGlobalClient` targets `https://drb.coinbase.com`. Its methods send JSON-RPC requests over HTTP and return the unwrapped `result`. Public instrument discovery needs no key. Save as `global-market-data.ts` and run `node global-market-data.ts`: + +<<< @/../examples/global-market-data.ts + +Use the returned instrument names for Global requests. Do not replace them with Advanced Trade spot product IDs such as `BTC-USD`. Global prices and sizes are JSON numbers, order enums are lowercase, and `label` is not a unique equivalent of `client_order_id`. Use the Global types rather than translating an Advanced Trade order mechanically. + +## Public WebSocket subscriptions {#public-streams} + +Use `WS_KEY_MAP.advTradeGlobalMarketData` for public streams at `streams.drb.coinbase.com`. Global channel parameters belong in the topic string, not an Advanced Trade `product_ids` payload. Select an instrument returned by the discovery request above; public intervals include `100ms` and `agg2`, while `raw` requires authentication. + +Save as `global-stream.ts` and run `node global-stream.ts`: + +<<< @/../examples/global-stream.ts + +The example runs until Ctrl+C. Verify the subscription response and implement the channel's snapshot/update rules before building local state. `connectAll()` does not open Global feeds; subscriptions connect on demand. + +## Private REST and order management {#orders} + +For private HTTP calls, provide the eligible CDP key name and private key. The SDK exchanges a fresh signed JWT through `public/auth`, caches the returned Bearer token and refreshes it when needed. Token rejection invalidates cached authorization; the failed operation is **not automatically replayed**. Reconcile uncertain writes before retrying. + +Read account state with `getAccountSummary()` and `getPositions()` for the required currency. Use `getOpenOrders()` to initialize open-order state. These methods use Global parameters and return types, not the Advanced Trade account schema. + +Set `COINBASE_GLOBAL_API_KEY` to your CDP key name and `COINBASE_GLOBAL_API_SECRET` to its complete private key. Preserve the original key header, footer and line breaks; the REST client normalizes escaped `\n` sequences, and the private-stream example normalizes them explicitly. Save as `global-account.ts` and run with your `.env` file: + +<<< @/../examples/global-account.ts + +For live trading, `submitBuy()` and `submitSell()` submit Global orders, `getOrderState()` resolves a specific unknown order, and `cancelOrder()` requests cancellation. These are live actions, not preview operations. Save exchange order IDs, inspect results and use private updates to track execution. A non-unique label alone is not sufficient duplicate protection. + +## Private WebSocket subscriptions {#private-streams} + +Use `WS_KEY_MAP.advTradeGlobalUserData` on `drb.coinbase.com`. The SDK authenticates through `public/auth` before `private/subscribe`, renews the session and restores subscriptions after authenticating a replacement connection. + +Save as `global-private-stream.ts` and run with the same Global environment variables: + +<<< @/../examples/global-private-stream.ts + +This example observes USDC futures order, trade and portfolio changes without placing an order. Adapt currency and product kind to your account. Apply updates passively to account state; use REST for starting state and specific gaps rather than polling. Mark state stale on `reconnect`; `reconnected` alone does not prove authentication, subscription or state recovery has completed. See [Account State](https://siebly.io/reference/glossary#accountstate). + +## Global WebSocket API {#ws-api} + +`WebsocketAPIClient` provides awaitable Global JSON-RPC methods. This public, one-shot example reads server time and closes its connection. Save as `global-ws-api.ts` and run `node global-ws-api.ts`: + +<<< @/../examples/global-ws-api.ts + +`advTradeGlobalWSAPI` is a dedicated request-response connection, separate from private subscriptions even though both use `drb.coinbase.com`. Read `response.result`; unlike the Global HTTP client, this API returns the envelope. `getWSClient()` exposes the underlying event and connection client. `connectWSAPI()` optionally warms up an authenticated session; public calls do not require this authentication step. + +Private calls authenticate the socket automatically. The SDK manages session renewal and can reauthenticate after reconnect. `wsAPITimeout` controls the reply wait after sending (default 10 seconds); connection/authentication use their own lifecycle handling. Pending requests are rejected if their connection is lost. `reauthWSAPIOnReconnect` defaults to enabled for a previously authenticated API session. A new session does not establish the outcome of a command sent on the old one. + +## Errors and uncertain outcomes {#errors} + +JSON-RPC protocol errors reject even when carried by HTTP 200. Inspect their code and message, not only HTTP status. HTTP error parsing is controlled by `parseExceptions`; Global credential redaction and rejected-token invalidation still apply when parsing is disabled. + +WebSocket API calls reject on protocol errors, reply timeout or connection failure. A timed-out trading command is not automatically safe to retry. Preserve its identifiers and reconcile its outcome from private events and targeted account/order reads before issuing a replacement. + +Global has no mapped sandbox. See the [Global client reference](/api/classes/CBAdvancedTradeGlobalClient) and [WebSocket API reference](/api/classes/WebsocketAPIClient) for supported methods. + +[Choose another API](/products) · [Authentication reference](/api/classes/CBAdvancedTradeGlobalClient) diff --git a/sdks/coinbase/docs/products/international.md b/sdks/coinbase/docs/products/international.md new file mode 100644 index 00000000..5477e418 --- /dev/null +++ b/sdks/coinbase/docs/products/international.md @@ -0,0 +1,64 @@ +--- +title: "Coinbase International Exchange SDK" +description: "Read INTX instruments, subscribe to authenticated market data, and manage portfolio balances, positions and orders." +--- + +# International Exchange + +`CBInternationalClient` targets INTX at `api.international.coinbase.com`. Keep INTX instruments, portfolio IDs and credentials together; its order schema is specific to this product. + +## Setup {#setup} + +Install the package with `npm install coinbase-api`. Save each example with the shown filename and run it with Node.js 24. Public examples need no credentials; for authenticated examples, add the named variables to a private `.env` file excluded from Git, then use `node --env-file=.env filename.ts`. Keep account output private. + +## Public REST instruments {#market-data} + +Save as `international-market-data.ts` and run `node international-market-data.ts`: + +<<< @/../examples/international-market-data.ts + +Select the instrument returned by `getInstruments()`. `getInstrumentDetails({ instrument })` reads its rules; `getQuotePerInstrument({ instrument })` reads a quote. Do not infer the instrument name or size constraints from another exchange's symbol. + +## Authentication {#authentication} + +INTX market WebSockets require an API key, secret and API passphrase, even though the public REST discovery request does not. Set these in `.env`: + +```dotenv +COINBASE_INTX_API_KEY=your-intx-key +COINBASE_INTX_API_SECRET=your-intx-secret +COINBASE_INTX_API_PASSPHRASE=your-api-passphrase +``` + +## Market WebSocket subscriptions {#public-streams} + +Save as `international-stream.ts`: + +<<< @/../examples/international-stream.ts + +The `LEVEL1` subscription provides market updates for `BTC-PERP`; choose an instrument available to your environment. Channel names are case-sensitive. The SDK signs requests for `internationalMarketData`. Inspect `response` and `exception` before relying on the stream, which runs until Ctrl+C. + +This is market data, not a stream of your portfolio positions or orders. + +## Read balances and positions {#account-state} + +Use `getUserPortfolios()` to find your INTX portfolio ID. Add `COINBASE_INTX_PORTFOLIO` to `.env`, then save this as `international-account.ts`: + +<<< @/../examples/international-account.ts + +Keep balances, positions and orders scoped to the same portfolio. `getPortfolioSummary()` provides a broader summary; `getPositionForPortfolioInstrument()` narrows a position read to one instrument. Respect each list endpoint's pagination. + +## Order management {#orders} + +Build an INTX `submitOrder()` request with `instrument`, `portfolio`, `side`, `size`, `type` and `tif`, plus a price where required. Generate and persist `client_order_id` with `client.generateNewOrderId()` before sending. Submission changes the live account. + +Use `getOrderDetails({ id, portfolio })` for a specific order, `getOpenOrders({ portfolio })` for current open orders, and `getPortfolioFills()` for execution history. `updateOpenOrder()` amends an order; `cancelOrder({ id, portfolio })` cancels it. Preserve the portfolio alongside the returned order ID. + +Do not treat the market WebSocket as confirmation of your fills. Resolve unknown submissions or cancellations with the relevant portfolio order/fill reads. If your application has a separate private event source, apply it passively and use REST for startup or missing state; this SDK does not map a separate INTX private-account feed or INTX WebSocket order API. + +## Environments and recovery {#environments} + +`useSandbox: true` selects INTX sandbox REST and WebSocket hosts. Use corresponding credentials and instruments. + +The market client reconnects and restores subscriptions. On `reconnect`, mark derived market state stale; on `reconnected`, confirm the channel and rebuild any missing snapshot state before making dependent decisions. A recovered market socket does not repair portfolio state automatically. + +[INTX client reference](/api/classes/CBInternationalClient) · [INTX WebSocket documentation](https://docs.cdp.coinbase.com/intx/docs/websocket-overview) · [Choose another API](/products) diff --git a/sdks/coinbase/docs/products/prime.md b/sdks/coinbase/docs/products/prime.md new file mode 100644 index 00000000..7f8b76ce --- /dev/null +++ b/sdks/coinbase/docs/products/prime.md @@ -0,0 +1,75 @@ +--- +title: "Coinbase Prime SDK" +description: "Read Prime portfolios and balances, preview orders, and configure authenticated market subscriptions." +--- + +# Prime + +`CBPrimeClient` targets institutional trading and custody at `api.prime.coinbase.com`. Requests use Prime entity, portfolio and wallet identifiers. There is no mapped Prime sandbox. + +## Setup {#setup} + +Install the package with `npm install coinbase-api`. Save each example with the shown filename and run it with Node.js 24. Public examples need no credentials; for authenticated examples, add the named variables to a private `.env` file excluded from Git, then use `node --env-file=.env filename.ts`. Keep account output private. + +## Authenticate and discover portfolios {#authentication} + +Prime REST requests require a Prime API key, secret and API passphrase. Keep the key's access limited to the required portfolios and operations: + +```dotenv +COINBASE_PRIME_API_KEY=your-prime-key +COINBASE_PRIME_API_SECRET=your-prime-secret +COINBASE_PRIME_API_PASSPHRASE=your-api-passphrase +``` + +Save as `prime-read.ts`: + +<<< @/../examples/prime-read.ts + +Choose the portfolio returned by `getPortfolios()` and set `COINBASE_PRIME_PORTFOLIO_ID`. The passphrase is an API credential, not the account password. + +## Portfolio balances and products {#account-state} + +Save as `prime-balances.ts`: + +<<< @/../examples/prime-balances.ts + +Choose the appropriate `balance_type`: `TRADING_BALANCES`, `VAULT_BALANCES` or `TOTAL_BALANCES`. A custody balance is not necessarily available trading liquidity. For one wallet, use `getWalletBalance({ portfolio_id, wallet_id })`. + +Use `getPortfolioProducts()` for the products available to the selected portfolio. Product access and order rules remain portfolio-specific. + +## Authenticated market subscriptions {#public-streams} + +The Prime market feed also requires authentication. Use a dedicated `WebsocketClient` with the Prime key, secret and passphrase. Select `WS_KEY_MAP.primeMarketData` and pass the channel's required fields through `payload`: + +- `svcAccountId`: the Prime service-account identifier. +- `portfolio_id`: the selected Prime portfolio. +- `product_ids`: the products to subscribe to. +- `product_type`: optional, when required by the selected product/channel. + +The SDK signs the subscription. Follow the [Prime channel schema](https://docs.cdp.coinbase.com/prime/docs/websocket-channels) for the channel name and snapshot/update behavior. Register `update`, `response` and `exception` listeners before subscribing. Keep the default logger, and call `closeAll()` on intentional shutdown. + +These market subscriptions do not represent a complete custody, order or portfolio ledger. On `reconnect`, mark derived market state stale. On `reconnected`, confirm subscriptions and restore missing snapshots before resuming dependent work. + +## Preview and manage orders {#orders} + +Save as `prime-preview.ts`. This example previews a market buy and does not submit it: + +<<< @/../examples/prime-preview.ts + +The amount is illustrative. Inspect the preview's fees, liquidity and errors; it does not reserve funds or guarantee execution. Use the portfolio's product rules and available trading balance to construct the real request. + +For live order entry, `submitOrder()` takes Prime fields including `portfolio_id`, `product_id`, `side`, `type`, and `base_quantity` or `quote_value`. Generate and persist a `client_order_id` with `client.generateNewOrderId()` before sending. Keep the exchange order ID and portfolio together. + +- `getOpenOrders()` and `getPortfolioOrders()` supply current and historical portfolio orders. +- `getOrderById({ portfolio_id, order_id })` resolves a specific order. +- `getOrderFills()` reads its executions. +- `editOrder()` changes an existing order. +- `cancelOrder({ portfolio_id, order_id })` requests cancellation; it cannot cancel an already filled order. + +Resolve uncertain outcomes before submitting replacements. Do not interpret a market-data update as an order acknowledgement. Where another authorized private event source supplies account changes, apply those events passively and use REST to initialize or repair state. This SDK does not provide a separate Prime private-account feed or Prime WebSocket order API. + +## Custody operations {#custody} + +`getPortfolioWallets()` and `getPortfolioTransactions()` support wallet and transfer inspection. `createTransfer()` and `createWithdrawal()` move funds; they are not read operations. Review the destination, asset, amount and required approvals before using them, and retain transaction identifiers for status reads. + +[Prime client reference](/api/classes/CBPrimeClient) · [Choose another API](/products) diff --git a/sdks/coinbase/docs/reference.md b/sdks/coinbase/docs/reference.md new file mode 100644 index 00000000..61e8ed08 --- /dev/null +++ b/sdks/coinbase/docs/reference.md @@ -0,0 +1,34 @@ +--- +title: "Coinbase SDK API reference" +description: "Find Coinbase REST and WebSocket client methods, parameters and response types." +--- + +# API reference + +Methods, parameters and response types exported by [`coinbase-api`](https://www.npmjs.com/package/coinbase-api). JavaScript and TypeScript share the same API. + +## Advanced Trade clients + +- [CBAdvancedTradeClient](/api/classes/CBAdvancedTradeClient): REST methods. +- [WebsocketClient](/api/classes/WebsocketClient): use Advanced Trade feed keys. + +For other products, use their [separate reference entry points](/products). + +## Advanced Trade tasks + +| Task | Method | +| --- | --- | +| Public product and price | [`getPublicProduct()`](/api/classes/CBAdvancedTradeClient#getpublicproduct) | +| Public order book | [`getPublicProductBook()`](/api/classes/CBAdvancedTradeClient#getpublicproductbook) | +| Authenticated account list | [`getAccounts()`](/api/classes/CBAdvancedTradeClient#getaccounts) | +| Check key permissions | [`getApiKeyPermissions()`](/api/classes/CBAdvancedTradeClient#getapikeypermissions) | +| Preview an order | [`previewOrder()`](/api/classes/CBAdvancedTradeClient#previeworder) | +| Submit an order | [`submitOrder()`](/api/classes/CBAdvancedTradeClient#submitorder) | +| Check a specific order | [`getOrder()`](/api/classes/CBAdvancedTradeClient#getorder) | +| List orders | [`getOrders()`](/api/classes/CBAdvancedTradeClient#getorders) | +| List fills | [`getFills()`](/api/classes/CBAdvancedTradeClient#getfills) | +| Cancel orders | [`cancelOrders()`](/api/classes/CBAdvancedTradeClient#cancelorders) | + +Use [Authentication](/guides/authentication), [Orders](/guides/orders) and [Private account updates](/guides/private-streams) for working context. Search methods and types with **⌘K / Ctrl+K**. + +[Reference index](/api/) · [Coinbase developer documentation](https://docs.cdp.coinbase.com/) diff --git a/sdks/coinbase/examples/account-state.ts b/sdks/coinbase/examples/account-state.ts new file mode 100644 index 00000000..73c79f93 --- /dev/null +++ b/sdks/coinbase/examples/account-state.ts @@ -0,0 +1,20 @@ +import { client } from './authenticated-client.js'; + +async function main() { + const permissions = await client.getApiKeyPermissions(); + console.log(permissions); + + const accounts = await client.getAccounts({ limit: 100 }); + for (const account of accounts.accounts) { + console.log(account.currency, { + available: account.available_balance.value, + held: account.hold.value, + }); + } + console.log('More pages:', accounts.has_next); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/app-market-data.ts b/sdks/coinbase/examples/app-market-data.ts new file mode 100644 index 00000000..0d125533 --- /dev/null +++ b/sdks/coinbase/examples/app-market-data.ts @@ -0,0 +1,12 @@ +import { CBAppClient } from 'coinbase-api'; + +async function main() { + const client = new CBAppClient(); + const response = await client.getSpotPrice({ currencyPair: 'BTC-USD' }); + console.log(response.data); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/app-read.ts b/sdks/coinbase/examples/app-read.ts new file mode 100644 index 00000000..f1a4c114 --- /dev/null +++ b/sdks/coinbase/examples/app-read.ts @@ -0,0 +1,23 @@ +import { CBAppClient } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +async function main() { + const client = new CBAppClient({ + apiKey: requiredEnvVar('COINBASE_APP_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_APP_API_SECRET'), + }); + const response = await client.getAccounts(); + console.log(response); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/app-transactions.ts b/sdks/coinbase/examples/app-transactions.ts new file mode 100644 index 00000000..50b116a6 --- /dev/null +++ b/sdks/coinbase/examples/app-transactions.ts @@ -0,0 +1,25 @@ +import { CBAppClient } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +async function main() { + const client = new CBAppClient({ + apiKey: requiredEnvVar('COINBASE_APP_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_APP_API_SECRET'), + }); + const account_id = requiredEnvVar('COINBASE_APP_ACCOUNT_ID'); + const response = await client.getTransactions({ account_id }); + console.log(response.data); + console.log('Pagination:', response.pagination); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/authenticated-client.ts b/sdks/coinbase/examples/authenticated-client.ts new file mode 100644 index 00000000..93355149 --- /dev/null +++ b/sdks/coinbase/examples/authenticated-client.ts @@ -0,0 +1,14 @@ +import { CBAdvancedTradeClient } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +export const client = new CBAdvancedTradeClient({ + apiKey: requiredEnvVar('COINBASE_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_API_SECRET'), +}); diff --git a/sdks/coinbase/examples/commerce-charge.ts b/sdks/coinbase/examples/commerce-charge.ts new file mode 100644 index 00000000..ae16e348 --- /dev/null +++ b/sdks/coinbase/examples/commerce-charge.ts @@ -0,0 +1,26 @@ +import { CBCommerceClient } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +async function main() { + const apiKey = requiredEnvVar('COINBASE_COMMERCE_API_KEY'); + const client = new CBCommerceClient( + { apiKey }, + { headers: { 'X-CC-Api-Key': apiKey } }, + ); + const response = await client.getCharge({ + charge_code_or_charge_id: requiredEnvVar('COINBASE_COMMERCE_CHARGE_ID'), + }); + console.log(response); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/commerce-read.ts b/sdks/coinbase/examples/commerce-read.ts new file mode 100644 index 00000000..808a8f2c --- /dev/null +++ b/sdks/coinbase/examples/commerce-read.ts @@ -0,0 +1,24 @@ +import { CBCommerceClient } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +async function main() { + const apiKey = requiredEnvVar('COINBASE_COMMERCE_API_KEY'); + const client = new CBCommerceClient( + { apiKey }, + { headers: { 'X-CC-Api-Key': apiKey } }, + ); + const response = await client.getAllCharges(); + console.log(response); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/configure-rest.ts b/sdks/coinbase/examples/configure-rest.ts new file mode 100644 index 00000000..eb21e3f7 --- /dev/null +++ b/sdks/coinbase/examples/configure-rest.ts @@ -0,0 +1,6 @@ +import { CBAdvancedTradeClient } from 'coinbase-api'; + +export const client = new CBAdvancedTradeClient( + { keepAlive: true, strictParamValidation: true }, + { timeout: 10000 }, +); diff --git a/sdks/coinbase/examples/exchange-account.ts b/sdks/coinbase/examples/exchange-account.ts new file mode 100644 index 00000000..e0adf083 --- /dev/null +++ b/sdks/coinbase/examples/exchange-account.ts @@ -0,0 +1,26 @@ +import { CBExchangeClient } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +async function main() { + const client = new CBExchangeClient({ + apiKey: requiredEnvVar('COINBASE_EXCHANGE_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_EXCHANGE_API_SECRET'), + apiPassphrase: requiredEnvVar('COINBASE_EXCHANGE_API_PASSPHRASE'), + }); + const accounts = await client.getAccounts(); + console.log(accounts); + const orders = await client.getOrders(); + console.log(orders); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/exchange-market-data.ts b/sdks/coinbase/examples/exchange-market-data.ts new file mode 100644 index 00000000..9a0b6de6 --- /dev/null +++ b/sdks/coinbase/examples/exchange-market-data.ts @@ -0,0 +1,12 @@ +import { CBExchangeClient } from 'coinbase-api'; + +async function main() { + const client = new CBExchangeClient(); + const ticker = await client.getProductTicker({ product_id: 'BTC-USD' }); + console.log(ticker); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/exchange-private-stream.ts b/sdks/coinbase/examples/exchange-private-stream.ts new file mode 100644 index 00000000..6573c294 --- /dev/null +++ b/sdks/coinbase/examples/exchange-private-stream.ts @@ -0,0 +1,42 @@ +import { WebsocketClient, WS_KEY_MAP } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +const ws = new WebsocketClient({ + apiKey: requiredEnvVar('COINBASE_EXCHANGE_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_EXCHANGE_API_SECRET'), + apiPassphrase: requiredEnvVar('COINBASE_EXCHANGE_API_PASSPHRASE'), +}); + +ws.on('update', (event) => { + console.log(event); +}); +ws.on('response', (event) => { + console.log(event); +}); +ws.on('exception', (error) => { + console.error(error); +}); +ws.on('reconnect', (event) => { + console.log(event); +}); +ws.on('reconnected', (event) => { + console.log(event); +}); + +ws.subscribe( + { topic: 'user', payload: { product_ids: ['BTC-USD'] } }, + WS_KEY_MAP.exchangeDirectMarketData, +); + +function stop() { + ws.closeAll(); +} +process.once('SIGINT', stop); +process.once('SIGTERM', stop); diff --git a/sdks/coinbase/examples/exchange-stream.ts b/sdks/coinbase/examples/exchange-stream.ts new file mode 100644 index 00000000..b16df945 --- /dev/null +++ b/sdks/coinbase/examples/exchange-stream.ts @@ -0,0 +1,30 @@ +import { WebsocketClient, WS_KEY_MAP } from 'coinbase-api'; + +const ws = new WebsocketClient(); + +ws.on('update', (event) => { + console.log(event); +}); +ws.on('response', (event) => { + console.log(event); +}); +ws.on('exception', (error) => { + console.error(error); +}); +ws.on('reconnect', (event) => { + console.log(event); +}); +ws.on('reconnected', (event) => { + console.log(event); +}); + +ws.subscribe( + { topic: 'ticker', payload: { product_ids: ['BTC-USD'] } }, + WS_KEY_MAP.exchangeMarketData, +); + +function stop() { + ws.closeAll(); +} +process.once('SIGINT', stop); +process.once('SIGTERM', stop); diff --git a/sdks/coinbase/examples/first-call.ts b/sdks/coinbase/examples/first-call.ts new file mode 100644 index 00000000..f80bd49c --- /dev/null +++ b/sdks/coinbase/examples/first-call.ts @@ -0,0 +1,12 @@ +import { CBAdvancedTradeClient } from 'coinbase-api'; + +async function main() { + const client = new CBAdvancedTradeClient(); + const product = await client.getPublicProduct({ product_id: 'BTC-USD' }); + console.log(`${product.product_id} price: ${product.price}`); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/global-account.ts b/sdks/coinbase/examples/global-account.ts new file mode 100644 index 00000000..8ccb0e66 --- /dev/null +++ b/sdks/coinbase/examples/global-account.ts @@ -0,0 +1,23 @@ +import { CBAdvancedTradeGlobalClient } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +async function main() { + const client = new CBAdvancedTradeGlobalClient({ + apiKey: requiredEnvVar('COINBASE_GLOBAL_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_GLOBAL_API_SECRET'), + }); + const summary = await client.getAccountSummary({ currency: 'USDC' }); + console.log(summary); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/global-market-data.ts b/sdks/coinbase/examples/global-market-data.ts new file mode 100644 index 00000000..58623cd3 --- /dev/null +++ b/sdks/coinbase/examples/global-market-data.ts @@ -0,0 +1,12 @@ +import { CBAdvancedTradeGlobalClient } from 'coinbase-api'; + +async function main() { + const client = new CBAdvancedTradeGlobalClient(); + const instruments = await client.getInstruments({ currency: 'BTC' }); + console.log(instruments); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/global-private-stream.ts b/sdks/coinbase/examples/global-private-stream.ts new file mode 100644 index 00000000..be507e84 --- /dev/null +++ b/sdks/coinbase/examples/global-private-stream.ts @@ -0,0 +1,41 @@ +import { WebsocketClient, WS_KEY_MAP } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +const ws = new WebsocketClient({ + apiKey: requiredEnvVar('COINBASE_GLOBAL_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_GLOBAL_API_SECRET').replace(/\\n/g, '\n'), +}); + +ws.on('update', (event) => { + console.log(event); +}); +ws.on('response', (event) => { + console.log('Subscription response:', event); +}); +ws.on('exception', (error) => { + console.error('WebSocket exception:', error); +}); +ws.on('reconnect', (event) => { + console.log('Reconnecting:', event); +}); +ws.on('reconnected', (event) => { + console.log('Reconnected:', event); +}); + +ws.subscribe( + ['user.orders.future.USDC.100ms', 'user.trades.future.USDC.100ms', 'user.portfolio.USDC'], + WS_KEY_MAP.advTradeGlobalUserData, +); + +function stop() { + ws.closeAll(); +} +process.once('SIGINT', stop); +process.once('SIGTERM', stop); diff --git a/sdks/coinbase/examples/global-stream.ts b/sdks/coinbase/examples/global-stream.ts new file mode 100644 index 00000000..2bc68dff --- /dev/null +++ b/sdks/coinbase/examples/global-stream.ts @@ -0,0 +1,28 @@ +import { WebsocketClient, WS_KEY_MAP } from 'coinbase-api'; + +const ws = new WebsocketClient(); + +ws.on('update', (event) => { + console.log(event); +}); +ws.on('response', (event) => { + console.log('Subscription response:', event); +}); +ws.on('exception', (error) => { + console.error('WebSocket exception:', error); +}); +ws.on('reconnect', (event) => { + console.log('Reconnecting:', event); +}); +ws.on('reconnected', (event) => { + console.log('Reconnected:', event); +}); + +// Replace this with an instrument returned by getInstruments(). +ws.subscribe('ticker.BTC_USDC-PERPETUAL.100ms', WS_KEY_MAP.advTradeGlobalMarketData); + +function stop() { + ws.closeAll(); +} +process.once('SIGINT', stop); +process.once('SIGTERM', stop); diff --git a/sdks/coinbase/examples/global-ws-api.ts b/sdks/coinbase/examples/global-ws-api.ts new file mode 100644 index 00000000..90bbba86 --- /dev/null +++ b/sdks/coinbase/examples/global-ws-api.ts @@ -0,0 +1,16 @@ +import { WebsocketAPIClient } from 'coinbase-api'; + +async function main() { + const client = new WebsocketAPIClient(); + try { + const response = await client.getTime(); + console.log(response.result); + } finally { + client.getWSClient().closeAll(); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/international-account.ts b/sdks/coinbase/examples/international-account.ts new file mode 100644 index 00000000..3424273e --- /dev/null +++ b/sdks/coinbase/examples/international-account.ts @@ -0,0 +1,26 @@ +import { CBInternationalClient } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +async function main() { + const client = new CBInternationalClient({ + apiKey: requiredEnvVar('COINBASE_INTX_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_INTX_API_SECRET'), + apiPassphrase: requiredEnvVar('COINBASE_INTX_API_PASSPHRASE'), + }); + const portfolio = requiredEnvVar('COINBASE_INTX_PORTFOLIO'); + console.log(await client.getPortfolioBalances({ portfolio })); + console.log(await client.getPortfolioPositions({ portfolio })); + console.log(await client.getOpenOrders({ portfolio })); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/international-market-data.ts b/sdks/coinbase/examples/international-market-data.ts new file mode 100644 index 00000000..c3a0cdf1 --- /dev/null +++ b/sdks/coinbase/examples/international-market-data.ts @@ -0,0 +1,12 @@ +import { CBInternationalClient } from 'coinbase-api'; + +async function main() { + const client = new CBInternationalClient(); + const instruments = await client.getInstruments(); + console.log(instruments); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/international-stream.ts b/sdks/coinbase/examples/international-stream.ts new file mode 100644 index 00000000..56ca5b05 --- /dev/null +++ b/sdks/coinbase/examples/international-stream.ts @@ -0,0 +1,42 @@ +import { WebsocketClient, WS_KEY_MAP } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +const ws = new WebsocketClient({ + apiKey: requiredEnvVar('COINBASE_INTX_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_INTX_API_SECRET'), + apiPassphrase: requiredEnvVar('COINBASE_INTX_API_PASSPHRASE'), +}); + +ws.on('update', (event) => { + console.log(event); +}); +ws.on('response', (event) => { + console.log(event); +}); +ws.on('exception', (error) => { + console.error(error); +}); +ws.on('reconnect', (event) => { + console.log(event); +}); +ws.on('reconnected', (event) => { + console.log(event); +}); + +ws.subscribe( + { topic: 'LEVEL1', payload: { product_ids: ['BTC-PERP'] } }, + WS_KEY_MAP.internationalMarketData, +); + +function stop() { + ws.closeAll(); +} +process.once('SIGINT', stop); +process.once('SIGTERM', stop); diff --git a/sdks/coinbase/examples/manage-orders.ts b/sdks/coinbase/examples/manage-orders.ts new file mode 100644 index 00000000..129dd324 --- /dev/null +++ b/sdks/coinbase/examples/manage-orders.ts @@ -0,0 +1,24 @@ +import type { SubmitAdvTradeOrderRequest } from 'coinbase-api'; +import { client } from './authenticated-client.js'; + +// These functions send live requests only when your application calls them. +// Persist request.client_order_id before calling submitReviewedOrder(). +export async function submitReviewedOrder(request: SubmitAdvTradeOrderRequest) { + const response = await client.submitOrder(request); + if (!response.success || !response.success_response) { + throw new Error(`Order rejected: ${JSON.stringify(response.error_response)}`); + } + const { order_id, client_order_id } = response.success_response; + return { order_id, client_order_id }; +} + +// Use this to resolve a specific gap, not after every healthy stream update. +export async function checkUnresolvedOrder(orderId: string) { + const { order } = await client.getOrder({ order_id: orderId }); + return order; +} + +export async function cancelOrder(orderId: string) { + const response = await client.cancelOrders({ order_ids: [orderId] }); + return response.results; +} diff --git a/sdks/coinbase/examples/order-book.ts b/sdks/coinbase/examples/order-book.ts new file mode 100644 index 00000000..966dd593 --- /dev/null +++ b/sdks/coinbase/examples/order-book.ts @@ -0,0 +1,15 @@ +import { CBAdvancedTradeClient } from 'coinbase-api'; + +async function main() { + const client = new CBAdvancedTradeClient(); + const { pricebook } = await client.getPublicProductBook({ + product_id: 'BTC-USD', + limit: 10, + }); + console.log(pricebook); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/preview-order.ts b/sdks/coinbase/examples/preview-order.ts new file mode 100644 index 00000000..578d27f9 --- /dev/null +++ b/sdks/coinbase/examples/preview-order.ts @@ -0,0 +1,24 @@ +import type { PreviewAdvTradeOrderRequest } from 'coinbase-api'; +import { client } from './authenticated-client.js'; + +async function main() { + const request: PreviewAdvTradeOrderRequest = { + product_id: 'BTC-USD', + side: 'BUY', + order_configuration: { + market_market_ioc: { quote_size: '10' }, + }, + }; + + const preview = await client.previewOrder(request); + console.log(preview); + if (preview.errs.length > 0) { + throw new Error(`Preview rejected: ${preview.errs.join(', ')}`); + } + console.log('Preview only. No order was submitted.'); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/prime-balances.ts b/sdks/coinbase/examples/prime-balances.ts new file mode 100644 index 00000000..9d8abe66 --- /dev/null +++ b/sdks/coinbase/examples/prime-balances.ts @@ -0,0 +1,28 @@ +import { CBPrimeClient } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +async function main() { + const client = new CBPrimeClient({ + apiKey: requiredEnvVar('COINBASE_PRIME_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_PRIME_API_SECRET'), + apiPassphrase: requiredEnvVar('COINBASE_PRIME_API_PASSPHRASE'), + }); + const portfolio_id = requiredEnvVar('COINBASE_PRIME_PORTFOLIO_ID'); + const balances = await client.getPortfolioBalances({ + portfolio_id, + balance_type: 'TRADING_BALANCES', + }); + console.log(balances); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/prime-preview.ts b/sdks/coinbase/examples/prime-preview.ts new file mode 100644 index 00000000..95de6559 --- /dev/null +++ b/sdks/coinbase/examples/prime-preview.ts @@ -0,0 +1,31 @@ +import { CBPrimeClient } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +async function main() { + const client = new CBPrimeClient({ + apiKey: requiredEnvVar('COINBASE_PRIME_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_PRIME_API_SECRET'), + apiPassphrase: requiredEnvVar('COINBASE_PRIME_API_PASSPHRASE'), + }); + const preview = await client.getOrderPreview({ + portfolio_id: requiredEnvVar('COINBASE_PRIME_PORTFOLIO_ID'), + product_id: 'BTC-USD', + side: 'BUY', + type: 'MARKET', + quote_value: '100', + }); + console.log(preview); + console.log('Preview only. No order was submitted.'); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/prime-read.ts b/sdks/coinbase/examples/prime-read.ts new file mode 100644 index 00000000..8bb483b3 --- /dev/null +++ b/sdks/coinbase/examples/prime-read.ts @@ -0,0 +1,24 @@ +import { CBPrimeClient } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +async function main() { + const client = new CBPrimeClient({ + apiKey: requiredEnvVar('COINBASE_PRIME_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_PRIME_API_SECRET'), + apiPassphrase: requiredEnvVar('COINBASE_PRIME_API_PASSPHRASE'), + }); + const response = await client.getPortfolios(); + console.log(response); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/sdks/coinbase/examples/private-stream.ts b/sdks/coinbase/examples/private-stream.ts new file mode 100644 index 00000000..014fd5e4 --- /dev/null +++ b/sdks/coinbase/examples/private-stream.ts @@ -0,0 +1,39 @@ +import { WebsocketClient, WS_KEY_MAP } from 'coinbase-api'; + +function requiredEnvVar(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`Set ${name}.`); + } + return value; +} + +const ws = new WebsocketClient({ + apiKey: requiredEnvVar('COINBASE_API_KEY'), + apiSecret: requiredEnvVar('COINBASE_API_SECRET').replace(/\\n/g, '\n'), +}); + +ws.on('update', (event) => { + console.log(event); +}); +ws.on('response', (event) => { + console.log('Subscription response:', event); +}); +ws.on('exception', (error) => { + console.error('WebSocket exception:', error); +}); +ws.on('reconnect', (event) => { + console.log('Reconnecting:', event); +}); +ws.on('reconnected', (event) => { + console.log('Reconnected:', event); +}); + +// Observe all available user orders; this example does not submit orders. +ws.subscribe(['user', 'heartbeats'], WS_KEY_MAP.advTradeUserData); + +function stop() { + ws.closeAll(); +} +process.once('SIGINT', stop); +process.once('SIGTERM', stop); diff --git a/sdks/coinbase/examples/proxy-agent.ts b/sdks/coinbase/examples/proxy-agent.ts new file mode 100644 index 00000000..b1eb762f --- /dev/null +++ b/sdks/coinbase/examples/proxy-agent.ts @@ -0,0 +1,14 @@ +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { CBAdvancedTradeClient, WebsocketClient } from 'coinbase-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 CBAdvancedTradeClient( + { keepAlive: false }, + { proxy: false, httpsAgent: agent, timeout: 20000 }, +); +export const ws = new WebsocketClient({ wsOptions: { agent } }); diff --git a/sdks/coinbase/examples/proxy-rest.ts b/sdks/coinbase/examples/proxy-rest.ts new file mode 100644 index 00000000..1be90512 --- /dev/null +++ b/sdks/coinbase/examples/proxy-rest.ts @@ -0,0 +1,9 @@ +import { CBAdvancedTradeClient } from 'coinbase-api'; + +export const client = new CBAdvancedTradeClient( + {}, + { + proxy: { protocol: 'http', host: '127.0.0.1', port: 8080 }, + timeout: 20000, + }, +); diff --git a/sdks/coinbase/examples/stream.ts b/sdks/coinbase/examples/stream.ts new file mode 100644 index 00000000..4d5fee37 --- /dev/null +++ b/sdks/coinbase/examples/stream.ts @@ -0,0 +1,33 @@ +import { WebsocketClient, WS_KEY_MAP } from 'coinbase-api'; + +const ws = new WebsocketClient(); + +ws.on('update', (event) => { + console.log(event); +}); +ws.on('response', (event) => { + console.log('Subscription response:', event); +}); +ws.on('exception', (error) => { + console.error('WebSocket exception:', error); +}); +ws.on('reconnect', (event) => { + console.log('Reconnecting:', event); +}); +ws.on('reconnected', (event) => { + console.log('Reconnected:', event); +}); + +ws.subscribe( + [ + { topic: 'ticker', payload: { product_ids: ['BTC-USD'] } }, + { topic: 'heartbeats' }, + ], + WS_KEY_MAP.advTradeMarketData, +); + +function stop() { + ws.closeAll(); +} +process.once('SIGINT', stop); +process.once('SIGTERM', stop); diff --git a/sdks/coinbase/sdk.config.mjs b/sdks/coinbase/sdk.config.mjs new file mode 100644 index 00000000..1c067a3a --- /dev/null +++ b/sdks/coinbase/sdk.config.mjs @@ -0,0 +1,93 @@ +const websocketSecretDescription = 'API signing secret for the selected product. For CDP authentication, pass the complete private key, including its original PEM header, footer and newlines when applicable.'; + +export default { + name: 'Coinbase', + sourceDirectory: 'coinbase-api', + packageName: 'coinbase-api', + entryPoint: 'src/index.ts', + tsconfig: 'tsconfig.json', + compatibilityPage: 'authored', + clients: [ + 'CBAdvancedTradeClient', + 'CBAdvancedTradeGlobalClient', + 'CBAppClient', + 'CBCommerceClient', + 'CBExchangeClient', + 'CBInternationalClient', + 'CBPrimeClient', + 'WebsocketClient', + 'WebsocketAPIClient', + ], + website: 'https://siebly.io/sdk/coinbase/javascript', + tutorial: 'https://siebly.io/sdk/coinbase/javascript/tutorial', + repository: 'https://github.com/sieblyio/coinbase-api', + referenceHelp: [ + { client: 'CBAdvancedTradeClient', method: 'getPublicProduct', guide: '/getting-started', title: 'Read an Advanced Trade public price' }, + { client: 'CBAdvancedTradeClient', method: 'getPublicProductBook', guide: '/guides/market-data', title: 'Read the public order book' }, + { client: 'CBAdvancedTradeClient', method: 'getAccounts', guide: '/guides/authentication', title: 'Read authenticated account balances' }, + { client: 'CBAdvancedTradeClient', method: 'getApiKeyPermissions', guide: '/guides/authentication', title: 'Check API key permissions' }, + ...['previewOrder', 'submitOrder', 'getOrder', 'getOrders', 'getFills', 'cancelOrders'].map(method => ({ + client: 'CBAdvancedTradeClient', method, guide: '/guides/orders', title: 'Order management', + })), + { client: 'WebsocketClient', method: 'subscribe', guide: '/guides/websockets', title: 'Advanced Trade subscriptions' }, + { client: 'CBAdvancedTradeGlobalClient', method: 'getInstruments', guide: '/products/global', title: 'Advanced Trade Global market data' }, + { client: 'WebsocketAPIClient', method: 'getTime', guide: '/products/global', title: 'Advanced Trade Global WebSocket API' }, + ], + referenceDescriptions: { + CBCommerceClient: 'REST client for Coinbase Commerce charges and checkouts.', + 'WSClientConfigurableOptions.apiSecret': websocketSecretDescription, + 'WebsocketClientOptions.apiSecret': websocketSecretDescription, + }, + referenceExcludeProperties: ['affiliate_promotion_fee', 'referrer_id'], + guides: [ + { text: 'Order management', link: '/guides/orders' }, + { text: 'Private account updates', link: '/guides/private-streams' }, + ], + productGroups: [ + { + text: 'Advanced Trade Global', slug: 'global', clients: ['CBAdvancedTradeGlobalClient', 'WebsocketAPIClient'], + guides: [ + ['Public REST', 'market-data'], ['Public subscriptions', 'public-streams'], + ['Account state and orders', 'orders'], ['Private account updates', 'private-streams'], + ['WebSocket API', 'ws-api'], ['Errors and recovery', 'errors'], + ], + }, + { + text: 'Exchange', slug: 'exchange', clients: ['CBExchangeClient'], + guides: [ + ['Public REST', 'market-data'], ['Public subscriptions', 'public-streams'], + ['Authentication', 'authentication'], ['Account state', 'account-state'], + ['Order management', 'orders'], ['Private account updates', 'private-streams'], ['Sandbox', 'environments'], + ], + }, + { + text: 'International Exchange', slug: 'international', clients: ['CBInternationalClient'], + guides: [ + ['Public REST', 'market-data'], ['Authentication', 'authentication'], + ['Market subscriptions', 'public-streams'], ['Account state', 'account-state'], + ['Order management', 'orders'], ['Sandbox', 'environments'], + ], + }, + { + text: 'Prime', slug: 'prime', clients: ['CBPrimeClient'], + guides: [ + ['Authentication', 'authentication'], ['Account state', 'account-state'], + ['Market subscriptions', 'public-streams'], ['Order management', 'orders'], ['Custody', 'custody'], + ], + }, + { + text: 'App', slug: 'app', clients: ['CBAppClient'], + guides: [ + ['Public prices', 'market-data'], ['Authentication', 'authentication'], ['Account state', 'account-state'], + ['Transactions', 'transactions'], ['Transfers', 'transfers'], + ], + }, + { + text: 'Commerce', slug: 'commerce', clients: ['CBCommerceClient'], + guides: [ + ['Authentication', 'authentication'], ['Read charges', 'charges'], ['Payment state', 'payment-state'], + ['Payment events', 'events'], ['Create payments', 'payments'], + ], + }, + ], +}; diff --git a/shared/coinbase-sidebar.mjs b/shared/coinbase-sidebar.mjs new file mode 100644 index 00000000..970a8861 --- /dev/null +++ b/shared/coinbase-sidebar.mjs @@ -0,0 +1,54 @@ +// Coinbase keeps the default path focused on Advanced Trade. Other product +// landing pages and REST classes show only their own family’s references. +export function coinbaseSidebar(sdk, groups) { + const guideGroup = groups.find(group => group.items?.some(item => item.link === '/guides/market-data')); + guideGroup.text = 'Advanced Trade'; + const setups = groups.find(group => group.items?.some(item => item.link === '/environments/node')); + delete setups.collapsed; + setups.text = 'Frameworks and runtimes (Advanced Trade)'; + const resources = groups.find(group => group.text === 'Resources'); + resources.items.find(item => item.link === '/compatibility').text = 'Runtime setup'; + const reference = groups.find(group => group.text === 'API reference'); + reference.items = [ + { text: 'Advanced Trade reference', link: '/reference' }, + { text: 'CBAdvancedTradeClient', link: '/api/classes/CBAdvancedTradeClient' }, + { text: 'Advanced Trade WebSocket subscriptions', link: '/api/classes/WebsocketClient' }, + { text: 'All SDK exports', link: '/api/' }, + ]; + const products = { + text: 'Other Coinbase products', collapsed: true, + items: [ + { text: 'Choose a product', link: '/products' }, + ...sdk.productGroups.map(product => ({ text: product.text, link: `/products/${product.slug}` })), + ], + }; + groups.splice(2, 0, products); + const sidebar = { '/': groups }; + for (const product of sdk.productGroups) { + const productSidebar = [ + { text: 'Coinbase products', items: [ + { text: 'Advanced Trade', link: '/' }, + { text: 'Choose another product', link: '/products' }, + ] }, + { text: product.text, items: [ + { text: 'Overview and setup', link: `/products/${product.slug}` }, + ...product.guides.map(([text, anchor]) => ({ text, link: `/products/${product.slug}#${anchor}` })), + ] }, + { text: 'API reference', items: [ + ...product.clients.map(name => ({ text: name, link: `/api/classes/${name}` })), + ...(product.slug === 'commerce' || product.slug === 'app' ? [] : [ + { text: 'Shared WebSocket client', link: '/api/classes/WebsocketClient' }, + ]), + ] }, + { text: 'SDK resources', items: [ + { text: 'Node.js proxy configuration', link: '/guides/proxies' }, + { text: 'Advanced Trade runtime examples', link: '/compatibility' }, + { text: 'npm package', link: `https://www.npmjs.com/package/${sdk.packageName}` }, + { text: 'GitHub repository', link: sdk.repository }, + ] }, + ]; + sidebar[`/products/${product.slug}`] = productSidebar; + for (const name of product.clients) sidebar[`/api/classes/${name}`] = productSidebar; + } + return sidebar; +} diff --git a/shared/config.mjs b/shared/config.mjs index f9d0ec2d..30964992 100644 --- a/shared/config.mjs +++ b/shared/config.mjs @@ -1,4 +1,5 @@ import path from 'node:path'; +import { coinbaseSidebar } from './coinbase-sidebar.mjs'; import { sourceCommentMarkup } from './markdown.mjs'; import { analyticsAttributes } from './site-services.mjs'; @@ -77,7 +78,7 @@ export default function siteConfig(data, docs) { ], // Full symbol catalogues live in All exports and search. Repeating every // type in every page's sidebar makes large SDK output grow quadratically. - sidebar: groups, + sidebar: sdk.productGroups ? coinbaseSidebar(sdk, groups) : groups, search: { provider: 'local', options: { detailedView: true } }, socialLinks: [{ icon: 'github', link: sdk.repository }], outline: { level: [2, 3], label: 'On this page' }, diff --git a/shared/environments/direct/exchanges.mjs b/shared/environments/direct/exchanges.mjs index 61dac865..e7af8edf 100644 --- a/shared/environments/direct/exchanges.mjs +++ b/shared/environments/direct/exchanges.mjs @@ -1,4 +1,32 @@ export const exchanges = { + coinbase: { + restClient: 'CBAdvancedTradeClient', + symbol: 'BTC-USD', + continuous: true, + cliSubscription: "ws.subscribe({ topic: 'ticker', payload: { product_ids: ['BTC-USD'] } }, 'advTradeMarketData')", + request: `const client = new CBAdvancedTradeClient(); +const ticker = await client.getPublicProduct({ product_id: 'BTC-USD' }); +const price = ticker.price;`, + event: 'update', + update: `if (message.channel !== 'ticker') { + console.log('WebSocket update:', message); + return; +} + +let price; +for (const event of message.events ?? []) { + for (const ticker of event.tickers ?? []) { + if (ticker.product_id === 'BTC-USD') { + price = ticker.price; + } + } +} +if (!price) { + console.log('WebSocket update:', message); +}`, + websocket: 'new WebsocketClient()', + subscription: "Promise.resolve(ws.subscribe({ topic: 'ticker', payload: { product_ids: ['BTC-USD'] } }, 'advTradeMarketData'))", + }, kraken: { restClient: 'SpotClient', symbol: 'BTC/USD', diff --git a/shared/environments/direct/render.mjs b/shared/environments/direct/render.mjs index 1550b9be..01c8b3f6 100644 --- a/shared/environments/direct/render.mjs +++ b/shared/environments/direct/render.mjs @@ -31,7 +31,7 @@ export async function programFiles(id, sdk, format = 'esm', extension = 'mjs') { SYMBOL: exchange.symbol, CLOSE: commonjs ? `}\n\nmain().catch((error) => {\n console.error(error);\n process.exitCode = 1;\n});` : '', }).trimEnd() + '\n'; - const stream = fill(await readFile(new URL('./stream.tpl', import.meta.url), 'utf8'), { + const stream = fill(await readFile(new URL(exchange.continuous ? './stream-continuous.tpl' : './stream.tpl', import.meta.url), 'utf8'), { IMPORT: `${importSdk('WebsocketClient')}\n${commonjs ? "const process = require('node:process');" : "import process from 'node:process';"}`, WEBSOCKET: exchange.websocket, SYMBOL: exchange.symbol, @@ -39,8 +39,8 @@ export async function programFiles(id, sdk, format = 'esm', extension = 'mjs') { PROMISE_TYPE: typescript ? 'BTC-USD REST price:
+BTC-USD WebSocket price:
+{error}
+Stream stopped. WebSocket closed.
} + +REST:
+WebSocket:
+ + + + + diff --git a/starters/coinbase/vite/main.js b/starters/coinbase/vite/main.js new file mode 100644 index 00000000..ddff80f6 --- /dev/null +++ b/starters/coinbase/vite/main.js @@ -0,0 +1,124 @@ +import { CBAdvancedTradeClient, WebsocketClient } from 'coinbase-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 CBAdvancedTradeClient(); + const ticker = await client.getPublicProduct({ product_id: 'BTC-USD' }); + const price = ticker.price; + + if (!stopped) { + restOutput.textContent = String(price); + } + } + + loadPrice().catch(showError); + + const timeout = setTimeout(() => { + showError(new Error('No BTC-USD update within 30 seconds.')); + }, 30_000); + + ws.on('update', (message) => { + if (message.channel !== 'ticker') { + console.log('WebSocket update:', message); + return; + } + + let price; + for (const event of message.events ?? []) { + for (const ticker of event.tickers ?? []) { + if (ticker.product_id === 'BTC-USD') { + price = ticker.price; + } + } + } + if (!price) { + console.log('WebSocket update:', message); + } + + if (!stopped && price) { + clearTimeout(timeout); + liveOutput.textContent = String(price); + } + }); + ws.on('exception', showError); + + Promise.resolve().then(async () => { + if (stopped) { + return; + } + + try { + await Promise.resolve(ws.subscribe({ topic: 'ticker', payload: { product_ids: ['BTC-USD'] } }, 'advTradeMarketData')); + } 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/coinbase/vite/package.json b/starters/coinbase/vite/package.json new file mode 100644 index 00000000..4efa04d2 --- /dev/null +++ b/starters/coinbase/vite/package.json @@ -0,0 +1,17 @@ +{ + "name": "coinbase-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": { + "coinbase-api": "latest", + "events": "3.3.0" + }, + "devDependencies": { + "vite": "5.4.21" + } +} diff --git a/starters/coinbase/vite/vite.config.js b/starters/coinbase/vite/vite.config.js new file mode 100644 index 00000000..1516aac1 --- /dev/null +++ b/starters/coinbase/vite/vite.config.js @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url'; + +// Public market data only. Coinbase's JWT helper imports Node crypto eagerly. +// Keep credentialed calls on a Node.js server rather than bundling private keys. +const publicAuth = '\0coinbase-public-auth'; +const sdkRoot = fileURLToPath(new URL('./node_modules/coinbase-api/', import.meta.url)); +export default { + // Keep SDK modules in Vite's transform pipeline so the JWT replacement also + // applies during development. Prebundle its CommonJS browser dependencies. + optimizeDeps: { + exclude: ['coinbase-api'], + include: ['events', 'coinbase-api > isomorphic-ws'], + }, + resolve: { + alias: { + events: fileURLToPath(new URL('./node_modules/events/events.js', import.meta.url)), + }, + }, + plugins: [{ + name: 'coinbase-public-market-data', + enforce: 'pre', + resolveId(source, importer) { + if (source.endsWith('/jwtNode.js') && importer?.startsWith(sdkRoot)) { + return publicAuth; + } + }, + load(id) { + if (id === publicAuth) { + return `export function signJWT() { throw new Error('JWT signing is server-only in this public market-data starter.'); } +export const signWSJWT = signJWT;`; + } + }, + }], +};