@liquiditytech/rapidx-cli 1.0.28 → 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
  }
@@ -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 },
@@ -43,9 +44,11 @@ export const CAPABILITIES = [
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.2";
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,10 +1,10 @@
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. Recommended format: BINANCE_PERP_<BASE>_<QUOTE>.",
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
8
  examples: ["BINANCE_PERP_BTC_USDT", "BINANCE_PERP_ETH_USDT"]
9
9
  };
10
10
  const sideSchema = {
@@ -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",
@@ -55,6 +55,19 @@ const acceptedRiskTextSchema = {
55
55
  description: "Human confirmation text bound to the exact symbol, side, maxNotional, real-order risk, and cancel/cleanup behavior.",
56
56
  examples: ["I authorize a real verification order for BINANCE_PERP_BTC_USDT BUY maxNotional 10 with cancel cleanup."]
57
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
+ };
58
71
  const previewIdSchema = {
59
72
  type: "string",
60
73
  description: "Preview id returned by the matching preview tool or command.",
@@ -82,6 +95,7 @@ const accountBalanceModeSchema = {
82
95
  };
83
96
  const positionSideSchema = {
84
97
  type: "string",
98
+ enum: ["LONG", "SHORT"],
85
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."
86
100
  };
87
101
  const closePositionReduceOnlySchema = {
@@ -121,6 +135,7 @@ export function inputSchemaForName(name) {
121
135
  price: priceSchema,
122
136
  quantity: quantitySchema,
123
137
  amount: amountSchema,
138
+ positionSide: positionSideSchema,
124
139
  timeInForce: stringSchema,
125
140
  postOnly: booleanSchema,
126
141
  reduceOnly: booleanSchema,
@@ -140,6 +155,7 @@ export function inputSchemaForName(name) {
140
155
  takeProfitLimitPrice: stringSchema,
141
156
  algoType: stringSchema,
142
157
  consentId: stringSchema,
158
+ ...automationProperties(),
143
159
  ...compatibilityProperties()
144
160
  }, ["targetCapabilityId"]);
145
161
  case "PreviewOrderInput":
@@ -147,6 +163,7 @@ export function inputSchemaForName(name) {
147
163
  ...objectSchema({
148
164
  ...tradeCreateProperties(),
149
165
  consentId: stringSchema,
166
+ ...automationProperties(),
150
167
  ...compatibilityProperties()
151
168
  }, ["symbol", "side", "orderType", "maxNotional", "clientOrderId"]),
152
169
  oneOf: [{ required: ["quantity"] }, { required: ["amount"] }]
@@ -166,7 +183,7 @@ export function inputSchemaForName(name) {
166
183
  };
167
184
  case "AmendOrderPreviewInput":
168
185
  return {
169
- ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, price: priceSchema, quantity: quantitySchema, ...compatibilityProperties() }),
186
+ ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, price: priceSchema, quantity: quantitySchema, ...automationProperties(), ...compatibilityProperties() }),
170
187
  allOf: [
171
188
  { anyOf: [{ required: ["orderId"] }, { required: ["clientOrderId"] }] },
172
189
  { anyOf: [{ required: ["price"] }, { required: ["quantity"] }] }
@@ -174,7 +191,7 @@ export function inputSchemaForName(name) {
174
191
  };
175
192
  case "CancelOrderPreviewInput":
176
193
  return {
177
- ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, ...compatibilityProperties() }),
194
+ ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, ...automationProperties(), ...compatibilityProperties() }),
178
195
  anyOf: [{ required: ["orderId"] }, { required: ["clientOrderId"] }]
179
196
  };
180
197
  case "AmendOrderInput":
@@ -193,7 +210,7 @@ export function inputSchemaForName(name) {
193
210
  case "ClosePositionInput":
194
211
  return objectSchema({ symbol: symbolSchema, positionSide: positionSideSchema, reduceOnly: closePositionReduceOnlySchema, maxNotional: maxNotionalSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["symbol", "reduceOnly", "maxNotional", "previewId", "continueConsentId"]);
195
212
  case "SetLeverageInput":
196
- 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"]);
197
214
  case "SetPositionModeInput":
198
215
  return objectSchema({ mode: { type: "string", enum: ["BOTH", "NET"] }, exchange: stringSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["mode", "exchange", "previewId", "continueConsentId"]);
199
216
  case "AlgoPlaceInput":
@@ -201,7 +218,6 @@ export function inputSchemaForName(name) {
201
218
  ...objectSchema({
202
219
  ...tradeCreateProperties(),
203
220
  algoType: stringSchema,
204
- positionSide: stringSchema,
205
221
  conditionType: algoConditionTypeSchema,
206
222
  triggerPrice: stringSchema,
207
223
  triggerType: triggerTypeSchema,
@@ -221,7 +237,7 @@ export function inputSchemaForName(name) {
221
237
  "previewId",
222
238
  "continueConsentId"
223
239
  ]),
224
- oneOf: [{ required: ["quantity"] }, { required: ["amount"] }]
240
+ anyOf: [{ required: ["quantity"] }, { required: ["amount"] }, { required: ["conditionType"] }]
225
241
  };
226
242
  case "AlgoAmendInput":
227
243
  return objectSchema({
@@ -244,6 +260,7 @@ export function inputSchemaForName(name) {
244
260
  side: sideSchema,
245
261
  maxNotional: maxNotionalSchema,
246
262
  clientOrderId: clientOrderIdSchema,
263
+ positionSide: positionSideSchema,
247
264
  explicitUserConsent: explicitUserConsentSchema,
248
265
  acceptedRiskText: acceptedRiskTextSchema
249
266
  }, ["symbol", "side", "maxNotional", "clientOrderId", "explicitUserConsent", "acceptedRiskText"]);
@@ -326,15 +343,27 @@ export function assertInputMatchesSchema(schemaName, input, options = {}) {
326
343
  collectAnyOfProblem(group.anyOf, input, problems);
327
344
  }
328
345
  }
346
+ collectSchemaSpecificProblems(schemaName, input, problems);
329
347
  if (problems.length > 0) {
330
348
  throw new ProductError({
331
- code: "RCORE00001",
332
- status: "FAIL",
349
+ code: CORE_ERRORS.SCHEMA_INVALID,
350
+ status: "INVALID_INPUT",
333
351
  message: `Input schema validation failed: ${problems.join("; ")}.`
334
352
  });
335
353
  }
336
354
  void allowedFields;
337
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
+ }
338
367
  function collectAnyOfProblem(rules, input, problems) {
339
368
  if (!rules.some((candidate) => (candidate.required ?? []).every((field) => hasValue(input[field])))) {
340
369
  const labels = rules.map((candidate) => (candidate.required ?? []).join(", ")).join(" or ");
@@ -362,7 +391,15 @@ function tradeCreateProperties() {
362
391
  postOnly: booleanSchema,
363
392
  reduceOnly: booleanSchema,
364
393
  maxNotional: maxNotionalSchema,
365
- clientOrderId: clientOrderIdSchema
394
+ clientOrderId: clientOrderIdSchema,
395
+ positionSide: positionSideSchema
396
+ };
397
+ }
398
+ function automationProperties() {
399
+ return {
400
+ automationMode: automationModeSchema,
401
+ automationConsentText: automationConsentTextSchema,
402
+ automationScope: automationScopeSchema
366
403
  };
367
404
  }
368
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);
@@ -13,6 +13,7 @@ export function buildLiveTradingVerificationProbes(input = {}) {
13
13
  orderType: "LIMIT",
14
14
  price: orderInput.price,
15
15
  quantity: orderInput.quantity,
16
+ positionSide: orderInput.positionSide,
16
17
  maxNotional: orderInput.maxNotional,
17
18
  clientOrderId: orderInput.clientOrderId,
18
19
  postOnly: true,
@@ -34,7 +35,7 @@ export function buildLiveTradingVerificationProbes(input = {}) {
34
35
  previewId: `internal-${order.orderId}`,
35
36
  continueConsentId: continueConsentId(order.orderId, "cancel")
36
37
  });
37
- const data = extractDataObject(result);
38
+ const data = extractDataObject(extractWrappedResponse(result));
38
39
  const status = normalizeOrderStatus(stringField(data, "orderState") ?? stringField(data, "action") ?? "");
39
40
  if (status === "CANCELED") {
40
41
  return { orderId: order.orderId, status };
@@ -186,6 +187,12 @@ function extractDataObject(result) {
186
187
  }
187
188
  return {};
188
189
  }
190
+ function extractWrappedResponse(result) {
191
+ if (result && typeof result === "object" && !Array.isArray(result) && Object.prototype.hasOwnProperty.call(result, "response")) {
192
+ return result.response;
193
+ }
194
+ return result;
195
+ }
189
196
  function extractArrayData(result) {
190
197
  const data = extractData(result);
191
198
  if (Array.isArray(data)) {