
Aicoin Market
- 580 installs
- 51 repo stars
- Updated June 9, 2026
- aicoincom/coinos-skills
aicoin-market is a Node.js AiCoin Open Data v3 API client skill that fetches real-time cryptocurrency prices and exchange data for developers who embed live market feeds in agents or scripts.
About
aicoin-market is a Node.js skill from aicoincom/coinos-skills that wraps the AiCoin Open Data v3 API with HMAC-SHA1 signed header authentication. Responses use a unified envelope with ok, data, error, and meta fields, and the client auto-loads credentials from .env paths including workspace and OpenClaw locations. Developers use aicoin-market when agents or scripts need programmatic access to live crypto prices, exchange information, and cataloged market endpoints without building signing logic from scratch. It fits backend and agent workflows that consume market data rather than execute trades directly.
- Unified response envelope with {ok, data, error, meta} structure across all endpoints
- Automatic HMAC-SHA1 request signing and header authentication
- Smart .env loading from multiple workspace and user locations
- Built-in fallback defaults and graceful error handling
- Node.js crypto and filesystem utilities for secure API access
Aicoin Market by the numbers
- 580 all-time installs (skills.sh)
- Ranked #698 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aicoincom/coinos-skills --skill aicoin-marketAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 580 |
|---|---|
| repo stars | ★ 51 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 9, 2026 |
| Repository | aicoincom/coinos-skills ↗ |
How do you query crypto market data from agents?
Query real-time cryptocurrency market data, prices, and exchange information through the AiCoin Open Data API directly from their agents or scripts.
Who is it for?
Developers building Node agents or scripts that need HMAC-signed AiCoin Open Data v3 market and exchange queries with .env credential loading.
Skip if: Developers who only need historical equity fundamentals or who want on-chain wallet analytics without exchange API integration.
When should I use this skill?
An agent or Node script must query real-time cryptocurrency prices or exchange data through the AiCoin Open Data v3 API with signed requests.
What you get
Signed API responses with prices, exchange metadata, and structured ok/data/error/meta envelopes.
- signed market API responses
- exchange metadata payloads
Files
AiCoin Market
Crypto market data from the AiCoin Open API v3 — prices, K-lines, derivatives, news, signals, airdrops, treasuries and more across 200+ exchanges.
从 SKILL.md 所在目录运行脚本。CoinClaw 三引擎容器(OpenClaw / Hermes / Claude Code)自动注入 skill 路径,cd 到 skill 目录即可。怎么用
一个命令调所有接口:
node scripts/aicoin.mjs <接口> '<JSON 参数>'<接口> 就是 /api/v3/ 后面那段路径,例如 market/ticker、coins/tickers。
- 返回统一信封
{ ok, data, error, meta }—— 先看 `ok`。ok:false时看error.code/error.message,里面写清楚了哪里错。 - 不确定有哪些接口、参数怎么填 →
node scripts/aicoin.mjs catalog [分组]。catalog 是线上实时的接口菜单,永远准。先查它再调。 - 想知道当前 key 能用哪些接口 →
node scripts/aicoin.mjs key。
node scripts/aicoin.mjs catalog # 看全部 183 个接口(按分组)
node scripts/aicoin.mjs catalog derivatives # 看 derivatives 分组每个接口的参数
node scripts/aicoin.mjs market/ticker '{"coin_key":"bitcoin","market":"binance"}'常用接口速查
| 想查什么 | 接口 + 例子 |
|---|---|
| 币价 / 涨跌 / 市值 / 净流入 | coins/tickers '{"coin_key":"bitcoin,ethereum"}' —— degree_24h_usd / degree_7day_usd 是 24h / 7 天涨跌幅(%) |
| 单交易对实时行情 | market/ticker '{"coin_key":"bitcoin","market":"binance"}' |
| K 线 | market/klines '{"coin_key":"bitcoin","market":"binance","interval":"1h","limit":100}' |
| 搜币种 / 查某币在哪些交易所 | coins/search '{"query":"PEPE"}' —— 每条结果带 db_keys,列出该币跨交易所的全部交易对 |
| 币种详情 / 简介 | coins/detail '{"coin_key":"bitcoin"}' |
| 全部币种 / 全部交易所 | coins '{"limit":100}' / markets |
| 某交易所全部交易对行情 | market/tickers '{"market":"binance"}' |
| 交易对列表 | pairs '{"market":"binance"}' |
| 热门赛道币 | markets/hot-coins '{"tab_key":"defi"}' |
| 资金费率 | derivatives/funding-rates '{"coin_key":"bitcoin","market":"binance"}' —— 返回 8h OHLC 序列,close 是当期结算费率(小数,×100 得百分比) |
| 多空比 | 单交易对历史序列 derivatives/long-short-ratio '{"coin_key":"bitcoin","market":"binance"}';全市场当前汇总 derivatives/long-short-ratio/summary(不分币种,别当成某个币的) |
| 合约持仓量排名 | derivatives/open-interest/ranking |
| 清算地图 / 清算汇总 | derivatives/liquidations/map '{"coin_key":"bitcoin","market":"binance","window":"24h"}' / derivatives/liquidations/summary |
| 大单 / 大单成交 | market/big-orders '{"coin_key":"bitcoin","market":"binance"}' / market/aggregate-trades '{"coin_key":"bitcoin","market":"binance"}' |
| 订单簿深度 | market/orderbook/latest-depth '{"coin_key":"bitcoin","market":"binance"}' |
| 资讯文章 / 快讯 | content/articles / content/newsflashes / 行业 content/newsflashes/industry |
| 搜快讯 | content/newsflashes/search '{"query":"bitcoin"}' |
| 推特/X | content/social/x/posts/latest / 搜 content/social/x/posts/search '{"query":"bitcoin"}' |
| 空投项目(有哪些值得做) | drop-radar/projects —— 项目最全;详情 drop-radar/projects/detail '{"project_id":"..."}' |
| 交易所空投 / 空投日历 | airdrops '{"source":"all"}'(交易所活动,可能为空)/ airdrops/calendar '{"year":2026,"month":5}' |
| 上市公司持币(国库) | treasuries/summary '{"coin_key":"bitcoin"}' / 实体 treasuries/entities '{"coin_key":"bitcoin"}' |
| 灰度持仓 | institutions/grayscale/holdings |
| 加密概念股 / 全球股指 | equities/crypto-exposure/quotes / macro/stock-indices |
| 指数(恐慌贪婪等) | indexes / indexes/ticker '{"index_key":"i:fgi:alternative"}' |
| 异动信号 / 预警 | signals/changes / signals/alerts |
| AI 选币推荐 | coins/recommendations '{"coin_keys":["bitcoin"]}' |
其他接口都在 catalog 里 —— 查不到想要的就先跑 catalog,别猜路径。
几条要记住的
1. 不编数据。 永远跑脚本拿真实数据。data 为空 / ok:false 就如实告诉用户,不要编解释、不要编价格。 2. 加密数据只用这个脚本,不要用 web_search / web_fetch / curl / 浏览器去拼。 3. `coin_key` 还是 `coin`? 普通接口用 coin_key —— AiCoin 币种 slug,小写(bitcoin、ethereum);hyperliquid/* 接口用 coin —— 交易符号(BTC、HYPE)。每个参数照 catalog 里的 desc / example 填。 4. 详情接口的 id 先从列表接口拿:快讯/文章详情先 content/newsflashes、content/articles 取 id;空投/项目详情先 airdrops、drop-radar/projects 取 project_id。 5. `ok:false` + HTTP 403 = 当前 key 无此接口权限,别重试。先别断言"套餐不够":本地 host 常见坑是脚本 fallback 到了免费/旧 key —— 先 node scripts/aicoin.mjs key 看 key_id 是不是用户的专业版(key 应在 ~/.coinos/.env)。确属套餐不足,再告诉用户去 https://www.aicoin.com/opendata 升级。200 + 空 data 是"此条件下没数据",不是出错。 6. 时间用 Unix 毫秒(start_time / end_time),分页用 limit / offset。 7. 用用户的语言回复(中文提问就全程中文)。
API Key
内置一个免费 key,开箱即用,够查行情、K 线、币种、新闻这些。资金费率、大单、清算、HL 鲸鱼、国库等需要付费套餐。收到 403 先 `node scripts/aicoin.mjs key` 核对 key_id 是不是用户的专业版(key 应放 `~/.coinos/.env`),确认没加载错 key 再判断是否真套餐不够 —— 把"key 没加载对"误报成"接口要付费"会让付费用户暴怒。
用自己的 key —— 推荐 set-key 命令(会先验证再写入 .env,禁止手编 .env):
node scripts/aicoin.mjs set-key <key_id> <secret>
# 或直接把 AiCoin 后台 JSON 整段喂进来,脚本认字段名:
node scripts/aicoin.mjs set-key '{"api_key":"<id>","access_key":"<secret>"}'⚠️ AiCoin 后台字段名反直觉:JSON 里的 api_key 其实是公开 ID(对应 AICOIN_ACCESS_KEY_ID),access_key 才是 SECRET(对应 AICOIN_ACCESS_SECRET)。脚本帮你按这个映射存,不用人脑反向。
环境变量名(写 .env 或 export 用):
AICOIN_ACCESS_KEY_ID=... # = AiCoin 后台的 api_key
AICOIN_ACCESS_SECRET=... # = AiCoin 后台的 access_key安全说明:AiCoin API Key 只用于获取市场数据,无法交易、无法读取你在交易所的任何信息。所有密钥只存在本地 .env,不上传任何服务器。CoinClaw 用户在 web UI 的 EnvSection 里配置。
跨 skill
| 需求 | 用哪个 skill |
|---|---|
| Hyperliquid 鲸鱼 / 聪明钱 / 链上大资金 | aicoin-hyperliquid |
| 交易所下单 / 平仓 / 查余额 | aicoin-trading |
| Freqtrade 策略 / 回测 / 部署 | aicoin-freqtrade |
| 链上 DEX swap / 钱包 / token | aicoin-onchain |
#!/usr/bin/env node
// AiCoin Open Data v3 API client — HMAC-SHA1 signed, header auth.
// One unified envelope {ok, data, error, meta}; see catalog for all endpoints.
import { createHmac, randomBytes } from 'node:crypto';
import { readFileSync, existsSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { loadEnv, writeEnvPath } from './env-loader.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
// .env auto-load (宿主可能不向子进程注入 env)。共享 loader,见 lib/env-loader.mjs。
loadEnv();
const defaults = JSON.parse(readFileSync(resolve(__dirname, 'defaults.json'), 'utf-8'));
export const BASE = process.env.AICOIN_BASE_URL || 'https://open.aicoin.com';
export const KEY = process.env.AICOIN_ACCESS_KEY_ID || defaults.accessKeyId;
const SECRET = process.env.AICOIN_ACCESS_SECRET || defaults.accessSecret;
export const USING_OWN_KEY = !!(process.env.AICOIN_ACCESS_KEY_ID && process.env.AICOIN_ACCESS_SECRET);
// HMAC-SHA1(signStr, secret) → hex → base64. The 4 values ride in X-Aic-* headers.
function authHeaders(keyId = KEY, secret = SECRET) {
const nonce = randomBytes(8).toString('hex');
const ts = Math.floor(Date.now() / 1000).toString();
const signStr = `AccessKeyId=${keyId}&SignatureNonce=${nonce}&Timestamp=${ts}`;
const hex = createHmac('sha1', secret).update(signStr).digest('hex');
return {
'X-Aic-AccessKey-Id': keyId,
'X-Aic-Signature-Nonce': nonce,
'X-Aic-Timestamp': ts,
'X-Aic-Signature': Buffer.from(hex).toString('base64'),
};
}
// Normalize a user-supplied endpoint to a full /api/v3/... path.
// "market/ticker" / "/market/ticker" / "/api/v3/market/ticker" → "/api/v3/market/ticker"
export function normalizePath(ep) {
let p = String(ep || '').trim().replace(/^https?:\/\/[^/]+/, '');
if (p.startsWith('/api/v3/') || p === '/api/v3') return p;
p = p.replace(/^\/?(api\/v3\/?)?/, '');
return '/api/v3/' + p;
}
// endpoints.json — a bundled catalog snapshot. Drives GET/POST selection and
// offline `catalog`. Live catalog is still the source of truth (see fetchCatalog).
let _snapshot = null;
export function snapshotEndpoints() {
if (_snapshot) return _snapshot;
try {
const j = JSON.parse(readFileSync(resolve(__dirname, 'endpoints.json'), 'utf-8'));
_snapshot = j.endpoints || [];
} catch { _snapshot = []; }
return _snapshot;
}
// Pull the live catalog. Falls back to the bundled snapshot when offline.
export async function fetchCatalog() {
try {
const { httpStatus, body } = await request('GET', '/api/v3/_catalog');
if (httpStatus === 200 && body?.data?.endpoints) return { endpoints: body.data.endpoints, live: true };
} catch { /* fall through to snapshot */ }
return { endpoints: snapshotEndpoints(), live: false };
}
// Core request. Returns { httpStatus, body }; body is the parsed envelope.
export async function request(method, path, params = {}) {
const full = normalizePath(path);
const m = (method || 'GET').toUpperCase();
const headers = authHeaders();
let url = `${BASE}${full}`;
const init = { method: m, headers, signal: AbortSignal.timeout(30000) };
if (m === 'GET' || m === 'DELETE') {
const qs = new URLSearchParams();
for (const [k, v] of Object.entries(params || {})) {
if (v === undefined || v === null || v === '') continue;
qs.set(k, Array.isArray(v) ? v.join(',') : String(v));
}
const s = qs.toString();
if (s) url += `?${s}`;
} else {
headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(params || {});
}
const res = await fetch(url, init);
const text = await res.text();
let body;
try { body = JSON.parse(text); } catch { body = { ok: false, error: { code: 'bad_response', message: text.slice(0, 300) } }; }
// v3 business endpoints answer with {ok,...}. Auth / quota errors from the
// gateway are still legacy-shaped ({success:false,errorCode,error}); fold them
// into the same envelope so callers only ever branch on `ok`.
if (body && typeof body === 'object' && typeof body.ok !== 'boolean' && (res.status >= 400 || body.success === false)) {
body = {
ok: false,
data: null,
error: {
code: body.errorCode != null ? String(body.errorCode) : String(res.status),
message: body.error || body.message || body.msg || `HTTP ${res.status}`,
},
meta: {},
};
}
return { httpStatus: res.status, body };
}
// Resolve which HTTP method an endpoint uses, from snapshot then live catalog.
export async function resolveMethod(path) {
const full = normalizePath(path);
let hit = snapshotEndpoints().find(e => e.path === full);
if (hit) return { method: hit.method, spec: hit };
// 快照里没有 → 查 live catalog。
const { endpoints, live } = await fetchCatalog();
hit = endpoints.find(e => e.path === full);
if (hit) return { method: hit.method, spec: hit };
// live catalog 可达且确实无此端点 → 真·未知端点,硬失败。
if (live) return null;
// catalog 不可达(离线/抖动):别误杀可能有效的新端点 —— 默认按 GET 尝试,
// 让请求本身的 HTTP 状态(404/405)做最终判定,而不是 pre-flight 拒绝。
return { method: 'GET', spec: null, _assumed: true, _note: 'catalog unreachable; assuming GET' };
}
// 给"时序数组"返回算一个**跟数组正序/倒序无关**的最新值,防止 agent 用 tail / arr[0]
// 猜错方向(history-long-ratio 等接口曾因倒序被 tail 读到 2 天前旧值,误判"大户边际加空")。
// 找一个所有元素都有、能解析成时间戳(秒/毫秒 epoch 或可解析日期串)的字段, latest=时间戳最大那条。
// 识别不出时间字段就返回 null(不附 _timeseries, 退回 SKILL.md 文档约定)。
export function summarizeTimeseries(arr) {
if (!Array.isArray(arr) || arr.length < 2) return null;
if (!arr.every(e => e && typeof e === 'object' && !Array.isArray(e))) return null;
const PREF = ['timestamp', 'time', 'ts', 't', 'date', 'datetime', 'create_time', 'created_at', 'update_time', 'updated_at'];
const toTs = (v) => {
if (typeof v === 'number' && isFinite(v)) {
if (v >= 1e12 && v < 4e12) return v; // 毫秒 epoch
if (v >= 1e9 && v < 4e9) return v * 1000; // 秒 epoch
return null;
}
if (typeof v === 'string') {
if (/^\d{13}$/.test(v)) return Number(v);
if (/^\d{10}$/.test(v)) return Number(v) * 1000;
const d = Date.parse(v);
return isNaN(d) ? null : d;
}
return null;
};
const keys = Object.keys(arr[0]);
// 只把"名字像时间"且值能解析成时间戳的字段当时间列 —— 否则十亿级的 volume/market_cap/OI、
// 万亿级的 total mcap 等纯数值列会被误判成 epoch,给排名/持仓类列表错附 _timeseries
// (latest 会指向数值最大那行而非最新)。名字 + 值双重 gate。
const isTimeName = (k) => {
const s = k.toLowerCase();
return PREF.includes(s) || /time|date/.test(s) || /_at$/.test(s) || s === 'ts' || s === 't';
};
const candidates = keys.filter(k => isTimeName(k) && arr.every(e => toTs(e[k]) != null));
if (!candidates.length) return null;
candidates.sort((a, b) => {
const ia = PREF.indexOf(a.toLowerCase()), ib = PREF.indexOf(b.toLowerCase());
return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib);
});
let field = null, order = 'unsorted';
for (const k of candidates) {
const ts = arr.map(e => toTs(e[k]));
let asc = true, desc = true;
for (let i = 1; i < ts.length; i++) { if (ts[i] < ts[i - 1]) asc = false; if (ts[i] > ts[i - 1]) desc = false; }
if (asc || desc) { field = k; order = asc ? 'ascending (最新在末尾)' : 'descending (最新在开头 arr[0])'; break; }
}
// 没有任何"时间列"单调 → 这多半不是真时序,而是带逐行时间戳的排名/快照列表(volume/OI/榜单等)。
// 此时 latest=时间戳最大那行会指向"最近更新的那一行"而非榜首,误导性强 —— 干脆不附 _timeseries,
// 退回 SKILL.md 文档约定。真正的时序数据一定按时间单调(asc/desc),会在上面命中。
if (!field) return null;
let li = 0, oi = 0, lv = toTs(arr[0][field]), ov = lv;
arr.forEach((e, i) => { const v = toTs(e[field]); if (v > lv) { lv = v; li = i; } if (v < ov) { ov = v; oi = i; } });
return {
count: arr.length, field, order, latest: { ...arr[li] }, oldest: { ...arr[oi] },
_note: '_timeseries.latest = 时间戳最大那条(与数组顺序无关);取"最新/当前"值用它,别默认数组末尾或开头。做"边际变化/趋势"用 latest vs oldest 或自行按 field 排序。',
};
}
// Persist a new key pair to the workspace .env (validates before writing).
export async function saveKey(keyId, secret) {
const headers = authHeaders(keyId, secret);
const res = await fetch(`${BASE}/api/v3/coins/tickers?coin_key=bitcoin`, { headers, signal: AbortSignal.timeout(15000) });
if (res.status === 401 || res.status === 403) return { ok: false, error: `key 验证失败 (HTTP ${res.status})` };
if (!res.ok) return { ok: false, error: `验证请求失败 (HTTP ${res.status})` };
const target = writeEnvPath();
let lines = existsSync(target) ? readFileSync(target, 'utf-8').split('\n') : [];
const set = (k, v) => {
const i = lines.findIndex(l => l.trim().startsWith(k + '='));
if (i >= 0) lines[i] = `${k}=${v}`; else lines.push(`${k}=${v}`);
};
set('AICOIN_ACCESS_KEY_ID', keyId);
set('AICOIN_ACCESS_SECRET', secret);
try { mkdirSync(dirname(target), { recursive: true }); } catch {}
writeFileSync(target, lines.join('\n'));
try { chmodSync(target, 0o600); } catch {}
return { ok: true, file: target };
}
{
"comment": "Public free-tier AiCoin API key. IP rate-limited. Users can replace with their own key via env vars.",
"accessKeyId": "ronJ8uI0Yj2soAfGVs5H1YALUIINbE22",
"accessSecret": "CWHZcH2us1CLSE7grroR1TpS0Z1JxTwU"
}
// Shared .env auto-loader for coinos-skills.
// 各 skill 自包含 → 本文件在每个 skill 的 lib/ 下保留一份**字节相同**的副本,
// 由 scripts/validate-skills.mjs 的 drift guard 强制一致(改一处必须同步全部)。
//
// key 的规范存放位置(coinos 文件夹),不再靠"向上爬目录找 .env"的启发式:
// - macOS / Linux: ~/.coinos/.env
// - Windows: %USERPROFILE%\.coinos\.env
// - CoinClaw 容器: /workspace/.env (产品 web UI EnvSection → entrypoint 注入, 保留)
// 另外也读: 当前目录 .env(临时/项目本地)+ 旧引擎位置(~/.openclaw 等, 向后兼容, 最低优先级)。
//
// 规则: 候选按下面顺序, 同一个 key 先命中者生效; 已注入的 env(process.env)永远优先
// (if (!process.env[k]) 守卫)。所以把 key 放进 ~/.coinos/.env 后, 旧的 ~/.openclaw
// 免费 key 不会再抢 —— 它排在后面, 对应的 key 已经先被填上了。
import { readFileSync, existsSync } from 'node:fs';
import { resolve, join } from 'node:path';
const HOME = process.env.HOME || process.env.USERPROFILE || '';
// CoinClaw 容器 sentinel → 产品注入的 /workspace/.env。
function containerEnvFile() {
if (existsSync('/workspace/.hermes') || existsSync('/workspace/.claude')) return '/workspace/.env';
if (existsSync('/home/node/.openclaw')) return '/home/node/.openclaw/workspace/.env';
return null;
}
// coinos 规范配置文件 —— 跨平台 ~/.coinos/.env(Windows: %USERPROFILE%\.coinos\.env)。
export function coinosEnvFile() {
return HOME ? join(HOME, '.coinos', '.env') : null;
}
// 候选 .env 路径(有序;同一个 key 先命中者生效,且注入 env 永远优先)。
export function envCandidates() {
const list = [];
const container = containerEnvFile();
if (container) list.push(container); // 1. 容器: 产品注入位置
const coinos = coinosEnvFile();
if (coinos) list.push(coinos); // 2. ~/.coinos/.env —— 规范位置
list.push(resolve(process.cwd(), '.env')); // 3. 当前目录(临时/项目本地)
if (HOME) { // 4. 旧引擎位置, 向后兼容(最低优先级)
list.push(resolve(HOME, '.openclaw', 'workspace', '.env'));
list.push(resolve(HOME, '.openclaw', '.env'));
list.push(resolve(HOME, '.hermes', '.env'));
}
return [...new Set(list)];
}
// 把候选 .env 载入 process.env,不覆盖已注入的变量。
export function loadEnv() {
for (const envFile of envCandidates()) {
try {
for (const line of readFileSync(envFile, 'utf-8').split('\n')) {
const t = line.trim();
if (!t || t.startsWith('#')) continue;
const eq = t.indexOf('=');
if (eq < 1) continue;
const k = t.slice(0, eq).trim();
let v = t.slice(eq + 1).trim();
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
if (!process.env[k]) process.env[k] = v;
}
} catch { /* 文件不存在或不可读,跳过 */ }
}
}
// saveKey 应写入的 .env 路径 —— 规范位置 ~/.coinos/.env(容器内写 /workspace/.env)。
// 调用方写入前需 mkdir -p 父目录(~/.coinos 可能还不存在)。
export function writeEnvPath() {
const container = containerEnvFile();
if (container) return container;
const coinos = coinosEnvFile();
if (coinos) return coinos;
return resolve(process.cwd(), '.env');
}
{
"name": "aicoin-market",
"version": "4.0.0",
"private": true,
"type": "module"
}
#!/usr/bin/env node
// AiCoin Open Data v3 — one entry point for every endpoint.
//
// node scripts/aicoin.mjs <endpoint> ['<json params>'] call any v3 endpoint
// node scripts/aicoin.mjs catalog [group|endpoint] list endpoints (the live API menu)
// node scripts/aicoin.mjs key show API key status + access probe
// node scripts/aicoin.mjs set-key <id> <secret> validate & save a new key to .env
//
// Endpoint = the path after /api/v3/ , e.g. market/ticker , hyperliquid/whales/open-positions
// Every call prints the v3 envelope {ok, data, error, meta}. Check `ok` first.
import { request, resolveMethod, fetchCatalog, saveKey, summarizeTimeseries, KEY, USING_OWN_KEY } from '../lib/client.mjs';
const out = (o) => console.log(JSON.stringify(o, null, 2));
const groupOf = (p) => p.replace(/^\/api\/v3\//, '').split('/')[0] || '_catalog';
const rel = (p) => p.replace(/^\/api\/v3\//, '');
const HINTS = {
401: 'HTTP 401 — 签名或鉴权失败,检查 API key 是否正确。',
403: 'HTTP 403 — 此接口当前 key 无权限。**先别断言「套餐不够」**:本地 host 最常见的坑是脚本 fallback 到了免费/旧 key —— 跑 `node scripts/aicoin.mjs key` 看 key_id 是不是你的专业版(key 应放 ~/.coinos/.env)。确属套餐不足,再让用户去 https://www.aicoin.com/opendata 升级。不要重试。',
404: 'HTTP 404 — 资源不存在,检查 id / 参数是否对。',
429: 'HTTP 429 — 触发限流,等 30-60 秒再试,或把多个查询合并成一次。',
500: 'HTTP 500 — 服务端/上游故障,可隔 1-2 分钟重试;持续失败请联系 service@aicoin.com。',
501: 'HTTP 501 — 该接口尚未实现(数据源未接通),换其他接口。',
502: 'HTTP 502 — 网关临时故障,隔 1-2 分钟重试。',
503: 'HTTP 503 — 服务暂时不可用,稍后重试。',
504: 'HTTP 504 — 网关超时,稍后重试。',
};
async function callEndpoint(endpoint, rawParams) {
let params = {};
if (rawParams) {
try { params = JSON.parse(rawParams); }
catch { return out({ ok: false, error: { code: 'bad_params', message: `参数不是合法 JSON: ${rawParams}` }, _hint: "参数要用 JSON 对象,例: '{\"coin_key\":\"bitcoin\",\"market\":\"binance\"}'" }); }
}
const resolved = await resolveMethod(endpoint);
if (!resolved) {
return out({ ok: false, error: { code: 'unknown_endpoint', message: `未知接口: ${endpoint}` }, _hint: '跑 `node scripts/aicoin.mjs catalog` 看全部接口。' });
}
let res;
try { res = await request(resolved.method, endpoint, params); }
catch (e) { return out({ ok: false, error: { code: 'network', message: e.message }, _hint: '网络/超时错误,稍后重试。' }); }
const body = (res.body && typeof res.body === 'object') ? res.body : { raw: res.body };
if (res.httpStatus !== 200 && HINTS[res.httpStatus]) body._hint = HINTS[res.httpStatus];
// 时序数组: 附一个 order-independent 的 latest, 防 agent 用 tail/arr[0] 猜错方向。
if (body && body.ok) {
let series = Array.isArray(body.data) ? body.data : null;
let where = 'data';
if (!series && body.data && typeof body.data === 'object') {
for (const k of ['list', 'items', 'records', 'rows', 'data']) {
if (Array.isArray(body.data[k])) { series = body.data[k]; where = `data.${k}`; break; }
}
}
if (series) { const ts = summarizeTimeseries(series); if (ts) body._timeseries = { in: where, ...ts }; }
}
out(body);
}
async function showCatalog(filter) {
const { endpoints, live } = await fetchCatalog();
endpoints.sort((a, b) => a.path.localeCompare(b.path));
if (filter) {
// Group match wins over a same-named bare endpoint (e.g. `indexes` is both
// the group and the path /api/v3/indexes) — the group view is more useful.
const inGroup = endpoints.filter((e) => groupOf(e.path) === filter);
if (inGroup.length) {
const lines = [`# ${filter} (${inGroup.length} 个接口) 来源: ${live ? '线上' : '本地快照'}\n`];
for (const e of inGroup) {
lines.push(`${e.method} ${rel(e.path)} — ${e.summary || ''}`);
for (const p of e.params || []) {
const bits = [p.in, p.required ? '必填' : '可选', p.type];
if (p.enum) bits.push('枚举:' + p.enum.join('/'));
if (p.example) bits.push('例:' + p.example);
lines.push(` ${p.name} (${bits.filter(Boolean).join(', ')}) ${p.desc || ''}`);
}
lines.push('');
}
return console.log(lines.join('\n'));
}
// Not a group — treat the filter as a single endpoint path.
const exact = endpoints.find((e) => rel(e.path) === filter || e.path === filter);
if (exact) return out({ source: live ? 'live' : 'snapshot', endpoint: exact });
return out({ ok: false, error: { code: 'no_match', message: `没有 "${filter}" 分组或接口` }, _hint: '不带参数跑 catalog 看全部分组。' });
}
// Full table of contents — grouped, paths + summaries (no params).
const groups = {};
for (const e of endpoints) (groups[groupOf(e.path)] ||= []).push(e);
const lines = [
`# AiCoin v3 接口清单 — ${endpoints.length} 个,${Object.keys(groups).length} 个分组 (来源: ${live ? '线上' : '本地快照'})`,
`# 看某分组的参数: node scripts/aicoin.mjs catalog <分组名>`,
`# 调用: node scripts/aicoin.mjs <接口> '<JSON 参数>'\n`,
];
for (const [g, es] of Object.entries(groups)) {
lines.push(`### ${g} (${es.length})`);
for (const e of es) lines.push(` ${e.method.padEnd(4)} ${rel(e.path).padEnd(46)} ${e.summary || ''}`);
lines.push('');
}
console.log(lines.join('\n'));
}
async function showKey() {
const probes = [
['coins/tickers', { coin_key: 'bitcoin' }],
['derivatives/funding-rates', { coin_key: 'bitcoin', market: 'binance' }],
['market/big-orders', { coin_key: 'bitcoin', market: 'binance' }],
['hyperliquid/whales/open-positions', { coin: 'BTC' }],
['treasuries/summary', { coin_key: 'bitcoin' }],
];
const access = [];
for (const [ep, params] of probes) {
try {
const { httpStatus, body } = await request('GET', ep, params);
access.push({ endpoint: ep, http: httpStatus, ok: body?.ok === true });
} catch (e) { access.push({ endpoint: ep, error: e.message }); }
}
out({
key_id: KEY ? KEY.slice(0, 6) + '…' : null,
source: USING_OWN_KEY ? '用户自己的 key (.env)' : '内置免费 key',
access,
note: '某个接口 http=403 表示当前套餐不覆盖它;要更多权限去 https://www.aicoin.com/opendata。',
});
}
const USAGE = `AiCoin Open Data v3
node scripts/aicoin.mjs <接口> '<JSON参数>' 调任意 v3 接口,例:
node scripts/aicoin.mjs market/ticker '{"coin_key":"bitcoin","market":"binance"}'
node scripts/aicoin.mjs coins/tickers '{"coin_key":"bitcoin,ethereum"}'
node scripts/aicoin.mjs hyperliquid/whales/open-positions '{"coin":"BTC"}'
node scripts/aicoin.mjs catalog [分组] 看接口清单(不确定接口/参数时先跑这个)
node scripts/aicoin.mjs key 看 key 状态 + 权限探测
node scripts/aicoin.mjs set-key <id> <secret> 校验并保存新 key`;
const [cmd, ...rest] = process.argv.slice(2);
(async () => {
if (!cmd || cmd === 'help' || cmd === '-h' || cmd === '--help') return console.log(USAGE);
if (cmd === 'catalog') return showCatalog(rest[0]);
if (cmd === 'key') return showKey();
if (cmd === 'set-key') {
let id, secret;
const raw = rest.join(' ').trim();
if (raw.startsWith('{')) {
// JSON 模式:兼容 AiCoin 后台直接拷下来的 {"api_key","access_key"}
// 注意 AiCoin 后台命名反直觉 —— `api_key` 是公开 ID,`access_key` 才是 SECRET。
// 也兼容 {"access_key_id","access_secret"} 等更直白的命名。
try {
const j = JSON.parse(raw);
id = j.access_key_id || j.accessKeyId || j.key_id || j.api_key || j.key;
secret = j.access_secret || j.accessSecret || j.secret_key || j.secret || j.access_key;
} catch {
return out({ ok: false, error: { code: 'bad_json', message: '参数不是合法 JSON' } });
}
} else if (rest.length >= 2) {
id = rest[0];
secret = rest[1];
}
if (!id || !secret) {
return out({ ok: false, error: { code: 'bad_args', message: "用法: set-key <key_id> <secret> 或 set-key '<json>'(JSON 字段名兼容 api_key/access_key、access_key_id/access_secret 等;AiCoin 后台 api_key 是 ID、access_key 是 SECRET)" } });
}
const r = await saveKey(id, secret);
return out(r.ok ? { ok: true, message: `key 已保存到 ${r.file}` } : { ok: false, error: { code: 'invalid_key', message: r.error } });
}
return callEndpoint(cmd, rest.join(' ').trim() || null);
})().catch((e) => { out({ ok: false, error: { code: 'fatal', message: e.message } }); process.exit(1); });
Related skills
How it compares
Use aicoin-market for signed AiCoin v3 crypto exchange data in Node agents; choose exchange-specific REST SDKs when only one venue's private trading API is required.
FAQ
What API version does aicoin-market use?
aicoin-market targets the AiCoin Open Data v3 API. Requests are signed with HMAC-SHA1 header authentication, and responses follow a unified envelope containing ok, data, error, and meta fields.
Where does aicoin-market load API keys from?
aicoin-market auto-loads credentials from .env files in the current working directory and common OpenClaw workspace paths. This helps child processes receive API keys when the parent runtime does not inject environment variables.
Is Aicoin Market safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.