Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8a20a8e
Add Coinbase introduction and runtime overview
MiniTiago Nov 10, 2026
81f96a8
Add Advanced Trade guides
MiniTiago Nov 10, 2026
212e603
Add separate Coinbase product guides
MiniTiago Nov 10, 2026
4295b1c
Add Coinbase account and streaming examples
MiniTiago Nov 10, 2026
b2b5926
Configure Coinbase products and API references
MiniTiago Nov 10, 2026
ed30312
Add continuous Coinbase starter streams
MiniTiago Nov 10, 2026
2ddc1be
Add Coinbase Node.js starter
MiniTiago Nov 10, 2026
42ba7a9
Add Coinbase TypeScript starter
MiniTiago Nov 10, 2026
816c91f
Add Coinbase React starter
MiniTiago Nov 10, 2026
44a57e1
Add Coinbase Vite starter
MiniTiago Nov 10, 2026
21d01e2
Add Coinbase Bun starter
MiniTiago Nov 10, 2026
266e997
Add Coinbase Deno starter
MiniTiago Nov 10, 2026
b03b59d
Add Coinbase Expo starter
MiniTiago Nov 10, 2026
b30eb14
Add Coinbase Next.js starter
MiniTiago Nov 10, 2026
2f1250f
Add Coinbase Next.js pages
MiniTiago Nov 10, 2026
9422fba
Add Coinbase server price route
MiniTiago Nov 10, 2026
1425476
List Coinbase runtime starters
MiniTiago Nov 10, 2026
4bed9b7
Add Coinbase public-data adapter
MiniTiago Nov 10, 2026
eb0d98f
Configure Coinbase public browser applications
MiniTiago Nov 10, 2026
02f8e71
Configure Coinbase public Expo applications
MiniTiago Nov 10, 2026
541143e
Support Coinbase runtime recipes
MiniTiago Nov 10, 2026
fc4231b
Add Coinbase to SDK release inputs
MiniTiago Nov 10, 2026
fe340df
Track Coinbase SDK release updates
MiniTiago Nov 10, 2026
3c23857
Integrate Coinbase documentation generation
MiniTiago Nov 10, 2026
ef7fa04
Add product-scoped Coinbase navigation
MiniTiago Nov 10, 2026
6f5e059
Support precise API member descriptions
MiniTiago Nov 10, 2026
ab1130d
Clarify Coinbase signing credential descriptions
MiniTiago Nov 10, 2026
6dab82f
Complete Global setup instructions
MiniTiago Nov 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ updates:
- dependency-name: bybit-api
- dependency-name: binance
- dependency-name: '@siebly/kraken-api'
- dependency-name: coinbase-api
groups:
sdk-releases:
patterns:
- '@siebly/hyperliquid-api'
- bybit-api
- binance
- '@siebly/kraken-api'
- coinbase-api
cooldown:
exclude:
- '@siebly/hyperliquid-api'
- bybit-api
- binance
- '@siebly/kraken-api'
- coinbase-api
25 changes: 25 additions & 0 deletions compatibility/adapters/coinbase.mjs
Original file line number Diff line number Diff line change
@@ -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();
`;
34 changes: 34 additions & 0 deletions compatibility/browser-config/coinbase.mjs
Original file line number Diff line number Diff line change
@@ -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;`;
}
},
}],
};
2 changes: 1 addition & 1 deletion compatibility/browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions compatibility/native-config/coinbase-public-auth.cjs
Original file line number Diff line number Diff line change
@@ -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 };
26 changes: 26 additions & 0 deletions compatibility/native-config/coinbase.metro.cjs
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 1 addition & 1 deletion compatibility/native-config/recipe.mjs
Original file line number Diff line number Diff line change
@@ -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}.`);
}
Expand Down
1 change: 1 addition & 0 deletions compatibility/native.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion releases/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
2 changes: 1 addition & 1 deletion scripts/check-public.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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]));
Expand Down
2 changes: 1 addition & 1 deletion scripts/context.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(', ')}.`);
Expand Down
10 changes: 9 additions & 1 deletion scripts/environment-guides.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions scripts/environments.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Expand Down Expand Up @@ -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.'
Expand Down
27 changes: 25 additions & 2 deletions scripts/generate.mjs
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'));
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion scripts/release-manifest.mjs
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion scripts/release-refresh.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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]' } }] });

Expand Down
4 changes: 2 additions & 2 deletions scripts/release-update.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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));
Expand Down
Loading
Loading