@okx_ai/okx-trade-mcp 1.2.2 → 1.2.3

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.
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { parseArgs } from "util";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
 
7
7
  // ../core/dist/index.js
8
+ import { ProxyAgent } from "undici";
8
9
  import { createHmac } from "crypto";
9
10
  import fs from "fs";
10
11
  import path from "path";
@@ -796,8 +797,10 @@ function sleep(ms) {
796
797
  var RateLimiter = class {
797
798
  buckets = /* @__PURE__ */ new Map();
798
799
  maxWaitMs;
799
- constructor(maxWaitMs = 3e4) {
800
+ verbose;
801
+ constructor(maxWaitMs = 3e4, verbose = false) {
800
802
  this.maxWaitMs = maxWaitMs;
803
+ this.verbose = verbose;
801
804
  }
802
805
  async consume(config, amount = 1) {
803
806
  const bucket = this.getBucket(config);
@@ -815,6 +818,10 @@ var RateLimiter = class {
815
818
  "Reduce tool call frequency or retry later."
816
819
  );
817
820
  }
821
+ if (this.verbose) {
822
+ process.stderr.write(`[verbose] \u23F3 rate-limit: waiting ${waitMs}ms for "${config.key}"
823
+ `);
824
+ }
818
825
  await sleep(waitMs);
819
826
  this.refill(bucket);
820
827
  if (bucket.tokens < amount) {
@@ -909,11 +916,38 @@ function buildQueryString(query) {
909
916
  }
910
917
  return params.toString();
911
918
  }
919
+ function maskKey(key) {
920
+ if (key.length <= 8) return "***";
921
+ return `${key.slice(0, 3)}***${key.slice(-3)}`;
922
+ }
923
+ function vlog(message) {
924
+ process.stderr.write(`[verbose] ${message}
925
+ `);
926
+ }
912
927
  var OkxRestClient = class {
913
928
  config;
914
- rateLimiter = new RateLimiter();
929
+ rateLimiter;
930
+ dispatcher;
915
931
  constructor(config) {
916
932
  this.config = config;
933
+ this.rateLimiter = new RateLimiter(3e4, config.verbose);
934
+ if (config.proxyUrl) {
935
+ this.dispatcher = new ProxyAgent(config.proxyUrl);
936
+ }
937
+ }
938
+ logRequest(method, url, auth) {
939
+ if (!this.config.verbose) return;
940
+ vlog(`\u2192 ${method} ${url}`);
941
+ const authInfo = auth === "private" && this.config.apiKey ? `auth=\u2713(${maskKey(this.config.apiKey)})` : `auth=${auth}`;
942
+ vlog(` ${authInfo} demo=${this.config.demo} timeout=${this.config.timeoutMs}ms`);
943
+ }
944
+ logResponse(status, rawLen, elapsed, traceId, code, msg) {
945
+ if (!this.config.verbose) return;
946
+ if (code && code !== "0" && code !== "1") {
947
+ vlog(`\u2717 ${status} | code=${code} | msg=${msg ?? "-"} | ${rawLen}B | ${elapsed}ms | trace=${traceId ?? "-"}`);
948
+ } else {
949
+ vlog(`\u2190 ${status} | code=${code ?? "0"} | ${rawLen}B | ${elapsed}ms | trace=${traceId ?? "-"}`);
950
+ }
917
951
  }
918
952
  async publicGet(path4, query, rateLimit) {
919
953
  return this.request({
@@ -942,126 +976,150 @@ var OkxRestClient = class {
942
976
  rateLimit
943
977
  });
944
978
  }
945
- async request(config) {
946
- const queryString = buildQueryString(config.query);
947
- const requestPath = queryString.length > 0 ? `${config.path}?${queryString}` : config.path;
948
- const url = `${this.config.baseUrl}${requestPath}`;
949
- const bodyJson = config.body ? JSON.stringify(config.body) : "";
950
- const timestamp = getNow();
951
- if (config.rateLimit) {
952
- await this.rateLimiter.consume(config.rateLimit);
953
- }
954
- const headers = new Headers({
955
- "Content-Type": "application/json",
956
- Accept: "application/json"
957
- });
958
- if (this.config.userAgent) {
959
- headers.set("User-Agent", this.config.userAgent);
960
- }
961
- if (config.auth === "private") {
962
- if (!this.config.hasAuth) {
963
- throw new ConfigError(
964
- "Private endpoint requires API credentials.",
965
- "Configure OKX_API_KEY, OKX_SECRET_KEY and OKX_PASSPHRASE."
966
- );
967
- }
968
- if (!this.config.apiKey || !this.config.secretKey || !this.config.passphrase) {
969
- throw new ConfigError(
970
- "Invalid private API credentials state.",
971
- "Ensure all OKX credentials are set."
972
- );
973
- }
974
- const payload = `${timestamp}${config.method.toUpperCase()}${requestPath}${bodyJson}`;
975
- const signature = signOkxPayload(payload, this.config.secretKey);
976
- headers.set("OK-ACCESS-KEY", this.config.apiKey);
977
- headers.set("OK-ACCESS-SIGN", signature);
978
- headers.set("OK-ACCESS-PASSPHRASE", this.config.passphrase);
979
- headers.set("OK-ACCESS-TIMESTAMP", timestamp);
979
+ setAuthHeaders(headers, method, requestPath, bodyJson, timestamp) {
980
+ if (!this.config.hasAuth) {
981
+ throw new ConfigError(
982
+ "Private endpoint requires API credentials.",
983
+ "Configure OKX_API_KEY, OKX_SECRET_KEY and OKX_PASSPHRASE."
984
+ );
980
985
  }
981
- if (this.config.demo) {
982
- headers.set("x-simulated-trading", "1");
986
+ if (!this.config.apiKey || !this.config.secretKey || !this.config.passphrase) {
987
+ throw new ConfigError(
988
+ "Invalid private API credentials state.",
989
+ "Ensure all OKX credentials are set."
990
+ );
983
991
  }
984
- let response;
985
- try {
986
- response = await fetch(url, {
987
- method: config.method,
988
- headers,
989
- body: config.method === "POST" ? bodyJson : void 0,
990
- signal: AbortSignal.timeout(this.config.timeoutMs)
991
- });
992
- } catch (error) {
993
- throw new NetworkError(
994
- `Failed to call OKX endpoint ${config.method} ${requestPath}.`,
995
- `${config.method} ${requestPath}`,
996
- error
992
+ const payload = `${timestamp}${method.toUpperCase()}${requestPath}${bodyJson}`;
993
+ const signature = signOkxPayload(payload, this.config.secretKey);
994
+ headers.set("OK-ACCESS-KEY", this.config.apiKey);
995
+ headers.set("OK-ACCESS-SIGN", signature);
996
+ headers.set("OK-ACCESS-PASSPHRASE", this.config.passphrase);
997
+ headers.set("OK-ACCESS-TIMESTAMP", timestamp);
998
+ }
999
+ throwOkxError(code, msg, reqConfig, traceId) {
1000
+ const message = msg || "OKX API request failed.";
1001
+ const endpoint = `${reqConfig.method} ${reqConfig.path}`;
1002
+ if (code === "50111" || code === "50112" || code === "50113") {
1003
+ throw new AuthenticationError(
1004
+ message,
1005
+ "Check API key, secret, passphrase and permissions.",
1006
+ endpoint,
1007
+ traceId
997
1008
  );
998
1009
  }
999
- const rawText = await response.text();
1000
- const traceId = extractTraceId(response.headers);
1010
+ const behavior = OKX_CODE_BEHAVIORS[code];
1011
+ const suggestion = behavior?.suggestion?.replace("{site}", this.config.site);
1012
+ if (code === "50011" || code === "50061") {
1013
+ throw new RateLimitError(message, suggestion, endpoint, traceId);
1014
+ }
1015
+ throw new OkxApiError(message, {
1016
+ code,
1017
+ endpoint,
1018
+ suggestion,
1019
+ traceId
1020
+ });
1021
+ }
1022
+ processResponse(rawText, response, elapsed, traceId, reqConfig, requestPath) {
1001
1023
  let parsed;
1002
1024
  try {
1003
1025
  parsed = rawText ? JSON.parse(rawText) : {};
1004
1026
  } catch (error) {
1027
+ this.logResponse(response.status, rawText.length, elapsed, traceId, "non-JSON");
1005
1028
  if (!response.ok) {
1006
1029
  const messagePreview = rawText.slice(0, 160).replace(/\s+/g, " ").trim();
1007
1030
  throw new OkxApiError(
1008
1031
  `HTTP ${response.status} from OKX: ${messagePreview || "Non-JSON response body"}`,
1009
1032
  {
1010
1033
  code: String(response.status),
1011
- endpoint: `${config.method} ${config.path}`,
1034
+ endpoint: `${reqConfig.method} ${reqConfig.path}`,
1012
1035
  suggestion: "Verify endpoint path and request parameters.",
1013
1036
  traceId
1014
1037
  }
1015
1038
  );
1016
1039
  }
1017
1040
  throw new NetworkError(
1018
- `OKX returned non-JSON response for ${config.method} ${requestPath}.`,
1019
- `${config.method} ${requestPath}`,
1041
+ `OKX returned non-JSON response for ${reqConfig.method} ${requestPath}.`,
1042
+ `${reqConfig.method} ${requestPath}`,
1020
1043
  error
1021
1044
  );
1022
1045
  }
1023
1046
  if (!response.ok) {
1047
+ this.logResponse(response.status, rawText.length, elapsed, traceId, parsed.code ?? "-", parsed.msg);
1024
1048
  throw new OkxApiError(
1025
1049
  `HTTP ${response.status} from OKX: ${parsed.msg ?? "Unknown error"}`,
1026
1050
  {
1027
1051
  code: String(response.status),
1028
- endpoint: `${config.method} ${config.path}`,
1052
+ endpoint: `${reqConfig.method} ${reqConfig.path}`,
1029
1053
  suggestion: "Retry later or verify endpoint parameters.",
1030
1054
  traceId
1031
1055
  }
1032
1056
  );
1033
1057
  }
1034
1058
  const responseCode = parsed.code;
1059
+ this.logResponse(response.status, rawText.length, elapsed, traceId, responseCode, parsed.msg);
1035
1060
  if (responseCode && responseCode !== "0" && responseCode !== "1") {
1036
- const message = parsed.msg || "OKX API request failed.";
1037
- const endpoint = `${config.method} ${config.path}`;
1038
- if (responseCode === "50111" || responseCode === "50112" || responseCode === "50113") {
1039
- throw new AuthenticationError(
1040
- message,
1041
- "Check API key, secret, passphrase and permissions.",
1042
- endpoint,
1043
- traceId
1044
- );
1045
- }
1046
- const behavior = OKX_CODE_BEHAVIORS[responseCode];
1047
- const suggestion = behavior?.suggestion?.replace("{site}", this.config.site);
1048
- if (responseCode === "50011" || responseCode === "50061") {
1049
- throw new RateLimitError(message, suggestion, endpoint, traceId);
1050
- }
1051
- throw new OkxApiError(message, {
1052
- code: responseCode,
1053
- endpoint,
1054
- suggestion,
1055
- traceId
1056
- });
1061
+ this.throwOkxError(responseCode, parsed.msg, reqConfig, traceId);
1057
1062
  }
1058
1063
  return {
1059
- endpoint: `${config.method} ${config.path}`,
1064
+ endpoint: `${reqConfig.method} ${reqConfig.path}`,
1060
1065
  requestTime: (/* @__PURE__ */ new Date()).toISOString(),
1061
1066
  data: parsed.data ?? null,
1062
1067
  raw: parsed
1063
1068
  };
1064
1069
  }
1070
+ async request(reqConfig) {
1071
+ const queryString = buildQueryString(reqConfig.query);
1072
+ const requestPath = queryString.length > 0 ? `${reqConfig.path}?${queryString}` : reqConfig.path;
1073
+ const url = `${this.config.baseUrl}${requestPath}`;
1074
+ const bodyJson = reqConfig.body ? JSON.stringify(reqConfig.body) : "";
1075
+ const timestamp = getNow();
1076
+ this.logRequest(reqConfig.method, url, reqConfig.auth);
1077
+ if (reqConfig.rateLimit) {
1078
+ await this.rateLimiter.consume(reqConfig.rateLimit);
1079
+ }
1080
+ const headers = new Headers({
1081
+ "Content-Type": "application/json",
1082
+ Accept: "application/json"
1083
+ });
1084
+ if (this.config.userAgent) {
1085
+ headers.set("User-Agent", this.config.userAgent);
1086
+ }
1087
+ if (reqConfig.auth === "private") {
1088
+ this.setAuthHeaders(headers, reqConfig.method, requestPath, bodyJson, timestamp);
1089
+ }
1090
+ if (this.config.demo) {
1091
+ headers.set("x-simulated-trading", "1");
1092
+ }
1093
+ const t0 = Date.now();
1094
+ let response;
1095
+ try {
1096
+ const fetchOptions = {
1097
+ method: reqConfig.method,
1098
+ headers,
1099
+ body: reqConfig.method === "POST" ? bodyJson : void 0,
1100
+ signal: AbortSignal.timeout(this.config.timeoutMs)
1101
+ };
1102
+ if (this.dispatcher) {
1103
+ fetchOptions.dispatcher = this.dispatcher;
1104
+ }
1105
+ response = await fetch(url, fetchOptions);
1106
+ } catch (error) {
1107
+ if (this.config.verbose) {
1108
+ const elapsed2 = Date.now() - t0;
1109
+ const cause = error instanceof Error ? error.message : String(error);
1110
+ vlog(`\u2717 NetworkError after ${elapsed2}ms: ${cause}`);
1111
+ }
1112
+ throw new NetworkError(
1113
+ `Failed to call OKX endpoint ${reqConfig.method} ${requestPath}.`,
1114
+ `${reqConfig.method} ${requestPath}`,
1115
+ error
1116
+ );
1117
+ }
1118
+ const rawText = await response.text();
1119
+ const elapsed = Date.now() - t0;
1120
+ const traceId = extractTraceId(response.headers);
1121
+ return this.processResponse(rawText, response, elapsed, traceId, reqConfig, requestPath);
1122
+ }
1065
1123
  };
1066
1124
  var DEFAULT_SOURCE_TAG = "MCP";
1067
1125
  var OKX_SITES = {
@@ -1087,6 +1145,10 @@ var BOT_SUB_MODULE_IDS = [
1087
1145
  "bot.dca"
1088
1146
  ];
1089
1147
  var BOT_DEFAULT_SUB_MODULES = ["bot.grid"];
1148
+ var EARN_SUB_MODULE_IDS = [
1149
+ "earn.savings",
1150
+ "earn.onchain"
1151
+ ];
1090
1152
  var MODULES = [
1091
1153
  "market",
1092
1154
  "spot",
@@ -1094,6 +1156,7 @@ var MODULES = [
1094
1156
  "futures",
1095
1157
  "option",
1096
1158
  "account",
1159
+ ...EARN_SUB_MODULE_IDS,
1097
1160
  ...BOT_SUB_MODULE_IDS
1098
1161
  ];
1099
1162
  var DEFAULT_MODULES = ["spot", "swap", "option", "account", ...BOT_DEFAULT_SUB_MODULES];
@@ -1159,6 +1222,13 @@ function compactObject(object) {
1159
1222
  }
1160
1223
  return next;
1161
1224
  }
1225
+ function normalizeResponse(response) {
1226
+ return {
1227
+ endpoint: response.endpoint,
1228
+ requestTime: response.requestTime,
1229
+ data: response.data
1230
+ };
1231
+ }
1162
1232
  var OKX_CANDLE_BARS = [
1163
1233
  "1m",
1164
1234
  "3m",
@@ -1198,6 +1268,14 @@ function privateRateLimit(key, rps = 10) {
1198
1268
  refillPerSecond: rps
1199
1269
  };
1200
1270
  }
1271
+ function assertNotDemo(config, endpoint) {
1272
+ if (config.demo) {
1273
+ throw new ConfigError(
1274
+ `"${endpoint}" is not supported in simulated trading mode.`,
1275
+ "Disable demo mode (remove OKX_DEMO=1 or --demo flag) to use this endpoint."
1276
+ );
1277
+ }
1278
+ }
1201
1279
  function normalize(response) {
1202
1280
  return {
1203
1281
  endpoint: response.endpoint,
@@ -1776,7 +1854,7 @@ function registerAlgoTradeTools() {
1776
1854
  },
1777
1855
  sz: {
1778
1856
  type: "string",
1779
- description: "Contracts to close"
1857
+ description: "Number of contracts to close (NOT USDT amount). Use market_get_instruments to get ctVal for conversion."
1780
1858
  },
1781
1859
  tpTriggerPx: {
1782
1860
  type: "string",
@@ -1871,7 +1949,7 @@ function registerAlgoTradeTools() {
1871
1949
  },
1872
1950
  sz: {
1873
1951
  type: "string",
1874
- description: "Contracts"
1952
+ description: "Number of contracts (NOT USDT amount). Use market_get_instruments to get ctVal for conversion."
1875
1953
  },
1876
1954
  callbackRatio: {
1877
1955
  type: "string",
@@ -2684,6 +2762,246 @@ function registerBotTools() {
2684
2762
  ...registerDcaTools()
2685
2763
  ];
2686
2764
  }
2765
+ function registerEarnTools() {
2766
+ return [
2767
+ {
2768
+ name: "earn_get_savings_balance",
2769
+ module: "earn.savings",
2770
+ description: "Get Simple Earn (savings/flexible earn) balance. Returns current holdings for all currencies or a specific one. Private endpoint. Rate limit: 6 req/s.",
2771
+ isWrite: false,
2772
+ inputSchema: {
2773
+ type: "object",
2774
+ properties: {
2775
+ ccy: {
2776
+ type: "string",
2777
+ description: "e.g. USDT or BTC. Omit for all."
2778
+ }
2779
+ }
2780
+ },
2781
+ handler: async (rawArgs, context) => {
2782
+ const args = asRecord(rawArgs);
2783
+ const response = await context.client.privateGet(
2784
+ "/api/v5/finance/savings/balance",
2785
+ compactObject({ ccy: readString(args, "ccy") }),
2786
+ privateRateLimit("earn_get_savings_balance", 6)
2787
+ );
2788
+ return normalizeResponse(response);
2789
+ }
2790
+ },
2791
+ {
2792
+ name: "earn_savings_purchase",
2793
+ module: "earn.savings",
2794
+ description: "Purchase Simple Earn (savings/flexible earn). [CAUTION] Moves real funds into earn product. Not supported in demo/simulated trading mode. Private endpoint. Rate limit: 6 req/s.",
2795
+ isWrite: true,
2796
+ inputSchema: {
2797
+ type: "object",
2798
+ properties: {
2799
+ ccy: {
2800
+ type: "string",
2801
+ description: "Currency to purchase, e.g. USDT"
2802
+ },
2803
+ amt: {
2804
+ type: "string",
2805
+ description: "Purchase amount"
2806
+ },
2807
+ rate: {
2808
+ type: "string",
2809
+ description: "Lending rate. Annual rate in decimal, e.g. 0.01 = 1%. Defaults to 0.01 (1%, minimum rate, easiest to match)."
2810
+ }
2811
+ },
2812
+ required: ["ccy", "amt"]
2813
+ },
2814
+ handler: async (rawArgs, context) => {
2815
+ assertNotDemo(context.config, "earn_savings_purchase");
2816
+ const args = asRecord(rawArgs);
2817
+ const response = await context.client.privatePost(
2818
+ "/api/v5/finance/savings/purchase-redempt",
2819
+ compactObject({
2820
+ ccy: requireString(args, "ccy"),
2821
+ amt: requireString(args, "amt"),
2822
+ side: "purchase",
2823
+ rate: readString(args, "rate") ?? "0.01"
2824
+ }),
2825
+ privateRateLimit("earn_savings_purchase", 6)
2826
+ );
2827
+ return normalizeResponse(response);
2828
+ }
2829
+ },
2830
+ {
2831
+ name: "earn_savings_redeem",
2832
+ module: "earn.savings",
2833
+ description: "Redeem Simple Earn (savings/flexible earn). [CAUTION] Withdraws funds from earn product. Not supported in demo/simulated trading mode. Private endpoint. Rate limit: 6 req/s.",
2834
+ isWrite: true,
2835
+ inputSchema: {
2836
+ type: "object",
2837
+ properties: {
2838
+ ccy: {
2839
+ type: "string",
2840
+ description: "Currency to redeem, e.g. USDT"
2841
+ },
2842
+ amt: {
2843
+ type: "string",
2844
+ description: "Redemption amount"
2845
+ }
2846
+ },
2847
+ required: ["ccy", "amt"]
2848
+ },
2849
+ handler: async (rawArgs, context) => {
2850
+ assertNotDemo(context.config, "earn_savings_redeem");
2851
+ const args = asRecord(rawArgs);
2852
+ const response = await context.client.privatePost(
2853
+ "/api/v5/finance/savings/purchase-redempt",
2854
+ compactObject({
2855
+ ccy: requireString(args, "ccy"),
2856
+ amt: requireString(args, "amt"),
2857
+ side: "redempt"
2858
+ }),
2859
+ privateRateLimit("earn_savings_redeem", 6)
2860
+ );
2861
+ return normalizeResponse(response);
2862
+ }
2863
+ },
2864
+ {
2865
+ name: "earn_set_lending_rate",
2866
+ module: "earn.savings",
2867
+ description: "Set lending rate for Simple Earn. [CAUTION] Changes your lending rate preference. Not supported in demo/simulated trading mode. Private endpoint. Rate limit: 6 req/s.",
2868
+ isWrite: true,
2869
+ inputSchema: {
2870
+ type: "object",
2871
+ properties: {
2872
+ ccy: {
2873
+ type: "string",
2874
+ description: "Currency, e.g. USDT"
2875
+ },
2876
+ rate: {
2877
+ type: "string",
2878
+ description: "Lending rate. Annual rate in decimal, e.g. 0.01 = 1%"
2879
+ }
2880
+ },
2881
+ required: ["ccy", "rate"]
2882
+ },
2883
+ handler: async (rawArgs, context) => {
2884
+ assertNotDemo(context.config, "earn_set_lending_rate");
2885
+ const args = asRecord(rawArgs);
2886
+ const response = await context.client.privatePost(
2887
+ "/api/v5/finance/savings/set-lending-rate",
2888
+ {
2889
+ ccy: requireString(args, "ccy"),
2890
+ rate: requireString(args, "rate")
2891
+ },
2892
+ privateRateLimit("earn_set_lending_rate", 6)
2893
+ );
2894
+ return normalizeResponse(response);
2895
+ }
2896
+ },
2897
+ {
2898
+ name: "earn_get_lending_history",
2899
+ module: "earn.savings",
2900
+ description: "Get lending history for Simple Earn. Returns lending records with details like amount, rate, and earnings. Private endpoint. Rate limit: 6 req/s.",
2901
+ isWrite: false,
2902
+ inputSchema: {
2903
+ type: "object",
2904
+ properties: {
2905
+ ccy: {
2906
+ type: "string",
2907
+ description: "e.g. USDT. Omit for all."
2908
+ },
2909
+ after: {
2910
+ type: "string",
2911
+ description: "Pagination: before this record ID"
2912
+ },
2913
+ before: {
2914
+ type: "string",
2915
+ description: "Pagination: after this record ID"
2916
+ },
2917
+ limit: {
2918
+ type: "number",
2919
+ description: "Max results (default 100)"
2920
+ }
2921
+ }
2922
+ },
2923
+ handler: async (rawArgs, context) => {
2924
+ const args = asRecord(rawArgs);
2925
+ const response = await context.client.privateGet(
2926
+ "/api/v5/finance/savings/lending-history",
2927
+ compactObject({
2928
+ ccy: readString(args, "ccy"),
2929
+ after: readString(args, "after"),
2930
+ before: readString(args, "before"),
2931
+ limit: readNumber(args, "limit")
2932
+ }),
2933
+ privateRateLimit("earn_get_lending_history", 6)
2934
+ );
2935
+ return normalizeResponse(response);
2936
+ }
2937
+ },
2938
+ {
2939
+ name: "earn_get_lending_rate_summary",
2940
+ module: "earn.savings",
2941
+ description: "Get market lending rate summary for Simple Earn. Public endpoint (no API key required). Returns current lending rates, estimated APY, and available amounts. Rate limit: 6 req/s.",
2942
+ isWrite: false,
2943
+ inputSchema: {
2944
+ type: "object",
2945
+ properties: {
2946
+ ccy: {
2947
+ type: "string",
2948
+ description: "e.g. USDT. Omit for all."
2949
+ }
2950
+ }
2951
+ },
2952
+ handler: async (rawArgs, context) => {
2953
+ const args = asRecord(rawArgs);
2954
+ const response = await context.client.publicGet(
2955
+ "/api/v5/finance/savings/lending-rate-summary",
2956
+ compactObject({ ccy: readString(args, "ccy") }),
2957
+ publicRateLimit("earn_get_lending_rate_summary", 6)
2958
+ );
2959
+ return normalizeResponse(response);
2960
+ }
2961
+ },
2962
+ {
2963
+ name: "earn_get_lending_rate_history",
2964
+ module: "earn.savings",
2965
+ description: "Get historical lending rates for Simple Earn. Public endpoint (no API key required). Returns past lending rate data for trend analysis. Rate limit: 6 req/s.",
2966
+ isWrite: false,
2967
+ inputSchema: {
2968
+ type: "object",
2969
+ properties: {
2970
+ ccy: {
2971
+ type: "string",
2972
+ description: "e.g. USDT. Omit for all."
2973
+ },
2974
+ after: {
2975
+ type: "string",
2976
+ description: "Pagination: before this timestamp (ms)"
2977
+ },
2978
+ before: {
2979
+ type: "string",
2980
+ description: "Pagination: after this timestamp (ms)"
2981
+ },
2982
+ limit: {
2983
+ type: "number",
2984
+ description: "Max results (default 100)"
2985
+ }
2986
+ }
2987
+ },
2988
+ handler: async (rawArgs, context) => {
2989
+ const args = asRecord(rawArgs);
2990
+ const response = await context.client.publicGet(
2991
+ "/api/v5/finance/savings/lending-rate-history",
2992
+ compactObject({
2993
+ ccy: readString(args, "ccy"),
2994
+ after: readString(args, "after"),
2995
+ before: readString(args, "before"),
2996
+ limit: readNumber(args, "limit")
2997
+ }),
2998
+ publicRateLimit("earn_get_lending_rate_history", 6)
2999
+ );
3000
+ return normalizeResponse(response);
3001
+ }
3002
+ }
3003
+ ];
3004
+ }
2687
3005
  var FUTURES_INST_TYPES = ["FUTURES", "SWAP"];
2688
3006
  function normalize5(response) {
2689
3007
  return {
@@ -2728,7 +3046,7 @@ function registerFuturesTools() {
2728
3046
  },
2729
3047
  sz: {
2730
3048
  type: "string",
2731
- description: "Contracts"
3049
+ description: "Number of contracts (NOT USDT amount). Use market_get_instruments to get ctVal for conversion."
2732
3050
  },
2733
3051
  px: {
2734
3052
  type: "string",
@@ -3049,6 +3367,278 @@ function registerFuturesTools() {
3049
3367
  }
3050
3368
  ];
3051
3369
  }
3370
+ function registerOnchainEarnTools() {
3371
+ return [
3372
+ // -------------------------------------------------------------------------
3373
+ // Get Offers
3374
+ // -------------------------------------------------------------------------
3375
+ {
3376
+ name: "onchain_earn_get_offers",
3377
+ module: "earn.onchain",
3378
+ description: "Get available on-chain earn (staking/DeFi) offers. Returns investment products with APY, terms, and limits. Private endpoint. Rate limit: 3 req/s.",
3379
+ isWrite: false,
3380
+ inputSchema: {
3381
+ type: "object",
3382
+ properties: {
3383
+ productId: {
3384
+ type: "string",
3385
+ description: "Specific product ID to query. Omit for all offers."
3386
+ },
3387
+ protocolType: {
3388
+ type: "string",
3389
+ description: "Protocol type filter: staking, defi. Omit for all types."
3390
+ },
3391
+ ccy: {
3392
+ type: "string",
3393
+ description: "Currency filter, e.g. ETH. Omit for all currencies."
3394
+ }
3395
+ }
3396
+ },
3397
+ handler: async (rawArgs, context) => {
3398
+ const args = asRecord(rawArgs);
3399
+ const response = await context.client.privateGet(
3400
+ "/api/v5/finance/staking-defi/offers",
3401
+ compactObject({
3402
+ productId: readString(args, "productId"),
3403
+ protocolType: readString(args, "protocolType"),
3404
+ ccy: readString(args, "ccy")
3405
+ }),
3406
+ privateRateLimit("onchain_earn_get_offers", 3)
3407
+ );
3408
+ return normalizeResponse(response);
3409
+ }
3410
+ },
3411
+ // -------------------------------------------------------------------------
3412
+ // Purchase
3413
+ // -------------------------------------------------------------------------
3414
+ {
3415
+ name: "onchain_earn_purchase",
3416
+ module: "earn.onchain",
3417
+ description: "Purchase on-chain earn (staking/DeFi) product. [CAUTION] Moves real funds into staking/DeFi product. Not supported in demo/simulated trading mode. Private endpoint. Rate limit: 2 req/s.",
3418
+ isWrite: true,
3419
+ inputSchema: {
3420
+ type: "object",
3421
+ properties: {
3422
+ productId: {
3423
+ type: "string",
3424
+ description: "Product ID to purchase"
3425
+ },
3426
+ investData: {
3427
+ type: "array",
3428
+ description: "Investment data array: [{ccy, amt}]. Each item specifies currency and amount.",
3429
+ items: {
3430
+ type: "object",
3431
+ properties: {
3432
+ ccy: { type: "string", description: "Currency, e.g. ETH" },
3433
+ amt: { type: "string", description: "Amount to invest" }
3434
+ },
3435
+ required: ["ccy", "amt"]
3436
+ }
3437
+ },
3438
+ term: {
3439
+ type: "string",
3440
+ description: "Investment term in days. Required for fixed-term products."
3441
+ },
3442
+ tag: {
3443
+ type: "string",
3444
+ description: "Order tag for tracking (optional)."
3445
+ }
3446
+ },
3447
+ required: ["productId", "investData"]
3448
+ },
3449
+ handler: async (rawArgs, context) => {
3450
+ assertNotDemo(context.config, "onchain_earn_purchase");
3451
+ const args = asRecord(rawArgs);
3452
+ const response = await context.client.privatePost(
3453
+ "/api/v5/finance/staking-defi/purchase",
3454
+ compactObject({
3455
+ productId: requireString(args, "productId"),
3456
+ investData: args.investData,
3457
+ term: readString(args, "term"),
3458
+ tag: readString(args, "tag")
3459
+ }),
3460
+ privateRateLimit("onchain_earn_purchase", 2)
3461
+ );
3462
+ return normalizeResponse(response);
3463
+ }
3464
+ },
3465
+ // -------------------------------------------------------------------------
3466
+ // Redeem
3467
+ // -------------------------------------------------------------------------
3468
+ {
3469
+ name: "onchain_earn_redeem",
3470
+ module: "earn.onchain",
3471
+ description: "Redeem on-chain earn (staking/DeFi) investment. [CAUTION] Withdraws funds from staking/DeFi product. Some products may have lock periods. Not supported in demo mode. Private endpoint. Rate limit: 2 req/s.",
3472
+ isWrite: true,
3473
+ inputSchema: {
3474
+ type: "object",
3475
+ properties: {
3476
+ ordId: {
3477
+ type: "string",
3478
+ description: "Order ID to redeem"
3479
+ },
3480
+ protocolType: {
3481
+ type: "string",
3482
+ description: "Protocol type: staking, defi"
3483
+ },
3484
+ allowEarlyRedeem: {
3485
+ type: "boolean",
3486
+ description: "Allow early redemption for fixed-term products (may incur penalties). Default false."
3487
+ }
3488
+ },
3489
+ required: ["ordId", "protocolType"]
3490
+ },
3491
+ handler: async (rawArgs, context) => {
3492
+ assertNotDemo(context.config, "onchain_earn_redeem");
3493
+ const args = asRecord(rawArgs);
3494
+ const response = await context.client.privatePost(
3495
+ "/api/v5/finance/staking-defi/redeem",
3496
+ compactObject({
3497
+ ordId: requireString(args, "ordId"),
3498
+ protocolType: requireString(args, "protocolType"),
3499
+ allowEarlyRedeem: readBoolean(args, "allowEarlyRedeem")
3500
+ }),
3501
+ privateRateLimit("onchain_earn_redeem", 2)
3502
+ );
3503
+ return normalizeResponse(response);
3504
+ }
3505
+ },
3506
+ // -------------------------------------------------------------------------
3507
+ // Cancel
3508
+ // -------------------------------------------------------------------------
3509
+ {
3510
+ name: "onchain_earn_cancel",
3511
+ module: "earn.onchain",
3512
+ description: "Cancel pending on-chain earn purchase. [CAUTION] Cancels a pending investment order. Not supported in demo mode. Private endpoint. Rate limit: 2 req/s.",
3513
+ isWrite: true,
3514
+ inputSchema: {
3515
+ type: "object",
3516
+ properties: {
3517
+ ordId: {
3518
+ type: "string",
3519
+ description: "Order ID to cancel"
3520
+ },
3521
+ protocolType: {
3522
+ type: "string",
3523
+ description: "Protocol type: staking, defi"
3524
+ }
3525
+ },
3526
+ required: ["ordId", "protocolType"]
3527
+ },
3528
+ handler: async (rawArgs, context) => {
3529
+ assertNotDemo(context.config, "onchain_earn_cancel");
3530
+ const args = asRecord(rawArgs);
3531
+ const response = await context.client.privatePost(
3532
+ "/api/v5/finance/staking-defi/cancel",
3533
+ {
3534
+ ordId: requireString(args, "ordId"),
3535
+ protocolType: requireString(args, "protocolType")
3536
+ },
3537
+ privateRateLimit("onchain_earn_cancel", 2)
3538
+ );
3539
+ return normalizeResponse(response);
3540
+ }
3541
+ },
3542
+ // -------------------------------------------------------------------------
3543
+ // Get Active Orders
3544
+ // -------------------------------------------------------------------------
3545
+ {
3546
+ name: "onchain_earn_get_active_orders",
3547
+ module: "earn.onchain",
3548
+ description: "Get active on-chain earn orders. Returns current staking/DeFi investments. Private endpoint. Rate limit: 3 req/s.",
3549
+ isWrite: false,
3550
+ inputSchema: {
3551
+ type: "object",
3552
+ properties: {
3553
+ productId: {
3554
+ type: "string",
3555
+ description: "Filter by product ID. Omit for all."
3556
+ },
3557
+ protocolType: {
3558
+ type: "string",
3559
+ description: "Filter by protocol type: staking, defi. Omit for all."
3560
+ },
3561
+ ccy: {
3562
+ type: "string",
3563
+ description: "Filter by currency, e.g. ETH. Omit for all."
3564
+ },
3565
+ state: {
3566
+ type: "string",
3567
+ description: "Filter by state: 8 (pending), 13 (cancelling), 9 (onchain), 1 (earning), 2 (redeeming). Omit for all."
3568
+ }
3569
+ }
3570
+ },
3571
+ handler: async (rawArgs, context) => {
3572
+ const args = asRecord(rawArgs);
3573
+ const response = await context.client.privateGet(
3574
+ "/api/v5/finance/staking-defi/orders-active",
3575
+ compactObject({
3576
+ productId: readString(args, "productId"),
3577
+ protocolType: readString(args, "protocolType"),
3578
+ ccy: readString(args, "ccy"),
3579
+ state: readString(args, "state")
3580
+ }),
3581
+ privateRateLimit("onchain_earn_get_active_orders", 3)
3582
+ );
3583
+ return normalizeResponse(response);
3584
+ }
3585
+ },
3586
+ // -------------------------------------------------------------------------
3587
+ // Get Order History
3588
+ // -------------------------------------------------------------------------
3589
+ {
3590
+ name: "onchain_earn_get_order_history",
3591
+ module: "earn.onchain",
3592
+ description: "Get on-chain earn order history. Returns past staking/DeFi investments including redeemed orders. Private endpoint. Rate limit: 3 req/s.",
3593
+ isWrite: false,
3594
+ inputSchema: {
3595
+ type: "object",
3596
+ properties: {
3597
+ productId: {
3598
+ type: "string",
3599
+ description: "Filter by product ID. Omit for all."
3600
+ },
3601
+ protocolType: {
3602
+ type: "string",
3603
+ description: "Filter by protocol type: staking, defi. Omit for all."
3604
+ },
3605
+ ccy: {
3606
+ type: "string",
3607
+ description: "Filter by currency, e.g. ETH. Omit for all."
3608
+ },
3609
+ after: {
3610
+ type: "string",
3611
+ description: "Pagination: return results before this order ID"
3612
+ },
3613
+ before: {
3614
+ type: "string",
3615
+ description: "Pagination: return results after this order ID"
3616
+ },
3617
+ limit: {
3618
+ type: "string",
3619
+ description: "Max results to return (default 100, max 100)"
3620
+ }
3621
+ }
3622
+ },
3623
+ handler: async (rawArgs, context) => {
3624
+ const args = asRecord(rawArgs);
3625
+ const response = await context.client.privateGet(
3626
+ "/api/v5/finance/staking-defi/orders-history",
3627
+ compactObject({
3628
+ productId: readString(args, "productId"),
3629
+ protocolType: readString(args, "protocolType"),
3630
+ ccy: readString(args, "ccy"),
3631
+ after: readString(args, "after"),
3632
+ before: readString(args, "before"),
3633
+ limit: readString(args, "limit")
3634
+ }),
3635
+ privateRateLimit("onchain_earn_get_order_history", 3)
3636
+ );
3637
+ return normalizeResponse(response);
3638
+ }
3639
+ }
3640
+ ];
3641
+ }
3052
3642
  function normalize6(response) {
3053
3643
  return {
3054
3644
  endpoint: response.endpoint,
@@ -3566,7 +4156,7 @@ function registerOptionTools() {
3566
4156
  },
3567
4157
  sz: {
3568
4158
  type: "string",
3569
- description: "Number of contracts"
4159
+ description: "Number of contracts (NOT USDT amount). Use market_get_instruments to get ctVal for conversion."
3570
4160
  },
3571
4161
  px: {
3572
4162
  type: "string",
@@ -3673,7 +4263,7 @@ function registerOptionTools() {
3673
4263
  instId: { type: "string", description: "e.g. BTC-USD-241227-50000-C" },
3674
4264
  ordId: { type: "string" },
3675
4265
  clOrdId: { type: "string" },
3676
- newSz: { type: "string", description: "New quantity (contracts)" },
4266
+ newSz: { type: "string", description: "New number of contracts (NOT USDT amount)" },
3677
4267
  newPx: { type: "string", description: "New price" }
3678
4268
  },
3679
4269
  required: ["instId"]
@@ -4635,7 +5225,7 @@ function registerSwapTradeTools() {
4635
5225
  },
4636
5226
  sz: {
4637
5227
  type: "string",
4638
- description: "Contracts (e.g. '1'; BTC-USDT-SWAP: 1ct=0.01 BTC)"
5228
+ description: "Number of contracts (NOT USDT amount). Use market_get_instruments to get ctVal for conversion."
4639
5229
  },
4640
5230
  px: {
4641
5231
  type: "string",
@@ -4900,7 +5490,7 @@ function registerSwapTradeTools() {
4900
5490
  properties: {
4901
5491
  instId: { type: "string", description: "e.g. BTC-USDT-SWAP" },
4902
5492
  algoId: { type: "string", description: "Algo order ID" },
4903
- newSz: { type: "string", description: "New quantity (contracts)" },
5493
+ newSz: { type: "string", description: "New number of contracts (NOT USDT amount)" },
4904
5494
  newTpTriggerPx: { type: "string", description: "New TP trigger price" },
4905
5495
  newTpOrdPx: { type: "string", description: "New TP order price; -1=market" },
4906
5496
  newSlTriggerPx: { type: "string", description: "New SL trigger price" },
@@ -5253,6 +5843,8 @@ function allToolSpecs() {
5253
5843
  ...registerAlgoTradeTools(),
5254
5844
  ...registerAccountTools(),
5255
5845
  ...registerBotTools(),
5846
+ ...registerEarnTools(),
5847
+ ...registerOnchainEarnTools(),
5256
5848
  ...registerAuditTools()
5257
5849
  ];
5258
5850
  }
@@ -5292,8 +5884,15 @@ function readTomlProfile(profileName) {
5292
5884
  return config.profiles?.[name] ?? {};
5293
5885
  }
5294
5886
  var BASE_MODULES = MODULES.filter(
5295
- (m) => !BOT_SUB_MODULE_IDS.includes(m)
5887
+ (m) => !BOT_SUB_MODULE_IDS.includes(m) && !EARN_SUB_MODULE_IDS.includes(m)
5296
5888
  );
5889
+ function expandShorthand(moduleId) {
5890
+ if (moduleId === "all") return [...BASE_MODULES, ...BOT_SUB_MODULE_IDS];
5891
+ if (moduleId === "earn" || moduleId === "earn.all") return [...EARN_SUB_MODULE_IDS];
5892
+ if (moduleId === "bot") return [...BOT_DEFAULT_SUB_MODULES];
5893
+ if (moduleId === "bot.all") return [...BOT_SUB_MODULE_IDS];
5894
+ return null;
5895
+ }
5297
5896
  function parseModuleList(rawModules) {
5298
5897
  if (!rawModules || rawModules.trim().length === 0) {
5299
5898
  return [...DEFAULT_MODULES];
@@ -5302,32 +5901,28 @@ function parseModuleList(rawModules) {
5302
5901
  if (trimmed === "all") {
5303
5902
  return [...BASE_MODULES, ...BOT_SUB_MODULE_IDS];
5304
5903
  }
5305
- const requested = trimmed.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
5904
+ const requested = trimmed.split(",").map((s) => s.trim()).filter(Boolean);
5306
5905
  if (requested.length === 0) {
5307
5906
  return [...DEFAULT_MODULES];
5308
5907
  }
5309
5908
  const deduped = /* @__PURE__ */ new Set();
5310
5909
  for (const moduleId of requested) {
5311
- if (moduleId === "bot") {
5312
- for (const sub of BOT_DEFAULT_SUB_MODULES) deduped.add(sub);
5313
- continue;
5314
- }
5315
- if (moduleId === "bot.all") {
5316
- for (const sub of BOT_SUB_MODULE_IDS) deduped.add(sub);
5910
+ const expanded = expandShorthand(moduleId);
5911
+ if (expanded) {
5912
+ expanded.forEach((sub) => deduped.add(sub));
5317
5913
  continue;
5318
5914
  }
5319
5915
  if (!MODULES.includes(moduleId)) {
5320
5916
  throw new ConfigError(
5321
5917
  `Unknown module "${moduleId}".`,
5322
- `Use one of: ${MODULES.join(", ")}, "bot", "bot.all", or "all".`
5918
+ `Use one of: ${MODULES.join(", ")}, "earn", "earn.all", "bot", "bot.all", or "all".`
5323
5919
  );
5324
5920
  }
5325
5921
  deduped.add(moduleId);
5326
5922
  }
5327
5923
  return Array.from(deduped);
5328
5924
  }
5329
- function loadConfig(cli) {
5330
- const toml = readTomlProfile(cli.profile);
5925
+ function loadCredentials(toml) {
5331
5926
  const apiKey = process.env.OKX_API_KEY?.trim() ?? toml.api_key;
5332
5927
  const secretKey = process.env.OKX_SECRET_KEY?.trim() ?? toml.secret_key;
5333
5928
  const passphrase = process.env.OKX_PASSPHRASE?.trim() ?? toml.passphrase;
@@ -5339,23 +5934,34 @@ function loadConfig(cli) {
5339
5934
  "Set OKX_API_KEY, OKX_SECRET_KEY and OKX_PASSPHRASE together (env vars or config.toml profile)."
5340
5935
  );
5341
5936
  }
5342
- const demo = cli.demo || process.env.OKX_DEMO === "1" || process.env.OKX_DEMO === "true" || (toml.demo ?? false);
5343
- const rawSite = cli.site?.trim() ?? process.env.OKX_SITE?.trim() ?? toml.site ?? "global";
5937
+ return { apiKey, secretKey, passphrase, hasAuth };
5938
+ }
5939
+ function resolveSite(cliSite, tomlSite) {
5940
+ const rawSite = cliSite?.trim() ?? process.env.OKX_SITE?.trim() ?? tomlSite ?? "global";
5344
5941
  if (!SITE_IDS.includes(rawSite)) {
5345
5942
  throw new ConfigError(
5346
5943
  `Unknown site "${rawSite}".`,
5347
5944
  `Use one of: ${SITE_IDS.join(", ")}.`
5348
5945
  );
5349
5946
  }
5350
- const site = rawSite;
5351
- const rawBaseUrl = process.env.OKX_API_BASE_URL?.trim() ?? toml.base_url ?? OKX_SITES[site].apiBaseUrl;
5947
+ return rawSite;
5948
+ }
5949
+ function resolveBaseUrl(site, tomlBaseUrl) {
5950
+ const rawBaseUrl = process.env.OKX_API_BASE_URL?.trim() ?? tomlBaseUrl ?? OKX_SITES[site].apiBaseUrl;
5352
5951
  if (!rawBaseUrl.startsWith("http://") && !rawBaseUrl.startsWith("https://")) {
5353
5952
  throw new ConfigError(
5354
5953
  `Invalid base URL "${rawBaseUrl}".`,
5355
5954
  "OKX_API_BASE_URL must start with http:// or https://"
5356
5955
  );
5357
5956
  }
5358
- const baseUrl = rawBaseUrl.replace(/\/+$/, "");
5957
+ return rawBaseUrl.replace(/\/+$/, "");
5958
+ }
5959
+ function loadConfig(cli) {
5960
+ const toml = readTomlProfile(cli.profile);
5961
+ const creds = loadCredentials(toml);
5962
+ const demo = cli.demo || process.env.OKX_DEMO === "1" || process.env.OKX_DEMO === "true" || (toml.demo ?? false);
5963
+ const site = resolveSite(cli.site, toml.site);
5964
+ const baseUrl = resolveBaseUrl(site, toml.base_url);
5359
5965
  const rawTimeout = process.env.OKX_TIMEOUT_MS ? Number(process.env.OKX_TIMEOUT_MS) : toml.timeout_ms ?? 15e3;
5360
5966
  if (!Number.isFinite(rawTimeout) || rawTimeout <= 0) {
5361
5967
  throw new ConfigError(
@@ -5363,11 +5969,15 @@ function loadConfig(cli) {
5363
5969
  "Set OKX_TIMEOUT_MS as a positive integer in milliseconds."
5364
5970
  );
5365
5971
  }
5972
+ const rawProxyUrl = toml.proxy_url?.trim();
5973
+ if (rawProxyUrl && !rawProxyUrl.startsWith("http://") && !rawProxyUrl.startsWith("https://")) {
5974
+ throw new ConfigError(
5975
+ `Invalid proxy URL "${rawProxyUrl}".`,
5976
+ "proxy_url must start with http:// or https://. SOCKS proxies are not supported."
5977
+ );
5978
+ }
5366
5979
  return {
5367
- apiKey,
5368
- secretKey,
5369
- passphrase,
5370
- hasAuth,
5980
+ ...creds,
5371
5981
  baseUrl,
5372
5982
  timeoutMs: Math.floor(rawTimeout),
5373
5983
  modules: parseModuleList(cli.modules),
@@ -5375,7 +5985,9 @@ function loadConfig(cli) {
5375
5985
  demo,
5376
5986
  site,
5377
5987
  userAgent: cli.userAgent,
5378
- sourceTag: cli.sourceTag ?? DEFAULT_SOURCE_TAG
5988
+ sourceTag: cli.sourceTag ?? DEFAULT_SOURCE_TAG,
5989
+ proxyUrl: rawProxyUrl || void 0,
5990
+ verbose: cli.verbose ?? false
5379
5991
  };
5380
5992
  }
5381
5993
  var CACHE_FILE = join2(homedir2(), ".okx", "update-check.json");
@@ -5660,7 +6272,7 @@ var _require = createRequire(import.meta.url);
5660
6272
  var pkg = _require("../package.json");
5661
6273
  var SERVER_NAME = "okx-trade-mcp";
5662
6274
  var SERVER_VERSION = pkg.version;
5663
- var GIT_HASH = true ? "c49054f" : "dev";
6275
+ var GIT_HASH = true ? "4427916" : "dev";
5664
6276
 
5665
6277
  // src/server.ts
5666
6278
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";