@liquiditytech/rapidx-cli 1.0.27 → 1.0.29

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.
@@ -1,4 +1,4 @@
1
- import { consumePreview, createTargetedTradePreview, createTradePreview, defaultSafetyPolicy, executeRapidXCapability, findCapabilityById, loadPreviewStoreFromFile, makeSafetyState, normalizeUnknownError, resolvePreviewStoreFile, savePreviewStoreToFile, verifyPreview } from "../../core/index.js";
1
+ import { consumePreview, createTargetedTradePreview, createTradePreview, defaultSafetyPolicy, executeRapidXCapability, findCapabilityById, loadPreviewStoreFromFile, makeSafetyState, normalizeUnknownError, resolvePreviewStoreFile, runPreviewPreflight, savePreviewStoreToFile, verifyPreview } from "../../core/index.js";
2
2
  import { fail, ok } from "../envelope.js";
3
3
  import { writeCliAudit } from "../audit.js";
4
4
  const safetyState = makeSafetyState();
@@ -12,6 +12,7 @@ export async function runOrderCommand(action, input) {
12
12
  }
13
13
  try {
14
14
  const preview = createTradePreview(capability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore);
15
+ Object.assign(preview, await runPreviewPreflight("order.place", input));
15
16
  savePreviewStoreToFile(previewStoreFile, previewStore);
16
17
  return ok(preview, `rapidx order ${action}`);
17
18
  }
@@ -28,6 +29,7 @@ export async function runOrderCommand(action, input) {
28
29
  }
29
30
  try {
30
31
  const preview = createTargetedTradePreview(targetCapability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore);
32
+ Object.assign(preview, await runPreviewPreflight(previewTarget, input));
31
33
  savePreviewStoreToFile(previewStoreFile, previewStore);
32
34
  return ok(preview, `rapidx order ${action}`);
33
35
  }
@@ -1,4 +1,4 @@
1
- import { createTargetedTradePreview, defaultSafetyPolicy, findCapabilityById, loadPreviewStoreFromFile, makeSafetyState, normalizeUnknownError, resolvePreviewStoreFile, savePreviewStoreToFile } from "../../core/index.js";
1
+ import { createTargetedTradePreview, defaultSafetyPolicy, findCapabilityById, loadPreviewStoreFromFile, makeSafetyState, normalizeUnknownError, resolvePreviewStoreFile, runPreviewPreflight, savePreviewStoreToFile } from "../../core/index.js";
2
2
  import { fail, ok } from "../envelope.js";
3
3
  const safetyState = makeSafetyState();
4
4
  export async function runTradeCommand(action, input) {
@@ -17,6 +17,7 @@ export async function runTradeCommand(action, input) {
17
17
  const previewStore = loadPreviewStoreFromFile(previewStoreFile);
18
18
  try {
19
19
  const preview = createTargetedTradePreview(targetCapability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore);
20
+ Object.assign(preview, await runPreviewPreflight(targetCapability.capabilityId, input));
20
21
  savePreviewStoreToFile(previewStoreFile, previewStore);
21
22
  return ok(preview, "rapidx trade preview");
22
23
  }
package/dist/cli/help.js CHANGED
@@ -13,7 +13,7 @@ export function formatCliHelp() {
13
13
  " rapidx market get-ticker --symbol BINANCE_PERP_BTC_USDT --json",
14
14
  " rapidx order preview --input '{\"symbol\":\"BINANCE_PERP_BTC_USDT\",\"side\":\"BUY\",\"orderType\":\"LIMIT\",\"price\":\"1\",\"quantity\":\"0.001\",\"maxNotional\":\"1\",\"clientOrderId\":\"example\"}' --json",
15
15
  " rapidx order cancel-preview --input '{\"orderId\":\"<order-id>\"}' --json",
16
- " rapidx trade verify-live --input '{\"symbol\":\"BINANCE_PERP_BTC_USDT\",\"side\":\"BUY\",\"maxNotional\":\"10\",\"clientOrderId\":\"verify-001\",\"explicitUserConsent\":true}' --json",
16
+ " rapidx trade verify-live --input '{\"symbol\":\"BINANCE_PERP_BTC_USDT\",\"side\":\"BUY\",\"maxNotional\":\"10\",\"clientOrderId\":\"verify-001\",\"explicitUserConsent\":true,\"acceptedRiskText\":\"I authorize a real verification order for BINANCE_PERP_BTC_USDT BUY maxNotional 10 with cancel cleanup.\"}' --json",
17
17
  " rapidx mcp serve",
18
18
  "",
19
19
  "Domains:",
@@ -32,7 +32,7 @@ export async function executeRapidXCapability(capabilityId, input = {}, options
32
32
  case "order.amend":
33
33
  return client.put("/api/v1/trading/order", orderAmendBody(input));
34
34
  case "order.cancel":
35
- return client.delete("/api/v1/trading/order", orderCancelBody(input));
35
+ return executeOrderCancelCapability(client, input);
36
36
  case "order.get":
37
37
  return client.get("/api/v1/trading/order", optionalParams(input, ["orderId", "clientOrderId", "symbol"]));
38
38
  case "order.list":
@@ -49,10 +49,11 @@ export async function executeRapidXCapability(capabilityId, input = {}, options
49
49
  case "position.close":
50
50
  return executePositionCloseCapability(client, input);
51
51
  case "position.set-leverage":
52
- return client.post("/api/v1/trading/position/leverage", {
52
+ return client.post("/api/v1/trading/position/leverage", compactObject({
53
53
  sym: String(input.symbol),
54
- leverage: String(input.leverage)
55
- });
54
+ leverage: String(input.leverage),
55
+ positionSide: input.positionSide
56
+ }));
56
57
  case "algo.place":
57
58
  return client.post("/api/v1/algo/order", algoPlaceBody(input));
58
59
  case "algo.amend":
@@ -70,8 +71,9 @@ export async function executeRapidXCapability(capabilityId, input = {}, options
70
71
  }
71
72
  }
72
73
  async function executeMarketCapability(capabilityId, input, fetchFn) {
73
- const symbol = stringValue(input.symbol ?? input.sym, "BINANCE_PERP_BTC_USDT");
74
- const parsed = parseRapidXSymbol(symbol);
74
+ const inputSymbol = stringValue(input.symbol ?? input.sym, "BINANCE_PERP_BTC_USDT");
75
+ const parsed = parseRapidXSymbol(inputSymbol);
76
+ const symbol = parsed.canonicalSymbol;
75
77
  if (capabilityId === "market.funding-rate" || capabilityId === "market.mark-price" || capabilityId === "market.symbol-info") {
76
78
  const client = new RapidXClient({ fetchFn });
77
79
  const path = capabilityId === "market.funding-rate"
@@ -96,6 +98,7 @@ async function executeMarketCapability(capabilityId, input, fetchFn) {
96
98
  case "market.ticker": {
97
99
  const raw = await fetchJson(fetchFn, `${base}${parsed.isPerp ? "/fapi/v1/ticker/24hr" : "/api/v3/ticker/24hr"}`, { symbol: parsed.binanceSymbol });
98
100
  return {
101
+ ...symbolMetadata(symbol, inputSymbol, parsed.binanceSymbol),
99
102
  symbol,
100
103
  lastPrice: raw.lastPrice,
101
104
  priceChange: raw.priceChange,
@@ -108,7 +111,7 @@ async function executeMarketCapability(capabilityId, input, fetchFn) {
108
111
  }
109
112
  case "market.orderbook": {
110
113
  const raw = await fetchJson(fetchFn, `${base}${parsed.isPerp ? "/fapi/v1/depth" : "/api/v3/depth"}`, { symbol: parsed.binanceSymbol, limit: stringValue(input.depth, "20") });
111
- return normalizeOrderbook(symbol, raw.bids, raw.asks);
114
+ return normalizeOrderbook(symbol, raw.bids, raw.asks, symbolMetadata(symbol, inputSymbol, parsed.binanceSymbol));
112
115
  }
113
116
  case "market.klines": {
114
117
  const raw = await fetchJson(fetchFn, `${base}${parsed.isPerp ? "/fapi/v1/klines" : "/api/v3/klines"}`, {
@@ -116,13 +119,14 @@ async function executeMarketCapability(capabilityId, input, fetchFn) {
116
119
  interval: stringValue(input.interval, "1h"),
117
120
  limit: stringValue(input.limit, "100")
118
121
  });
119
- return { symbol, interval: stringValue(input.interval, "1h"), count: raw.length, candles: raw };
122
+ return { ...symbolMetadata(symbol, inputSymbol, parsed.binanceSymbol), symbol, interval: stringValue(input.interval, "1h"), count: raw.length, candles: raw };
120
123
  }
121
124
  case "market.open-interest": {
122
125
  if (!parsed.isPerp) {
123
126
  throw new ProductError({ code: "RCORE00001", status: "FAIL", message: "open interest only supports perpetual symbols" });
124
127
  }
125
- return fetchJson(fetchFn, `${base}/fapi/v1/openInterest`, { symbol: parsed.binanceSymbol });
128
+ const raw = await fetchJson(fetchFn, `${base}/fapi/v1/openInterest`, { symbol: parsed.binanceSymbol });
129
+ return { ...raw, ...symbolMetadata(symbol, inputSymbol, String(raw.symbol ?? parsed.binanceSymbol)), symbol };
126
130
  }
127
131
  default:
128
132
  throw new ProductError({ code: "RCORE30002", status: "FAIL", message: `Unknown market capability: ${capabilityId}` });
@@ -159,23 +163,39 @@ async function executePositionCloseCapability(client, input) {
159
163
  response
160
164
  };
161
165
  }
166
+ async function executeOrderCancelCapability(client, input) {
167
+ const response = await client.delete("/api/v1/trading/order", orderCancelBody(input));
168
+ const lastObservedOrderState = normalizeCancelOrderState(extractOrderState(response));
169
+ const terminalStateConfirmed = isCancelTerminalState(lastObservedOrderState);
170
+ return {
171
+ cancelAccepted: true,
172
+ terminalStateConfirmed,
173
+ ...compactObject({ lastObservedOrderState }),
174
+ recommendedAction: terminalStateConfirmed
175
+ ? "verify final status with order/get or order/list when needed"
176
+ : "poll order/get until CANCELED, REJECTED, EXPIRED, or timeout",
177
+ response
178
+ };
179
+ }
162
180
  async function executeOkxMarketCapability(capabilityId, symbol, instId, input, fetchFn) {
181
+ const inputSymbol = stringValue(input.symbol ?? input.sym, symbol);
163
182
  switch (capabilityId) {
164
183
  case "market.ticker": {
165
184
  const data = await fetchOkx(fetchFn, "/api/v5/market/ticker", { instId });
166
185
  const raw = data[0] ?? {};
167
- return { symbol, lastPrice: raw.last, highPrice: raw.high24h, lowPrice: raw.low24h, volume: raw.vol24h, quoteVolume: raw.volCcy24h };
186
+ return { ...symbolMetadata(symbol, inputSymbol, String(raw.instId ?? instId)), symbol, lastPrice: raw.last, highPrice: raw.high24h, lowPrice: raw.low24h, volume: raw.vol24h, quoteVolume: raw.volCcy24h };
168
187
  }
169
188
  case "market.orderbook": {
170
189
  const data = await fetchOkx(fetchFn, "/api/v5/market/books", { instId, sz: stringValue(input.depth, "20") });
171
190
  const raw = data[0] ?? { bids: [], asks: [] };
172
191
  const bids = Array.isArray(raw.bids) ? raw.bids : [];
173
192
  const asks = Array.isArray(raw.asks) ? raw.asks : [];
174
- return normalizeOrderbook(symbol, bids, asks);
193
+ return normalizeOrderbook(symbol, bids, asks, symbolMetadata(symbol, inputSymbol, instId));
175
194
  }
176
195
  case "market.open-interest": {
177
196
  const data = await fetchOkx(fetchFn, "/api/v5/public/open-interest", { instId });
178
- return { symbol, ...(data[0] ?? {}) };
197
+ const raw = data[0] ?? {};
198
+ return { ...raw, ...symbolMetadata(symbol, inputSymbol, String(raw.instId ?? instId)), symbol };
179
199
  }
180
200
  default:
181
201
  throw new ProductError({ code: "RCORE30002", status: "FAIL", message: `OKX adapter not registered for ${capabilityId}` });
@@ -200,10 +220,11 @@ async function fetchOkx(fetchFn, path, params) {
200
220
  }
201
221
  return payload.data ?? [];
202
222
  }
203
- function normalizeOrderbook(symbol, bidsRaw, asksRaw) {
223
+ function normalizeOrderbook(symbol, bidsRaw, asksRaw, metadata = {}) {
204
224
  const bids = bidsRaw.map(([price, quantity]) => ({ price, quantity }));
205
225
  const asks = asksRaw.map(([price, quantity]) => ({ price, quantity }));
206
226
  return {
227
+ ...metadata,
207
228
  symbol,
208
229
  bestBid: bids[0] ?? null,
209
230
  bestAsk: asks[0] ?? null,
@@ -211,6 +232,12 @@ function normalizeOrderbook(symbol, bidsRaw, asksRaw) {
211
232
  asks
212
233
  };
213
234
  }
235
+ function symbolMetadata(symbol, inputSymbol, originalSymbol) {
236
+ return compactObject({
237
+ inputSymbol: inputSymbol !== symbol ? inputSymbol : undefined,
238
+ originalSymbol: originalSymbol !== symbol ? originalSymbol : undefined
239
+ });
240
+ }
214
241
  function orderPlaceBody(input) {
215
242
  const body = requiredParams(input, ["symbol", "side", "orderType", "clientOrderId"]);
216
243
  const quantity = input.quantity ?? input.qty;
@@ -232,6 +259,9 @@ function orderPlaceBody(input) {
232
259
  if (input.reduceOnly !== undefined) {
233
260
  body.reduceOnly = input.reduceOnly === true ? "TRUE" : "FALSE";
234
261
  }
262
+ if (input.positionSide !== undefined && input.positionSide !== null && input.positionSide !== "") {
263
+ body.positionSide = String(input.positionSide);
264
+ }
235
265
  return {
236
266
  sym: String(body.symbol),
237
267
  side: String(body.side),
@@ -377,6 +407,44 @@ function positionMatchesSymbol(item, symbol) {
377
407
  }
378
408
  return [item.sym, item.symbol, item.instrumentId, item.instrument].some((value) => value === symbol);
379
409
  }
410
+ function extractOrderState(value) {
411
+ if (!isRecord(value)) {
412
+ return undefined;
413
+ }
414
+ const directState = value.orderState ?? value.state ?? value.status;
415
+ if (directState !== undefined && directState !== null && directState !== "") {
416
+ return String(directState);
417
+ }
418
+ if (isRecord(value.data)) {
419
+ const nestedState = extractOrderState(value.data);
420
+ if (nestedState) {
421
+ return nestedState;
422
+ }
423
+ }
424
+ if (isRecord(value.order)) {
425
+ const nestedState = extractOrderState(value.order);
426
+ if (nestedState) {
427
+ return nestedState;
428
+ }
429
+ }
430
+ return undefined;
431
+ }
432
+ function normalizeCancelOrderState(state) {
433
+ if (!state) {
434
+ return undefined;
435
+ }
436
+ const normalized = state.trim().toUpperCase().replace(/[-\s]+/g, "_");
437
+ if (["CANCELLED", "CANCEL_COMPLETE", "CANCEL_SUCCESS", "CANCELED"].includes(normalized)) {
438
+ return "CANCELED";
439
+ }
440
+ if (["CANCEL_PENDING", "PENDING_CANCEL", "CANCELING"].includes(normalized)) {
441
+ return "CANCEL_PENDING";
442
+ }
443
+ return normalized;
444
+ }
445
+ function isCancelTerminalState(state) {
446
+ return state === "CANCELED" || state === "REJECTED" || state === "EXPIRED";
447
+ }
380
448
  function compactObject(input) {
381
449
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== ""));
382
450
  }
@@ -65,10 +65,10 @@ export class RapidXClient {
65
65
  await delay(this.readOnlyRetryDelayMs * (2 ** (attempt - 1)));
66
66
  continue;
67
67
  }
68
- const hint = upstreamHttpErrorHint(path, response.status);
68
+ const hint = upstreamHttpErrorHint(path, response.status, text);
69
69
  throw new ProductError({
70
- code: upstreamHttpErrorCode(response.status),
71
- status: "FAIL",
70
+ code: upstreamHttpErrorCode(path, response.status, text),
71
+ status: upstreamHttpErrorStatus(path, response.status, text),
72
72
  message: hint ? `RapidX upstream error ${response.status}. ${hint}` : `RapidX upstream error ${response.status}`,
73
73
  details: { status: response.status, body: text.slice(0, 500), attempts: attempt, ...(hint ? { hint } : {}) }
74
74
  });
@@ -111,13 +111,19 @@ function resolveBaseUrl(explicitBaseUrl) {
111
111
  function firstNonEmpty(...values) {
112
112
  return values.find((value) => value !== undefined && value.length > 0);
113
113
  }
114
- function upstreamHttpErrorHint(path, status) {
115
- if (path === "/api/v1/account/balance" && status >= 500) {
114
+ function upstreamHttpErrorHint(path, status, body) {
115
+ if (isAccountBalanceScopeError(path, status, body)) {
116
116
  return "account balance mode=account requires account-level credentials; portfolio keys should use mode=portfolio.";
117
117
  }
118
118
  return undefined;
119
119
  }
120
- function upstreamHttpErrorCode(status) {
120
+ function upstreamHttpErrorCode(path, status, body) {
121
+ if (isAccountBalanceScopeError(path, status, body)) {
122
+ return CORE_ERRORS.PERMISSION_SCOPE_ERROR;
123
+ }
124
+ if (status === 404) {
125
+ return CORE_ERRORS.NOT_FOUND;
126
+ }
121
127
  if (status === 429) {
122
128
  return CORE_ERRORS.UPSTREAM_RATE_LIMITED;
123
129
  }
@@ -126,6 +132,25 @@ function upstreamHttpErrorCode(status) {
126
132
  }
127
133
  return CORE_ERRORS.INVALID_CREDENTIAL;
128
134
  }
135
+ function upstreamHttpErrorStatus(path, status, body) {
136
+ if (isAccountBalanceScopeError(path, status, body)) {
137
+ return "PERMISSION_SCOPE_ERROR";
138
+ }
139
+ if (status === 404) {
140
+ return "NOT_FOUND";
141
+ }
142
+ return "FAIL";
143
+ }
144
+ function isAccountBalanceScopeError(path, status, body) {
145
+ if (path !== "/api/v1/account/balance" || status < 400) {
146
+ return false;
147
+ }
148
+ const lowerBody = body.toLowerCase();
149
+ return lowerBody.includes("account-level")
150
+ || lowerBody.includes("permission scope")
151
+ || lowerBody.includes("credential scope")
152
+ || (lowerBody.includes("portfolio") && lowerBody.includes("account"));
153
+ }
129
154
  function delay(ms) {
130
155
  if (ms <= 0) {
131
156
  return Promise.resolve();
@@ -144,9 +169,10 @@ function assertRapidXBusinessSuccess(payload) {
144
169
  const upstreamMessage = typeof body.message === "string" && body.message.length > 0
145
170
  ? `: ${body.message}`
146
171
  : "";
172
+ const classification = classifyBusinessError(code, typeof body.message === "string" ? body.message : "");
147
173
  throw new ProductError({
148
- code: CORE_ERRORS.UPSTREAM_REJECTED,
149
- status: "FAIL",
174
+ code: classification.code,
175
+ status: classification.status,
150
176
  message: `RapidX upstream business error ${code}${upstreamMessage}`,
151
177
  details: {
152
178
  upstreamCode: code,
@@ -154,3 +180,10 @@ function assertRapidXBusinessSuccess(payload) {
154
180
  }
155
181
  });
156
182
  }
183
+ function classifyBusinessError(upstreamCode, upstreamMessage) {
184
+ const lowerMessage = upstreamMessage.toLowerCase();
185
+ if (lowerMessage.includes("not found") || lowerMessage.includes("not exist") || lowerMessage.includes("does not exist")) {
186
+ return { code: CORE_ERRORS.NOT_FOUND, status: "NOT_FOUND" };
187
+ }
188
+ return { code: CORE_ERRORS.UPSTREAM_REJECTED, status: "BUSINESS_ERROR" };
189
+ }
@@ -10,18 +10,22 @@ export function parseRapidXSymbol(symbol) {
10
10
  });
11
11
  }
12
12
  const exchange = exchangeRaw.toUpperCase();
13
- const type = typeRaw.toUpperCase();
13
+ const requestedType = typeRaw.toUpperCase();
14
+ const type = requestedType === "SWAP" && exchange === "OKX" ? "PERP" : requestedType;
14
15
  if (type !== "PERP" && type !== "SPOT") {
15
16
  throw new ProductError({
16
17
  code: "RCORE12001",
17
18
  status: "FAIL",
18
- message: `Unsupported RapidX symbol type: ${typeRaw}`
19
+ message: `Unsupported RapidX symbol type: ${typeRaw}. Use PERP or SPOT; for OKX perpetual swaps, use OKX_PERP_<BASE>_<QUOTE> or OKX_SWAP_<BASE>_<QUOTE> as an input alias.`
19
20
  });
20
21
  }
21
22
  const base = baseRaw.toUpperCase();
22
23
  const quote = quoteRaw.toUpperCase();
23
24
  const isPerp = type === "PERP";
25
+ const canonicalSymbol = `${exchange}_${type}_${base}_${quote}`;
24
26
  const parsed = {
27
+ inputSymbol: symbol,
28
+ canonicalSymbol,
25
29
  exchange,
26
30
  base,
27
31
  quote,
@@ -1,4 +1,5 @@
1
1
  import { SCHEMA_VERSION } from "./types.js";
2
+ import { inputSchemaForName } from "./input-schema.js";
2
3
  export const CAPABILITIES = [
3
4
  { capabilityId: "config.get", cliCommand: "rapidx config get", operationType: "READ", riskLevel: "read", inputSchema: "ConfigGetInput", outputSchema: "ConfigResult", previewRequired: false },
4
5
  { capabilityId: "config.set", cliCommand: "rapidx config set", operationType: "DIAGNOSTIC", riskLevel: "write-config", inputSchema: "ConfigSetInput", outputSchema: "ConfigResult", previewRequired: false },
@@ -38,14 +39,16 @@ export const CAPABILITIES = [
38
39
  { capabilityId: "algo.amend", cliCommand: "rapidx algo amend", mcpTool: "rapidx/algo/amend", operationType: "TRADE_WRITE", riskLevel: "trade-write", inputSchema: "AlgoAmendInput", outputSchema: "AlgoOrderStatus", previewRequired: true },
39
40
  { capabilityId: "algo.cancel", cliCommand: "rapidx algo cancel", mcpTool: "rapidx/algo/cancel", operationType: "TRADE_WRITE", riskLevel: "trade-write", inputSchema: "AlgoCancelInput", outputSchema: "AlgoOrderStatus", previewRequired: true },
40
41
  { capabilityId: "algo.list", cliCommand: "rapidx algo list", mcpTool: "rapidx/algo/list", operationType: "TRADE_READ", riskLevel: "trade-read", inputSchema: "AlgoListInput", outputSchema: "AlgoList", previewRequired: false },
41
- { capabilityId: "trading.verify", cliCommand: "rapidx self-check trade-verify", mcpTool: "rapidx/trading-verification", operationType: "TRADE_WRITE", riskLevel: "critical-trade-write", inputSchema: "TradingVerificationInput", outputSchema: "TradingVerificationReport", previewRequired: false },
42
- { capabilityId: "trading.verify-live", cliCommand: "rapidx trade verify-live", mcpTool: "rapidx/trade/verify-live", operationType: "TRADE_WRITE", riskLevel: "critical-trade-write", inputSchema: "TradingVerificationInput", outputSchema: "TradingVerificationReport", previewRequired: false },
42
+ { capabilityId: "trading.verify", cliCommand: "rapidx self-check trade-verify", mcpTool: "rapidx/trading-verification", operationType: "TRADE_WRITE", riskLevel: "critical-trade-write", inputSchema: "TradingVerificationInput", outputSchema: "TradingVerificationReport", previewRequired: false, containsRealOrder: true, requiresExplicitHumanConfirmation: true, confirmationMode: "internal-preview-and-parameter-bound-consent" },
43
+ { capabilityId: "trading.verify-live", cliCommand: "rapidx trade verify-live", mcpTool: "rapidx/trade/verify-live", operationType: "TRADE_WRITE", riskLevel: "critical-trade-write", inputSchema: "TradingVerificationInput", outputSchema: "TradingVerificationReport", previewRequired: false, containsRealOrder: true, requiresExplicitHumanConfirmation: true, confirmationMode: "internal-preview-and-parameter-bound-consent" },
43
44
  { capabilityId: "invocation.check", cliCommand: "rapidx invocation check", operationType: "DIAGNOSTIC", riskLevel: "read", inputSchema: "InvocationCheckInput", outputSchema: "InvocationCompliance", previewRequired: false }
44
45
  ];
45
46
  export function getSchemaResult() {
47
+ const schemaNames = [...new Set(CAPABILITIES.map((capability) => capability.inputSchema))].sort();
46
48
  return {
47
49
  schemaVersion: SCHEMA_VERSION,
48
- capabilities: [...CAPABILITIES]
50
+ capabilities: [...CAPABILITIES],
51
+ inputSchemas: Object.fromEntries(schemaNames.map((name) => [name, inputSchemaForName(name)]))
49
52
  };
50
53
  }
51
54
  export function listMcpCapabilities() {
@@ -1,7 +1,7 @@
1
1
  import { SCHEMA_VERSION } from "./types.js";
2
2
  import { RAPIDX_VERSION } from "../version.js";
3
3
  export const RAPIDX_SKILLS_DISTRIBUTION = "github";
4
- export const RAPIDX_SKILLS_VERSION = "1.0.1";
4
+ export const RAPIDX_SKILLS_VERSION = "1.0.3";
5
5
  export const RAPIDX_SKILLS_SCHEMA_VERSION = "1.0.0";
6
6
  export function buildCompatibilityReport(input = {}) {
7
7
  const checks = [
@@ -1,11 +1,11 @@
1
- import { ProductError } from "../errors/product-error.js";
1
+ import { CORE_ERRORS, ProductError } from "../errors/product-error.js";
2
2
  const stringSchema = { type: "string" };
3
3
  const booleanSchema = { type: "boolean" };
4
4
  const numberSchema = { type: "number" };
5
5
  const symbolSchema = {
6
6
  type: "string",
7
- description: "RapidX symbol, for example BINANCE_PERP_BTC_USDT or OKX_SWAP_BTC_USDT.",
8
- examples: ["BINANCE_PERP_BTC_USDT", "OKX_SWAP_BTC_USDT"]
7
+ description: "RapidX symbol. Recommended format: BINANCE_PERP_<BASE>_<QUOTE>. OKX_PERP_<BASE>_<QUOTE> is supported; OKX_SWAP_<BASE>_<QUOTE> is accepted as an input alias and normalized to OKX_PERP.",
8
+ examples: ["BINANCE_PERP_BTC_USDT", "BINANCE_PERP_ETH_USDT"]
9
9
  };
10
10
  const sideSchema = {
11
11
  type: "string",
@@ -15,7 +15,7 @@ const sideSchema = {
15
15
  const orderTypeSchema = {
16
16
  type: "string",
17
17
  enum: ["LIMIT", "MARKET"],
18
- description: "Order type. LIMIT is the expected default for agent-driven trading; MARKET may be blocked by safety policy."
18
+ description: "Order type. MARKET is allowed when the write operation passes preview and consent checks; preview includes immediate-execution and slippage risk notes."
19
19
  };
20
20
  const priceSchema = {
21
21
  type: "string",
@@ -46,6 +46,28 @@ const clientOrderIdSchema = {
46
46
  description: "Caller-generated idempotency key for the order request.",
47
47
  examples: ["agent-test-20260529-001"]
48
48
  };
49
+ const explicitUserConsentSchema = {
50
+ type: "boolean",
51
+ description: "Must be true only after the human user explicitly authorizes this exact live verification request."
52
+ };
53
+ const acceptedRiskTextSchema = {
54
+ type: "string",
55
+ description: "Human confirmation text bound to the exact symbol, side, maxNotional, real-order risk, and cancel/cleanup behavior.",
56
+ examples: ["I authorize a real verification order for BINANCE_PERP_BTC_USDT BUY maxNotional 10 with cancel cleanup."]
57
+ };
58
+ const automationModeSchema = {
59
+ type: "boolean",
60
+ description: "Set true only when the human user has explicitly enabled RapidX automation mode in chat for this preview."
61
+ };
62
+ const automationConsentTextSchema = {
63
+ type: "string",
64
+ description: "Human chat authorization text for automation mode. The agent must not invent this text.",
65
+ examples: ["I enable RapidX automation mode for BINANCE_PERP_BTC_USDT with maxNotional 100 and accept automated preview-submit execution."]
66
+ };
67
+ const automationScopeSchema = {
68
+ type: "string",
69
+ description: "Optional automation scope label, for example single-preview, strategy-session, or the user's exact scope phrase."
70
+ };
49
71
  const previewIdSchema = {
50
72
  type: "string",
51
73
  description: "Preview id returned by the matching preview tool or command.",
@@ -73,6 +95,7 @@ const accountBalanceModeSchema = {
73
95
  };
74
96
  const positionSideSchema = {
75
97
  type: "string",
98
+ enum: ["LONG", "SHORT"],
76
99
  description: "Position side when the account is in hedge mode, for example LONG or SHORT. Omit in one-way NET mode unless RapidX returns a specific side."
77
100
  };
78
101
  const closePositionReduceOnlySchema = {
@@ -112,6 +135,7 @@ export function inputSchemaForName(name) {
112
135
  price: priceSchema,
113
136
  quantity: quantitySchema,
114
137
  amount: amountSchema,
138
+ positionSide: positionSideSchema,
115
139
  timeInForce: stringSchema,
116
140
  postOnly: booleanSchema,
117
141
  reduceOnly: booleanSchema,
@@ -131,6 +155,7 @@ export function inputSchemaForName(name) {
131
155
  takeProfitLimitPrice: stringSchema,
132
156
  algoType: stringSchema,
133
157
  consentId: stringSchema,
158
+ ...automationProperties(),
134
159
  ...compatibilityProperties()
135
160
  }, ["targetCapabilityId"]);
136
161
  case "PreviewOrderInput":
@@ -138,6 +163,7 @@ export function inputSchemaForName(name) {
138
163
  ...objectSchema({
139
164
  ...tradeCreateProperties(),
140
165
  consentId: stringSchema,
166
+ ...automationProperties(),
141
167
  ...compatibilityProperties()
142
168
  }, ["symbol", "side", "orderType", "maxNotional", "clientOrderId"]),
143
169
  oneOf: [{ required: ["quantity"] }, { required: ["amount"] }]
@@ -157,7 +183,7 @@ export function inputSchemaForName(name) {
157
183
  };
158
184
  case "AmendOrderPreviewInput":
159
185
  return {
160
- ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, price: priceSchema, quantity: quantitySchema, ...compatibilityProperties() }),
186
+ ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, price: priceSchema, quantity: quantitySchema, ...automationProperties(), ...compatibilityProperties() }),
161
187
  allOf: [
162
188
  { anyOf: [{ required: ["orderId"] }, { required: ["clientOrderId"] }] },
163
189
  { anyOf: [{ required: ["price"] }, { required: ["quantity"] }] }
@@ -165,7 +191,7 @@ export function inputSchemaForName(name) {
165
191
  };
166
192
  case "CancelOrderPreviewInput":
167
193
  return {
168
- ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, ...compatibilityProperties() }),
194
+ ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, ...automationProperties(), ...compatibilityProperties() }),
169
195
  anyOf: [{ required: ["orderId"] }, { required: ["clientOrderId"] }]
170
196
  };
171
197
  case "AmendOrderInput":
@@ -184,7 +210,7 @@ export function inputSchemaForName(name) {
184
210
  case "ClosePositionInput":
185
211
  return objectSchema({ symbol: symbolSchema, positionSide: positionSideSchema, reduceOnly: closePositionReduceOnlySchema, maxNotional: maxNotionalSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["symbol", "reduceOnly", "maxNotional", "previewId", "continueConsentId"]);
186
212
  case "SetLeverageInput":
187
- return objectSchema({ symbol: symbolSchema, leverage: numberSchema, positionSide: stringSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["symbol", "leverage", "previewId", "continueConsentId"]);
213
+ return objectSchema({ symbol: symbolSchema, leverage: numberSchema, positionSide: positionSideSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["symbol", "leverage", "previewId", "continueConsentId"]);
188
214
  case "SetPositionModeInput":
189
215
  return objectSchema({ mode: { type: "string", enum: ["BOTH", "NET"] }, exchange: stringSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["mode", "exchange", "previewId", "continueConsentId"]);
190
216
  case "AlgoPlaceInput":
@@ -192,7 +218,6 @@ export function inputSchemaForName(name) {
192
218
  ...objectSchema({
193
219
  ...tradeCreateProperties(),
194
220
  algoType: stringSchema,
195
- positionSide: stringSchema,
196
221
  conditionType: algoConditionTypeSchema,
197
222
  triggerPrice: stringSchema,
198
223
  triggerType: triggerTypeSchema,
@@ -212,7 +237,7 @@ export function inputSchemaForName(name) {
212
237
  "previewId",
213
238
  "continueConsentId"
214
239
  ]),
215
- oneOf: [{ required: ["quantity"] }, { required: ["amount"] }]
240
+ anyOf: [{ required: ["quantity"] }, { required: ["amount"] }, { required: ["conditionType"] }]
216
241
  };
217
242
  case "AlgoAmendInput":
218
243
  return objectSchema({
@@ -230,7 +255,15 @@ export function inputSchemaForName(name) {
230
255
  case "AlgoCancelInput":
231
256
  return objectSchema({ algoOrderId: stringSchema, clientOrderId: clientOrderIdSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["previewId", "continueConsentId"]);
232
257
  case "TradingVerificationInput":
233
- return objectSchema({ symbol: symbolSchema, side: sideSchema, maxNotional: maxNotionalSchema, clientOrderId: clientOrderIdSchema, explicitUserConsent: booleanSchema, acceptedRiskText: stringSchema }, ["symbol", "side", "maxNotional", "clientOrderId"]);
258
+ return objectSchema({
259
+ symbol: symbolSchema,
260
+ side: sideSchema,
261
+ maxNotional: maxNotionalSchema,
262
+ clientOrderId: clientOrderIdSchema,
263
+ positionSide: positionSideSchema,
264
+ explicitUserConsent: explicitUserConsentSchema,
265
+ acceptedRiskText: acceptedRiskTextSchema
266
+ }, ["symbol", "side", "maxNotional", "clientOrderId", "explicitUserConsent", "acceptedRiskText"]);
234
267
  case "ConfigGetInput":
235
268
  case "ConfigUnsetInput":
236
269
  return objectSchema({ key: stringSchema }, ["key"]);
@@ -310,15 +343,27 @@ export function assertInputMatchesSchema(schemaName, input, options = {}) {
310
343
  collectAnyOfProblem(group.anyOf, input, problems);
311
344
  }
312
345
  }
346
+ collectSchemaSpecificProblems(schemaName, input, problems);
313
347
  if (problems.length > 0) {
314
348
  throw new ProductError({
315
- code: "RCORE00001",
316
- status: "FAIL",
349
+ code: CORE_ERRORS.SCHEMA_INVALID,
350
+ status: "INVALID_INPUT",
317
351
  message: `Input schema validation failed: ${problems.join("; ")}.`
318
352
  });
319
353
  }
320
354
  void allowedFields;
321
355
  }
356
+ function collectSchemaSpecificProblems(schemaName, input, problems) {
357
+ if (schemaName !== "AlgoPlaceInput") {
358
+ return;
359
+ }
360
+ if (String(input.conditionType ?? "").toUpperCase() !== "ENTIRE_CLOSE_POSITION") {
361
+ return;
362
+ }
363
+ if (!hasValue(input.stopLossPrice) && !hasValue(input.takeProfitPrice)) {
364
+ problems.push("TPSL ENTIRE_CLOSE_POSITION requires stopLossPrice or takeProfitPrice");
365
+ }
366
+ }
322
367
  function collectAnyOfProblem(rules, input, problems) {
323
368
  if (!rules.some((candidate) => (candidate.required ?? []).every((field) => hasValue(input[field])))) {
324
369
  const labels = rules.map((candidate) => (candidate.required ?? []).join(", ")).join(" or ");
@@ -346,7 +391,15 @@ function tradeCreateProperties() {
346
391
  postOnly: booleanSchema,
347
392
  reduceOnly: booleanSchema,
348
393
  maxNotional: maxNotionalSchema,
349
- clientOrderId: clientOrderIdSchema
394
+ clientOrderId: clientOrderIdSchema,
395
+ positionSide: positionSideSchema
396
+ };
397
+ }
398
+ function automationProperties() {
399
+ return {
400
+ automationMode: automationModeSchema,
401
+ automationConsentText: automationConsentTextSchema,
402
+ automationScope: automationScopeSchema
350
403
  };
351
404
  }
352
405
  function compatibilityProperties() {
@@ -27,6 +27,8 @@ export const CORE_ERRORS = {
27
27
  MISSING_API_HOST: "RCORE01003",
28
28
  SECRET_REDACTION_RISK: "RCORE90002",
29
29
  UPSTREAM_REJECTED: "RCORE22001",
30
+ PERMISSION_SCOPE_ERROR: "RCORE01004",
31
+ NOT_FOUND: "RCORE22004",
30
32
  UPSTREAM_TIMEOUT: "RCORE23002",
31
33
  UPSTREAM_RATE_LIMITED: "RCORE23003",
32
34
  SCHEMA_INVALID: "RCORE30001"
@@ -16,6 +16,7 @@ export * from "./client/symbol.js";
16
16
  export * from "./safety/policy.js";
17
17
  export * from "./safety/raw-api.js";
18
18
  export * from "./trading/preview.js";
19
+ export * from "./trading/preview-preflight.js";
19
20
  export * from "./update/check-update.js";
20
21
  export * from "./self-check/run-self-check.js";
21
22
  export * from "./self-check/live-read-only-probes.js";
@@ -78,8 +78,8 @@ export function evaluateSafety(capability, input, policy, state = makeSafetyStat
78
78
  return { allowed: false, code: "RCORE00001", reason: "mode must be BOTH or NET.", paramsHash };
79
79
  }
80
80
  }
81
- if (String(input.orderType ?? "").toUpperCase() === "MARKET" && (policy.marketOrderPolicy ?? "BLOCK_BY_DEFAULT") === "BLOCK_BY_DEFAULT") {
82
- return { allowed: false, code: "RCORE20002", reason: "Market orders are blocked by default.", paramsHash };
81
+ if (String(input.orderType ?? "").toUpperCase() === "MARKET" && (policy.marketOrderPolicy ?? "ALLOW") === "BLOCK_BY_DEFAULT") {
82
+ return { allowed: false, code: "RCORE20002", reason: "Market orders are blocked by safety policy.", paramsHash };
83
83
  }
84
84
  if (requiresNotionalLimit(capability)) {
85
85
  const maxNotional = input.maxNotional ?? policy.maxNotional;
@@ -114,7 +114,7 @@ function requiredTradeFields(capability, input) {
114
114
  requireString(input, "orderType", missing);
115
115
  requireString(input, "clientOrderId", missing);
116
116
  requireString(input, "maxNotional", missing);
117
- if (!hasString(input, "quantity") && !hasString(input, "amount")) {
117
+ if (!hasString(input, "quantity") && !hasString(input, "amount") && !isEntireCloseAlgo(capability, input)) {
118
118
  missing.push("quantity or amount");
119
119
  }
120
120
  if (String(input.orderType ?? "").toUpperCase() === "LIMIT") {
@@ -194,6 +194,9 @@ function stringValue(value) {
194
194
  function hasString(input, field) {
195
195
  return typeof input[field] === "string" && input[field].length > 0;
196
196
  }
197
+ function isEntireCloseAlgo(capability, input) {
198
+ return capability.capabilityId === "algo.place" && String(input.conditionType ?? "").toUpperCase() === "ENTIRE_CLOSE_POSITION";
199
+ }
197
200
  function requireString(input, field, missing) {
198
201
  if (!hasString(input, field)) {
199
202
  missing.push(field);