@aicoin/aicoin-mcp 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -7
  2. package/build/index.js +142 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@ AiCoin MCP Server — real-time crypto market data & exchange trading for any AI
6
6
 
7
7
  ## Quick Start
8
8
 
9
- Works out of the box — a free API key is built in (10 req/min, IP rate-limited).
9
+ Works out of the box — a free API key is built in (15 req/min, IP rate-limited).
10
10
 
11
11
  ### Claude Code
12
12
 
@@ -137,12 +137,13 @@ Exchange API keys are stored locally only and never sent to AiCoin servers.
137
137
 
138
138
  ## API Tiers
139
139
 
140
- | Tier | Price | Rate Limit | Features |
141
- |------|-------|------------|----------|
142
- | Basic | Free | 10 req/min | Market data, news, K-lines, indexes |
143
- | Normal | ¥99/mo | 60 req/min | + Funding rate, liquidation, OI |
144
- | Premium | ¥299/mo | 120 req/min | + Depth, whale tracking, order flow |
145
- | Professional | ¥999/mo | 300 req/min | + Full depth, all features |
140
+ | Tier | Price | Rate Limit | Monthly Quota | Features |
141
+ |------|-------|------------|---------------|----------|
142
+ | Free | $0 | 15 req/min | 20K | Market, coin & special data |
143
+ | Basic | $29/mo | 30 req/min | 20K | + Content data |
144
+ | Standard | $79/mo | 80 req/min | 500K | + Content data |
145
+ | Advanced | $299/mo | 300 req/min | 1.5M | + Content data, commercial use |
146
+ | Professional | $699/mo | 1200 req/min | 3.5M | + Content data, commercial use |
146
147
 
147
148
  Upgrade at [aicoin.com/opendata](https://www.aicoin.com/opendata). Your existing key is automatically upgraded.
148
149
 
package/build/index.js CHANGED
@@ -741,7 +741,7 @@ function generateSignature(accessKeyId, accessSecret) {
741
741
  // src/client/api.ts
742
742
  var DOMAIN = process.env.AICOIN_BASE_URL || "https://open.aicoin.com";
743
743
  var TIMEOUT_MS = 3e4;
744
- var UPGRADE_GUIDE = "\n\n--- How to fix ---\nYour current API tier does not have access to this endpoint. Please upgrade your plan.\n1. Visit https://www.aicoin.com/opendata to purchase or upgrade\n2. Tiers: Basic(free) | Normal(\xA599) | Premium(\xA5299) | Professional(\xA5999)\n3. Set AICOIN_ACCESS_KEY_ID and AICOIN_ACCESS_SECRET in your MCP config\n4. Restart your MCP client";
744
+ var UPGRADE_GUIDE = "\n\n--- How to fix ---\nYour current API tier does not have access to this endpoint. Please upgrade your plan.\n1. Visit https://www.aicoin.com/opendata to purchase or upgrade\n2. Tiers: Free($0) | Basic($29/mo) | Standard($79/mo) | Advanced($299/mo) | Professional($699/mo)\n3. Set AICOIN_ACCESS_KEY_ID and AICOIN_ACCESS_SECRET in your MCP config\n4. Restart your MCP client";
745
745
  var AUTH_GUIDE = "\n\n--- How to fix ---\nAn API key is required to access this endpoint.\n1. Visit https://www.aicoin.com/opendata to register and create an API key\n2. Set AICOIN_ACCESS_KEY_ID and AICOIN_ACCESS_SECRET in your MCP config\n3. Restart your MCP client";
746
746
  function throwApiError(status, body, path) {
747
747
  let msg = `API ${status}: ${body}`;
@@ -1109,6 +1109,72 @@ function registerContentTools(server2) {
1109
1109
  }
1110
1110
  }
1111
1111
  );
1112
+ server2.tool(
1113
+ "twitter",
1114
+ "Twitter/X crypto tweets.\n\u2022 latest \u2014 latest crypto tweets, cursor-paginated\n\u2022 search \u2014 search tweets by keyword. Requires: keyword\n\u2022 members \u2014 search Twitter KOL/users. Requires: word\n\u2022 interaction_stats \u2014 tweet engagement stats. Requires: flash_ids (POST)",
1115
+ {
1116
+ action: z4.enum(["latest", "search", "members", "interaction_stats"]).describe(
1117
+ "latest: latest crypto tweets; search: search by keyword; members: search KOL/users; interaction_stats: tweet engagement"
1118
+ ),
1119
+ keyword: z4.string().optional().describe("REQUIRED for search. Search keyword"),
1120
+ word: z4.string().optional().describe("REQUIRED for members. User search keyword"),
1121
+ flash_ids: z4.string().optional().describe(
1122
+ 'REQUIRED for interaction_stats. Comma-separated tweet flash IDs, e.g. "123,456,789" (max 50)'
1123
+ ),
1124
+ language: z4.string().optional().describe("For latest/search: language filter (cn, en)"),
1125
+ last_time: z4.string().optional().describe("For latest/search: cursor for pagination (last item timestamp)"),
1126
+ page_size: z4.string().optional().describe("For latest/search: page size, default 20"),
1127
+ page: z4.string().optional().describe("For members: page number, default 1"),
1128
+ size: z4.string().optional().describe("For members: page size, default 20"),
1129
+ ...maxItemsParam
1130
+ },
1131
+ async ({ action, keyword, word, flash_ids, language, last_time, page_size, page, size, _max_items }) => {
1132
+ try {
1133
+ switch (action) {
1134
+ case "latest": {
1135
+ const params = {};
1136
+ if (language) params.language = language;
1137
+ if (last_time) params.last_time = last_time;
1138
+ if (page_size) params.page_size = page_size;
1139
+ return okList(
1140
+ await apiGet("/api/upgrade/v2/content/twitter/latest", params),
1141
+ parseMax(_max_items, 20)
1142
+ );
1143
+ }
1144
+ case "search": {
1145
+ if (!keyword) return err("keyword is required for search action");
1146
+ const params = { keyword };
1147
+ if (language) params.language = language;
1148
+ if (last_time) params.last_time = last_time;
1149
+ if (page_size) params.page_size = page_size;
1150
+ return okList(
1151
+ await apiGet("/api/upgrade/v2/content/twitter/search", params),
1152
+ parseMax(_max_items, 20)
1153
+ );
1154
+ }
1155
+ case "members": {
1156
+ if (!word) return err("word is required for members action");
1157
+ const params = { word };
1158
+ if (page) params.page = page;
1159
+ if (size) params.size = size;
1160
+ return okList(
1161
+ await apiGet("/api/upgrade/v2/content/twitter/members", params),
1162
+ parseMax(_max_items, 20)
1163
+ );
1164
+ }
1165
+ case "interaction_stats": {
1166
+ if (!flash_ids) return err("flash_ids is required for interaction_stats action");
1167
+ const ids = flash_ids.split(",").map((s) => Number(s.trim())).filter((n) => !isNaN(n));
1168
+ if (ids.length === 0) return err("flash_ids must contain valid numeric IDs");
1169
+ if (ids.length > 50) return err("flash_ids max 50 items");
1170
+ return ok(await apiPost("/api/upgrade/v2/content/twitter/interaction-stats", { flash_ids: ids }));
1171
+ }
1172
+ }
1173
+ } catch (e) {
1174
+ return err(e);
1175
+ }
1176
+ }
1177
+ );
1112
1178
  }
1113
1179
 
1114
1180
  // src/tools/markets.ts
@@ -1632,15 +1698,17 @@ function registerHyperliquidTools(server2) {
1632
1698
  );
1633
1699
  server2.tool(
1634
1700
  "hl_liquidation",
1635
- "Hyperliquid liquidation data. All actions support optional coin filter.\n\u2022 history \u2014 liquidation history\n\u2022 stats \u2014 aggregate stats\n\u2022 stats_by_coin \u2014 per-coin stats\n\u2022 top_positions \u2014 top liquidated positions",
1701
+ "Hyperliquid liquidation data. All actions support optional coin filter.\n\u2022 history \u2014 liquidation history\n\u2022 stats \u2014 aggregate stats\n\u2022 stats_by_coin \u2014 per-coin stats\n\u2022 top_positions \u2014 top liquidated positions. Requires: coin + interval",
1636
1702
  {
1637
1703
  action: z7.enum(["history", "stats", "stats_by_coin", "top_positions"]).describe(
1638
1704
  "history: liquidation history; stats: aggregate stats; stats_by_coin: per-coin stats; top_positions: top liquidated"
1639
1705
  ),
1640
- coin: z7.string().optional().describe("Optional. Coin filter in uppercase, e.g. BTC"),
1706
+ coin: z7.string().optional().describe("Optional. Coin filter in uppercase, e.g. BTC. REQUIRED for top_positions"),
1707
+ interval: z7.string().optional().describe("REQUIRED for top_positions. Interval: 4h, 1d"),
1708
+ limit: z7.string().optional().describe("Optional for top_positions. Max results"),
1641
1709
  ...maxItemsParam
1642
1710
  },
1643
- async ({ action, coin, _max_items }) => {
1711
+ async ({ action, coin, interval, limit, _max_items }) => {
1644
1712
  try {
1645
1713
  const params = {};
1646
1714
  if (coin) params.coin = coin;
@@ -1654,11 +1722,16 @@ function registerHyperliquidTools(server2) {
1654
1722
  return ok(await apiGet("/api/upgrade/v2/hl/liquidations/stat"));
1655
1723
  case "stats_by_coin":
1656
1724
  return ok(await apiGet("/api/upgrade/v2/hl/liquidations/stat-by-coin", params));
1657
- case "top_positions":
1725
+ case "top_positions": {
1726
+ if (!coin) return err("coin is required for top_positions action");
1727
+ if (!interval) return err('interval is required for top_positions action (e.g. "4h", "1d")');
1728
+ const tpParams = { coin, interval };
1729
+ if (limit) tpParams.limit = limit;
1658
1730
  return okList(
1659
- await apiGet("/api/upgrade/v2/hl/liquidations/top-positions", params),
1731
+ await apiGet("/api/upgrade/v2/hl/liquidations/top-positions", tpParams),
1660
1732
  parseMax(_max_items, 50)
1661
1733
  );
1734
+ }
1662
1735
  }
1663
1736
  } catch (e) {
1664
1737
  return err(e);
@@ -1799,7 +1872,7 @@ function registerHyperliquidTools(server2) {
1799
1872
  );
1800
1873
  server2.tool(
1801
1874
  "hl_fills",
1802
- "Hyperliquid trade fills.\n\u2022 by_address \u2014 fills by wallet. Requires: address\n\u2022 by_oid \u2014 fills by order ID. Requires: oid\n\u2022 by_twapid \u2014 fills by TWAP ID. Requires: twapid\n\u2022 top_trades \u2014 top trades, no required params",
1875
+ "Hyperliquid trade fills.\n\u2022 by_address \u2014 fills by wallet. Requires: address\n\u2022 by_oid \u2014 fills by order ID. Requires: oid\n\u2022 by_twapid \u2014 fills by TWAP ID. Requires: twapid\n\u2022 top_trades \u2014 top trades. Requires: interval + coin",
1803
1876
  {
1804
1877
  action: z7.enum(["by_address", "by_oid", "by_twapid", "top_trades"]).describe(
1805
1878
  "by_address: fills by wallet; by_oid: fills by order ID; by_twapid: fills by TWAP ID; top_trades: top trades"
@@ -1807,8 +1880,8 @@ function registerHyperliquidTools(server2) {
1807
1880
  address: z7.string().optional().describe("REQUIRED for by_address. Wallet address (0x...)"),
1808
1881
  oid: z7.string().optional().describe("REQUIRED for by_oid. Order ID"),
1809
1882
  twapid: z7.string().optional().describe("REQUIRED for by_twapid. TWAP ID"),
1810
- coin: z7.string().optional().describe("Optional. Coin filter in uppercase, e.g. BTC"),
1811
- interval: z7.string().optional().describe("Optional for top_trades. Interval: 4h, 1d"),
1883
+ coin: z7.string().optional().describe("Optional. Coin filter in uppercase, e.g. BTC. REQUIRED for top_trades"),
1884
+ interval: z7.string().optional().describe("REQUIRED for top_trades. Interval: 4h, 1d"),
1812
1885
  limit: z7.string().optional().describe("Max results"),
1813
1886
  ...maxItemsParam
1814
1887
  },
@@ -1834,9 +1907,9 @@ function registerHyperliquidTools(server2) {
1834
1907
  return ok(await apiGet(`/api/upgrade/v2/hl/fills/twapid/${twapid}`));
1835
1908
  }
1836
1909
  case "top_trades": {
1837
- const params = {};
1838
- if (coin) params.coin = coin;
1839
- if (interval) params.interval = interval;
1910
+ if (!coin) return err("coin is required for top_trades action");
1911
+ if (!interval) return err('interval is required for top_trades action (e.g. "4h", "1d")');
1912
+ const params = { coin, interval };
1840
1913
  if (limit) params.limit = limit;
1841
1914
  return okList(
1842
1915
  await apiGet("/api/upgrade/v2/hl/fills/top-trades", params),
@@ -1984,9 +2057,10 @@ function registerHyperliquidTools(server2) {
1984
2057
  window: z7.string().optional().describe('REQUIRED for portfolio ONLY. Time window: "day", "week", "month", "allTime". NOT used by max_drawdown/net_flow'),
1985
2058
  period: z7.string().optional().describe("Optional for pnls. Period in days: 0=allTime, 1, 7, 30"),
1986
2059
  days: z7.string().optional().describe("REQUIRED for max_drawdown, net_flow. Number of days: 7, 30, 90. NOT used by portfolio"),
2060
+ scope: z7.string().optional().describe('Optional for max_drawdown. Scope: "perp" (default), "all"'),
1987
2061
  ...maxItemsParam
1988
2062
  },
1989
- async ({ action, address, window: win, period, days, _max_items }) => {
2063
+ async ({ action, address, window: win, period, days, scope, _max_items }) => {
1990
2064
  try {
1991
2065
  switch (action) {
1992
2066
  case "portfolio": {
@@ -2006,7 +2080,8 @@ function registerHyperliquidTools(server2) {
2006
2080
  }
2007
2081
  case "max_drawdown": {
2008
2082
  if (!days) return err("days is required for max_drawdown action");
2009
- return ok(await apiGet(`/api/upgrade/v2/hl/max-drawdown/${address}`, { days }));
2083
+ const params = { days, scope: scope || "perp" };
2084
+ return ok(await apiGet(`/api/upgrade/v2/hl/max-drawdown/${address}`, params));
2010
2085
  }
2011
2086
  case "net_flow": {
2012
2087
  if (!days) return err("days is required for net_flow action");
@@ -2099,12 +2174,13 @@ Visit https://www.aicoin.com/opendata to manage your API keys.
2099
2174
  ## Step 3: Create API Key
2100
2175
  Click "Create Key" and select your desired tier:
2101
2176
 
2102
- | Tier | Price | Rate Limit | Features |
2103
- |------|-------|------------|----------|
2104
- | Basic | Free | 10 req/min | Market data, news, basic indicators |
2105
- | Normal | \xA599/mo | 60 req/min | + Funding rate, liquidation data |
2106
- | Premium | \xA5299/mo | 120 req/min | + Depth data, whale tracking |
2107
- | Professional | \xA5999/mo | 300 req/min | + Full depth, all features |
2177
+ | Tier | Price | Rate Limit | Monthly Quota | Features |
2178
+ |------|-------|------------|---------------|----------|
2179
+ | Free | $0 | 15 req/min | 20K | Market, coin & special data |
2180
+ | Basic | $29/mo | 30 req/min | 20K | + Content data |
2181
+ | Standard | $79/mo | 80 req/min | 500K | + Content data |
2182
+ | Advanced | $299/mo | 300 req/min | 1.5M | + Content data, commercial use |
2183
+ | Professional | $699/mo | 1200 req/min | 3.5M | + Content data, commercial use |
2108
2184
 
2109
2185
  ## Step 4: Configure MCP Server
2110
2186
  After creating the key, you will get two values:
@@ -2139,18 +2215,15 @@ ${endpoint ? `Endpoint: ${endpoint}` : ""}
2139
2215
 
2140
2216
  ## API Tier Comparison
2141
2217
 
2142
- | Feature | Basic (Free) | Normal (\xA599) | Premium (\xA5299) | Professional (\xA5999) |
2143
- |---------|:---:|:---:|:---:|:---:|
2144
- | Market tickers & K-lines | \u2705 | \u2705 | \u2705 | \u2705 |
2145
- | News & flash | \u2705 | \u2705 | \u2705 | \u2705 |
2146
- | Index data | \u2705 | \u2705 | \u2705 | \u2705 |
2147
- | Funding rate history | \u274C | \u2705 | \u2705 | \u2705 |
2148
- | Liquidation data | \u274C | \u2705 | \u2705 | \u2705 |
2149
- | OI history | \u274C | \u2705 | \u2705 | \u2705 |
2150
- | Whale order tracking | \u274C | \u274C | \u2705 | \u2705 |
2151
- | Depth data | \u274C | \u274C | \u2705 | \u2705 |
2152
- | Strategy signals | \u274C | \u274C | \u274C | \u2705 |
2153
- | Full depth & grouped | \u274C | \u274C | \u274C | \u2705 |
2218
+ | Feature | Free ($0) | Basic ($29) | Standard ($79) | Advanced ($299) | Professional ($699) |
2219
+ |---------|:---:|:---:|:---:|:---:|:---:|
2220
+ | Market data | \u2705 | \u2705 | \u2705 | \u2705 | \u2705 |
2221
+ | Coin data | \u2705 | \u2705 | \u2705 | \u2705 | \u2705 |
2222
+ | Special data | \u2705 | \u2705 | \u2705 | \u2705 | \u2705 |
2223
+ | Content data | \u274C | \u2705 | \u2705 | \u2705 | \u2705 |
2224
+ | Rate limit | 15/min | 30/min | 80/min | 300/min | 1200/min |
2225
+ | Monthly quota | 20K | 20K | 500K | 1.5M | 3.5M |
2226
+ | Commercial use | \u274C | \u274C | \u274C | \u2705 | \u2705 |
2154
2227
 
2155
2228
  ## How to Upgrade
2156
2229
  1. Visit https://www.aicoin.com/opendata
@@ -2289,6 +2362,21 @@ var FreqtradeClient = class {
2289
2362
  );
2290
2363
  }
2291
2364
  if (res.status === 503) {
2365
+ const endpoint = url.pathname.replace("/api/v1/", "");
2366
+ const devEndpoints = [
2367
+ "strategies",
2368
+ "strategy/",
2369
+ "available_pairs",
2370
+ "backtest",
2371
+ "pair_history",
2372
+ "freqaimodels"
2373
+ ];
2374
+ const isDev = devEndpoints.some((e) => endpoint.startsWith(e));
2375
+ if (isDev) {
2376
+ throw new Error(
2377
+ `This endpoint requires webserver mode (503). Freqtrade separates "trade" and "webserver" modes: strategy/backtest/candle-analysis endpoints are only available in webserver mode. To use this feature, restart Freqtrade with: freqtrade webserver --config user_data/config.json. Note: webserver mode and trade mode cannot run simultaneously.`
2378
+ );
2379
+ }
2292
2380
  throw new Error(
2293
2381
  `Bot is not in the correct state (503). This usually means the bot was started in webserver-only mode without a strategy. Use ft_info action=config to check the current state. If "state" is empty and "strategy" is null, restart Freqtrade with a strategy: freqtrade trade --strategy <name> --config user_data/config.json`
2294
2382
  );
@@ -3009,7 +3097,7 @@ ${roiStr}
3009
3097
  }
3010
3098
 
3011
3099
  // src/tools/freqtrade-dev.ts
3012
- import { readFile, writeFile, access, mkdir } from "fs/promises";
3100
+ import { readFile, writeFile, readdir, access, mkdir } from "fs/promises";
3013
3101
  import { join, dirname } from "path";
3014
3102
  import { fileURLToPath } from "url";
3015
3103
  var __pkgroot = join(dirname(fileURLToPath(import.meta.url)), "..");
@@ -3033,7 +3121,7 @@ async function fileExists(path) {
3033
3121
  function registerFreqtradeDevTools(server2) {
3034
3122
  server2.tool(
3035
3123
  "ft_backtest",
3036
- "Freqtrade backtesting.\n- start: start a backtest (requires strategy, optional params)\n- status: get current backtest progress/results\n- abort: abort running backtest\n- delete: clear backtest state\n- history: list all backtest history entries\n- history_result: get specific result (requires filename & strategy)",
3124
+ "Freqtrade backtesting.\n- start: start a backtest (requires strategy, optional params)\n- status: get current backtest progress/results\n- abort: abort running backtest\n- delete: clear backtest state\n- history: list all backtest history entries\n- history_result: get specific result (requires filename & strategy)\nNOTE: Requires Freqtrade in webserver mode (freqtrade webserver). Not available in trade mode.",
3037
3125
  {
3038
3126
  action: z10.enum(["start", "status", "abort", "delete", "history", "history_result"]).describe("Backtest action"),
3039
3127
  strategy: z10.string().optional().describe("Strategy class name (required for start & history_result)"),
@@ -3093,7 +3181,7 @@ function registerFreqtradeDevTools(server2) {
3093
3181
  );
3094
3182
  server2.tool(
3095
3183
  "ft_candles",
3096
- "Freqtrade K-line / candle data.\n- live: live candlestick data (pair + timeframe required)\n- analyzed: historical data with strategy indicators (pair + timeframe + strategy required)\n- available: list available pairs with downloaded data",
3184
+ 'Freqtrade K-line / candle data.\n- live: live candlestick data (pair + timeframe required)\n- analyzed: historical data with strategy indicators (pair + timeframe + strategy required)\n- available: list available pairs with downloaded data\nNOTE: "analyzed" and "available" require Freqtrade in webserver mode. "live" works in trade mode.',
3097
3185
  {
3098
3186
  action: z10.enum(["live", "analyzed", "available"]).describe("Candle data query type"),
3099
3187
  pair: z10.string().optional().describe("Trading pair, e.g. BTC/USDT (for live & analyzed)"),
@@ -3164,13 +3252,28 @@ function registerFreqtradeDevTools(server2) {
3164
3252
  },
3165
3253
  async ({ action, strategy, name, template, timeframe, stoploss, roi, overwrite }) => {
3166
3254
  try {
3167
- const client = getFreqtradeClient();
3168
3255
  switch (action) {
3169
- case "list":
3170
- return ok(await client.strategies());
3256
+ case "list": {
3257
+ const userDataPath = getUserDataPath();
3258
+ const strategiesDir = join(userDataPath, "strategies");
3259
+ try {
3260
+ const files = await readdir(strategiesDir);
3261
+ const strategies = files.filter((f) => f.endsWith(".py") && !f.startsWith("__") && f !== "aicoin_provider.py").map((f) => f.replace(/\.py$/, ""));
3262
+ return ok({ strategies });
3263
+ } catch {
3264
+ return ok({ strategies: [], note: `No strategies directory found at ${strategiesDir}` });
3265
+ }
3266
+ }
3171
3267
  case "get": {
3172
3268
  if (!strategy) return err("strategy name is required for get action");
3173
- return ok(await client.strategy(strategy));
3269
+ const userDataPath = getUserDataPath();
3270
+ const filePath = join(userDataPath, "strategies", `${strategy}.py`);
3271
+ try {
3272
+ const code = await readFile(filePath, "utf-8");
3273
+ return ok({ strategy, code });
3274
+ } catch {
3275
+ return err(`Strategy file not found: ${filePath}`);
3276
+ }
3174
3277
  }
3175
3278
  case "create": {
3176
3279
  if (!name) return err("name is required for create action");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicoin/aicoin-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "AiCoin MCP Server - unified crypto market data & trading via AiCoin API + CCXT",
5
5
  "main": "build/index.js",
6
6
  "type": "module",