@michaleffffff/mcp-trading-server 3.0.31 → 3.1.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.1.1 - 2026-03-23
4
+ ### Changed
5
+ - Upgraded the active SDK baseline to `@myx-trade/sdk@^1.0.4`.
6
+ - Synced operator-facing docs to the current baseline:
7
+ - `README.md` now references `@myx-trade/sdk@^1.0.4`.
8
+ - `mcp_config_guide.md` no longer refers to a separate Beta environment for LP / create-market flows.
9
+ - Current testnet live regression notes now align with the post-upgrade trade/account behavior.
10
+ - Deduplicated SDK compatibility helpers used by market lookup / `get_base_detail` without changing runtime behavior.
11
+
12
+ ## 3.1.0 - 2026-03-20
13
+ ### Changed
14
+ - 发布 `3.1.0`,统一版本元数据到新的主次版本号。
15
+ - 运行环境现在只区分测试环境与正式环境,不再走 beta host / beta 地址分支。
16
+ - 修复测试环境下的市场发现链路,`list_pools` 恢复返回当前链池列表,`get_all_tickers` 在主接口不可用时继续走 fallback。
17
+ - `open_position_simple` 在用户提供 `size + price + leverage` 但未提供 `collateralAmount` 时,改为自动反推保证金;若用户传入的 `collateralAmount` 明显不足,则提前返回清晰错误。
18
+ - `execute_trade` / `close_position` 不再对外暴露 `timeInForce`;MCP 内部固定使用 `IOC (0)`,避免把一个不可配置字段误导成可配置参数。
19
+
3
20
  ## 3.0.31 - 2026-03-20
4
21
  ### Fixed
5
22
  - 修复 `normalizeSlippageRatio` 在处理 1% 到 100% 之间数值时的歧义问题。
package/README.md CHANGED
@@ -6,11 +6,12 @@ A production-ready MCP (Model Context Protocol) server for deep integration with
6
6
 
7
7
  # Release Notes
8
8
 
9
- - **Current release: 3.0.24**
10
- - **SDK baseline**: `@myx-trade/sdk@^1.0.4-beta.4` compatibility completed.
9
+ - **Current release: 3.1.0**
10
+ - **SDK baseline**: `@myx-trade/sdk@^1.0.4` compatibility completed.
11
11
  - **Refinement**: Consolidated 40+ specialized tools into ~26 high-level unified tools.
12
12
  - **Improved UX**: Enhanced AI parameter parsing, automated unit conversion, and structured error reporting.
13
13
  - **Safety refresh**: Docs and prompt guidance now reflect explicit-price execution for `open_position_simple`, exact-approval defaults, notional-based fee checks, TP/SL semantic validation, and LP preview fail-close behavior.
14
+ - **Regression status**: Current trade/account compatibility was verified against the `1.0.4` baseline, including testnet live checks for `get_trade_flow`, `get_base_detail`, `account_deposit`, `account_withdraw`, `cancel_orders`, `manage_tp_sl`, and `close_position`.
14
15
  - **Breaking changes**: Many low-level tools (e.g., `get_market_price`, `get_oracle_price`, `get_open_orders`) have been merged into unified counterparts.
15
16
 
16
17
  ---
@@ -37,7 +38,6 @@ CHAIN_ID=...
37
38
  BROKER_ADDRESS=0x...
38
39
  QUOTE_TOKEN_ADDRESS=0x...
39
40
  QUOTE_TOKEN_DECIMALS=...
40
- IS_BETA_MODE=true
41
41
  ```
42
42
 
43
43
  ## Testnet `MYXBroker` Reference
@@ -46,7 +46,6 @@ IS_BETA_MODE=true
46
46
  - Linea test: `0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2`
47
47
 
48
48
  Use the broker that matches your active RPC and chain configuration.
49
- If `IS_BETA_MODE` is omitted, MCP now auto-detects beta mode for the two testnet brokers above.
50
49
 
51
50
  ---
52
51
 
@@ -91,7 +90,6 @@ If `IS_BETA_MODE` is omitted, MCP now auto-detects beta mode for the two testnet
91
90
 
92
91
  - **Explicit execution price**: `open_position_simple` no longer auto-fills a fresh Oracle price for `MARKET` orders; provide `price` explicitly when opening a position.
93
92
  - **SDK-delegated funding delta**: MCP no longer performs its own increase-order margin/deposit reconciliation. New increase orders delegate collateral shortfall handling to SDK `createIncreaseOrder`.
94
- - **Beta broker compatibility**: current Arbitrum Sepolia / Linea Sepolia test brokers auto-enable beta mode when `IS_BETA_MODE` is unset.
95
93
  - **Exact approvals by default**: local fallback flows now prefer exact approvals instead of implicit unlimited approval.
96
94
  - **Size semantics**: `size` always means base-asset quantity, not USD notional.
97
95
  - **Pre-check semantics**: `check_account_ready` now uses SDK `getAvailableMarginBalance`. When that read fails, the response marks `summary.degraded=true` and includes diagnostics instead of silently trusting stale `freeMargin`.
@@ -137,8 +135,9 @@ Use these conventions when generating tool arguments:
137
135
  - `status`: `OPEN` / `HISTORY` / `ALL` are canonical; lowercase is tolerated
138
136
  - `poolType`: `BASE` / `QUOTE` are canonical; lowercase is tolerated
139
137
  - `orderType`: `MARKET` / `LIMIT` / `STOP` / `CONDITIONAL`
140
- - `timeInForce`: SDK `v1.0.4-beta.4` currently supports `IOC` only, so use `0` or `"IOC"`
138
+ - Trading MCP tools now fix `timeInForce` internally to `IOC (0)`; do not pass this field
141
139
  - `size`: base token quantity, not USD notional; expected order value is usually `collateralAmount * leverage`
140
+ - `open_position_simple`: if `size + price + leverage` are provided, MCP can auto-compute `collateralAmount`; if `collateralAmount` is also provided, MCP validates it against the requested notional
142
141
  - `executionFeeToken`: must be a real token address; zero address is rejected. Use the pool `quoteToken`
143
142
  - `slippagePct`: trading tools use 4-decimal raw units where `100 = 1.00%` and `50 = 0.50%`
144
143
  - Human units: `"100"` means 100 USDC or 100 token units depending on field
package/TOOL_EXAMPLES.md CHANGED
@@ -81,7 +81,7 @@ Recommended high-level entry tool.
81
81
 
82
82
  `marketId` is optional on `open_position_simple`. If supplied, it is validated against the market resolved from `poolId` or `keyword`.
83
83
  `size` is always the base-asset quantity, not the USD notional. For example, a 500 USD order at price 1200 implies `size ≈ 0.416666...`.
84
- `collateralAmount` remains required on `open_position_simple`; if omitted, MCP now returns an actionable suggestion instead of a generic parse error.
84
+ If `size + price + leverage` are provided on `open_position_simple`, MCP can now auto-compute `collateralAmount`. If you also provide `collateralAmount`, MCP validates that it is sufficient for the requested position.
85
85
  Provide `price` explicitly for both `MARKET` and `LIMIT/STOP` on `open_position_simple`. MCP no longer auto-fills a fresh Oracle price for `MARKET`.
86
86
  Auto-computed `tradingFee` follows notional semantics rather than raw collateral-only estimation.
87
87
  `autoDeposit` is now a deprecated compatibility flag. MCP delegates increase-order funding deltas to the SDK during `createIncreaseOrder`.
@@ -116,13 +116,12 @@ Low-level increase-order tool when you want full control.
116
116
  "price": "2500",
117
117
  "size": "0.2",
118
118
  "collateralAmount": "100",
119
- "timeInForce": 0,
120
119
  "leverage": 5
121
120
  }
122
121
  }
123
122
  ```
124
123
 
125
- `timeInForce` should be `0` (or `"IOC"` in string form) for SDK `v1.0.4-beta.4`.
124
+ `timeInForce` is fixed internally to `IOC (0)` in MCP trading tools; do not pass this field.
126
125
  `executionFeeToken` must be a real token address; do not pass the zero address. Use the pool `quoteToken`.
127
126
  If `positionId` is supplied on increase flows, `direction` must remain consistent with the live position.
128
127
 
@@ -140,7 +139,6 @@ Close or reduce a position. Use `ALL` for a full close.
140
139
  "collateralAmount": "ALL",
141
140
  "size": "ALL",
142
141
  "price": "2200",
143
- "timeInForce": 0,
144
142
  "postOnly": false,
145
143
  "slippagePct": "50",
146
144
  "executionFeeToken": "0x...",
@@ -2,10 +2,6 @@ import { MyxClient } from "@myx-trade/sdk";
2
2
  import { JsonRpcProvider, Wallet } from "ethers";
3
3
  import { normalizeAddress } from "../utils/address.js";
4
4
  let cached = null;
5
- const BETA_BROKERS_BY_CHAIN = {
6
- 421614: ["0x895c4ae2a22bb26851011d733a9355f663a1f939"],
7
- 59141: ["0x634efdc9dc76d7abf6e49279875a31b02e9891e2"],
8
- };
9
5
  function getDefaultBrokerByChainId(chainId) {
10
6
  // Testnet mappings
11
7
  if (chainId === 421614)
@@ -22,14 +18,6 @@ function getDefaultQuoteTokenByChainId(chainId) {
22
18
  return "0xD984fd34f91F92DA0586e1bE82E262fF27DC431b"; // Linea Sepolia
23
19
  return "0xD984fd34f91F92DA0586e1bE82E262fF27DC431b";
24
20
  }
25
- function resolveIsBetaMode(chainId, brokerAddressRaw) {
26
- const explicit = String(process.env.IS_BETA_MODE ?? "").trim().toLowerCase();
27
- if (explicit === "true")
28
- return true;
29
- if (explicit === "false")
30
- return false;
31
- return Boolean(BETA_BROKERS_BY_CHAIN[chainId]?.includes(String(brokerAddressRaw ?? "").trim().toLowerCase()));
32
- }
33
21
  export async function resolveClient() {
34
22
  if (cached)
35
23
  return cached;
@@ -50,7 +38,6 @@ export async function resolveClient() {
50
38
  throw new Error("QUOTE_TOKEN_ADDRESS env var is required.");
51
39
  const brokerAddress = normalizeAddress(brokerAddressRaw, "BROKER_ADDRESS");
52
40
  const quoteToken = normalizeAddress(quoteTokenRaw, "QUOTE_TOKEN_ADDRESS");
53
- const isBetaMode = resolveIsBetaMode(chainId, brokerAddress);
54
41
  const provider = new JsonRpcProvider(rpcUrl);
55
42
  const signer = new Wallet(privateKey, provider);
56
43
  // Inject the EIP-1193 mock so SDK can sign transactions seamlessly
@@ -75,7 +62,7 @@ export async function resolveClient() {
75
62
  signer: signer,
76
63
  brokerAddress,
77
64
  isTestnet,
78
- isBetaMode,
65
+ isBetaMode: false,
79
66
  walletClient: walletClient
80
67
  });
81
68
  cached = { client, address: signer.address, signer, chainId, quoteToken, quoteDecimals };
@@ -12,7 +12,7 @@ export const tradingGuidePrompt = {
12
12
  content: {
13
13
  type: "text",
14
14
  text: `
15
- # MYX Trading MCP Best Practices (v3.0.31)
15
+ # MYX Trading MCP Best Practices (v3.1.0)
16
16
 
17
17
  You are an expert crypto trader using the MYX Protocol. To ensure successful execution and safe handling of user funds, follow these patterns:
18
18
 
@@ -34,9 +34,9 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
34
34
  - **LP Strategy**: Use \`get_my_lp_holdings\` to monitor liquidity positions. Naming follows \`mBASE.QUOTE\` (e.g., \`mBTC.USDC\`).
35
35
  - **Enum Tolerance**: The server tolerates common lowercase or alias inputs such as \`open\`, \`base\`, \`buy\`, and \`add\`, but canonical forms are still preferred in documentation.
36
36
  - **Execution Price**: \`open_position_simple\` no longer auto-fills a fresh Oracle price for \`MARKET\`; provide \`price\` explicitly when you want MCP to compute size / fee previews.
37
+ - **Collateral Auto-Compute**: If you provide \`size + price + leverage\` to \`open_position_simple\`, MCP can infer \`collateralAmount\` automatically. If you provide \`collateralAmount\` too, MCP validates that it is sufficient for the intended notional.
37
38
  - **Funding Delta Ownership**: MCP no longer performs its own increase-order margin/deposit reconciliation. SDK \`createIncreaseOrder\` owns deposit-delta handling for new increase orders.
38
39
  - **Pre-check Diagnostics**: \`check_account_ready\` now reports SDK \`availableMarginBalance\` first. If that read degrades, inspect \`summary.degraded\` and \`diagnostics.availableMarginError\` before trusting fallback account fields.
39
- - **Beta Broker Mode**: Current Arbitrum Sepolia / Linea Sepolia test brokers auto-enable beta mode when \`IS_BETA_MODE\` is omitted.
40
40
  - **Approval Safety**: Local fallback flows prefer exact approval sizing. Do not assume unlimited approvals are necessary.
41
41
  - **Position Semantics**: \`size\` is BASE quantity, not USD notional. If a \`positionId\` is supplied, \`direction\` must match the live position.
42
42
  - **TP/SL Semantics**: LONG should use \`tpPrice > entryPrice\` and \`slPrice < entryPrice\`; SHORT uses the inverse. Plain integer strings like \`"65000"\` are treated as human prices, not implicit raw 30-decimal values.
@@ -49,7 +49,6 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
49
49
  - Arbitrum test: \`0x895C4ae2A22bB26851011d733A9355f663a1F939\`
50
50
  - Linea test: \`0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2\`
51
51
  - Always keep \`RPC_URL\`, \`CHAIN_ID\`, and \`BROKER_ADDRESS\` on the same network.
52
- - If \`IS_BETA_MODE\` is unset, MCP auto-detects beta mode for those two testnet brokers.
53
52
 
54
53
  Current Session:
55
54
  - Wallet: ${address}
package/dist/server.js CHANGED
@@ -461,7 +461,7 @@ function zodSchemaToJsonSchema(zodSchema) {
461
461
  };
462
462
  }
463
463
  // ─── MCP Server ───
464
- const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.31" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
464
+ const server = new Server({ name: "myx-mcp-trading-server", version: "3.1.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
465
465
  // List tools
466
466
  server.setRequestHandler(ListToolsRequestSchema, async () => {
467
467
  return {
@@ -582,7 +582,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
582
582
  async function main() {
583
583
  const transport = new StdioServerTransport();
584
584
  await server.connect(transport);
585
- logger.info("🚀 MYX Trading MCP Server v3.0.24 running (stdio, pure on-chain, prod ready)");
585
+ logger.info("🚀 MYX Trading MCP Server v3.1.0 running (stdio, pure on-chain, prod ready)");
586
586
  }
587
587
  main().catch((err) => {
588
588
  logger.error("Fatal Server Startup Error", err);
@@ -1,7 +1,7 @@
1
- import { getChainId } from "../auth/resolveClient.js";
1
+ import { getChainId, getQuoteToken } from "../auth/resolveClient.js";
2
2
  export async function getBalances(client, address, chainIdOverride) {
3
3
  const chainId = chainIdOverride ?? getChainId();
4
- return client.account.getWalletQuoteTokenBalance(chainId, address);
4
+ return client.account.getWalletQuoteTokenBalance({ chainId, address, tokenAddress: getQuoteToken() });
5
5
  }
6
6
  export async function getMarginBalance(client, address, poolId, chainIdOverride) {
7
7
  const chainId = chainIdOverride ?? getChainId();
@@ -46,6 +46,12 @@ function normalizePoolId(row) {
46
46
  const poolId = row?.poolId ?? row?.pool_id ?? "";
47
47
  return String(poolId);
48
48
  }
49
+ function matchesChainId(row, chainId) {
50
+ const raw = row?.chainId ?? row?.chain_id;
51
+ if (raw === undefined || raw === null || raw === "")
52
+ return true;
53
+ return Number(raw) === chainId;
54
+ }
49
55
  function matchesKeyword(row, keywordUpper) {
50
56
  const poolId = normalizePoolId(row);
51
57
  const haystack = [
@@ -63,11 +69,28 @@ function matchesKeyword(row, keywordUpper) {
63
69
  .join("|");
64
70
  return haystack.includes(keywordUpper);
65
71
  }
72
+ export async function searchMarketCompat(client, params) {
73
+ const normalizedKeyword = String(params.keyword ?? "").trim();
74
+ const attempts = [
75
+ { chainId: params.chainId, keyword: normalizedKeyword, limit: params.limit },
76
+ { chainId: params.chainId, searchKey: normalizedKeyword },
77
+ ];
78
+ let lastError = null;
79
+ for (const attempt of attempts) {
80
+ try {
81
+ return await client.markets.searchMarket(attempt);
82
+ }
83
+ catch (error) {
84
+ lastError = error;
85
+ }
86
+ }
87
+ throw lastError ?? new Error("searchMarket failed");
88
+ }
66
89
  async function fetchApiMarketRows(client, chainId) {
67
90
  // Preferred path: documented markets.searchMarket
68
91
  try {
69
- const searchRes = await client.markets.searchMarket({ chainId, keyword: "", limit: 2000 });
70
- const searchRows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row));
92
+ const searchRes = await searchMarketCompat(client, { chainId, keyword: "", limit: 2000 });
93
+ const searchRows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
71
94
  if (searchRows.length > 0)
72
95
  return searchRows;
73
96
  }
@@ -76,7 +99,7 @@ async function fetchApiMarketRows(client, chainId) {
76
99
  // Secondary path: documented markets.getPoolSymbolAll
77
100
  try {
78
101
  const symbolsRes = await client.markets.getPoolSymbolAll();
79
- const symbolRows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row));
102
+ const symbolRows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
80
103
  if (symbolRows.length > 0)
81
104
  return symbolRows;
82
105
  }
@@ -84,12 +107,12 @@ async function fetchApiMarketRows(client, chainId) {
84
107
  }
85
108
  // Legacy fallback: internal api namespace (for backward compatibility)
86
109
  const marketListRes = await client.api?.getMarketList?.().catch(() => null);
87
- const marketRows = extractMarketRows(marketListRes, chainId);
110
+ const marketRows = extractMarketRows(marketListRes, chainId).filter((row) => matchesChainId(row, chainId));
88
111
  const marketRowsWithPoolId = marketRows.filter((row) => normalizePoolId(row));
89
112
  if (marketRowsWithPoolId.length > 0)
90
113
  return marketRowsWithPoolId;
91
114
  const poolListRes = await client.api?.getPoolList?.().catch(() => null);
92
- return collectRows(poolListRes?.data ?? poolListRes).filter((row) => normalizePoolId(row));
115
+ return collectRows(poolListRes?.data ?? poolListRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
93
116
  }
94
117
  export async function getMarketPrice(client, poolId, chainIdOverride) {
95
118
  const chainId = chainIdOverride ?? getChainId();
@@ -150,7 +173,11 @@ export async function searchMarket(client, keyword, limit = 1000, chainIdOverrid
150
173
  const requestedLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1000;
151
174
  let dataList = [];
152
175
  try {
153
- const searchRes = await client.markets.searchMarket({ chainId, keyword: normalizedKeyword, limit: requestedLimit });
176
+ const searchRes = await searchMarketCompat(client, {
177
+ chainId,
178
+ keyword: normalizedKeyword,
179
+ limit: requestedLimit,
180
+ });
154
181
  dataList = extractMarketRows(searchRes, chainId);
155
182
  }
156
183
  catch {
@@ -229,8 +256,8 @@ export async function getMarketDetail(client, poolId, chainIdOverride) {
229
256
  export async function getPoolList(client, chainIdOverride) {
230
257
  const chainId = chainIdOverride ?? getChainId();
231
258
  try {
232
- const searchRes = await client.markets.searchMarket({ chainId, keyword: "", limit: 2000 });
233
- const rows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row));
259
+ const searchRes = await searchMarketCompat(client, { chainId, keyword: "", limit: 2000 });
260
+ const rows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
234
261
  if (rows.length > 0)
235
262
  return rows;
236
263
  }
@@ -238,13 +265,14 @@ export async function getPoolList(client, chainIdOverride) {
238
265
  }
239
266
  try {
240
267
  const symbolsRes = await client.markets.getPoolSymbolAll();
241
- const rows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row));
268
+ const rows = collectRows(symbolsRes?.data ?? symbolsRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
242
269
  if (rows.length > 0)
243
270
  return rows;
244
271
  }
245
272
  catch {
246
273
  }
247
- return client.api?.getPoolList?.();
274
+ const poolListRes = await client.api?.getPoolList?.();
275
+ return collectRows(poolListRes?.data ?? poolListRes).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
248
276
  }
249
277
  /**
250
278
  * 获取池子分级配置
@@ -13,12 +13,6 @@ const POOL_MANAGER_BY_CHAIN = {
13
13
  97: "0x4F917ef137b573D9790b87e3cF6dfb698cF00c9c",
14
14
  56: "0x13F2130c2F3bfd612BBCBF35FB9E467dd32bAF3A",
15
15
  };
16
- const POOL_MANAGER_BY_CHAIN_BETA = {
17
- 421614: "0x05314a21Fc97B74f168730153b2B63A870D25dE5",
18
- 59141: "0xcf51a6895864c6D8E507fC31EF16b9011287c5f4",
19
- 97: "0x9E84a999e15CCdb2F64a5AF10939c25769dF6b07",
20
- 56: "0x9E84a999e15CCdb2F64a5AF10939c25769dF6b07",
21
- };
22
16
  const POOL_MANAGER_ABI = [
23
17
  "function deployPool((bytes32 marketId,address baseToken))",
24
18
  "function getMarketPool(bytes32 marketId,address asset) view returns ((bytes32 marketId,bytes32 poolId,address baseToken,address quoteToken,uint8 riskTier,uint8 state,address basePoolToken,address quotePoolToken,uint16 maxPriceDeviation,bool compoundEnabled,uint64 windowCapUsd,address poolVault,address tradingVault))",
@@ -45,28 +39,6 @@ const LP_ROUTER_BY_CHAIN = {
45
39
  quotePool: "0xB1E6df749A602892FafB27bb39Fd4F044527121E",
46
40
  },
47
41
  };
48
- const LP_ROUTER_BY_CHAIN_BETA = {
49
- 421614: {
50
- router: "0xfb790ECE13Cd9e296b4a06ABF38D10431360c236",
51
- basePool: "0x1F767BEa83EDe5E0C18904f439C579f52c2c0F1b",
52
- quotePool: "0x50ad7da312c58c6689bEF028954a366cb5b8ee13",
53
- },
54
- 59141: {
55
- router: "0x0AE31989318565620c03d35889a0B2536b8fba5C",
56
- basePool: "0xc9e3826c42183207B418116A16827C1605132c51",
57
- quotePool: "0x9A9934D3b22103dE822d94A22A3177d728FE0e5a",
58
- },
59
- 97: {
60
- router: "0x0F4C6f18Fb136DD1eBd6Da3C5d86a86597CF79a3",
61
- basePool: "0x51B62554a76197d5DF2D5dC4D57FF54d40775938",
62
- quotePool: "0x783Ed065a12e1C1D33c2a8d6408385C1843D3084",
63
- },
64
- 56: {
65
- router: "0x0F4C6f18Fb136DD1eBd6Da3C5d86a86597CF79a3",
66
- basePool: "0x51B62554a76197d5DF2D5dC4D57FF54d40775938",
67
- quotePool: "0x783Ed065a12e1C1D33c2a8d6408385C1843D3084",
68
- },
69
- };
70
42
  const PREVIEW_POOL_ABI = [
71
43
  "function previewLpAmountOut(bytes32,uint256,uint256) view returns (uint256)",
72
44
  "function previewQuoteAmountOut(bytes32,uint256,uint256) view returns (uint256)",
@@ -165,24 +137,19 @@ async function withMutedSdkAbiMismatchLogs(runner) {
165
137
  console.error = original;
166
138
  }
167
139
  }
168
- function isBetaModeEnabled() {
169
- return String(process.env.IS_BETA_MODE ?? "").trim().toLowerCase() === "true";
170
- }
171
140
  function getPoolManagerAddress(chainId) {
172
141
  const envAddress = String(process.env.POOL_MANAGER_ADDRESS ?? "").trim();
173
142
  if (envAddress) {
174
143
  return normalizeAddress(envAddress, "POOL_MANAGER_ADDRESS");
175
144
  }
176
- const source = isBetaModeEnabled() ? POOL_MANAGER_BY_CHAIN_BETA : POOL_MANAGER_BY_CHAIN;
177
- const mapped = source[chainId];
145
+ const mapped = POOL_MANAGER_BY_CHAIN[chainId];
178
146
  if (!mapped) {
179
- throw new Error(`Pool manager address is not configured for chainId=${chainId} (beta=${isBetaModeEnabled()}). Set POOL_MANAGER_ADDRESS env var.`);
147
+ throw new Error(`Pool manager address is not configured for chainId=${chainId}. Set POOL_MANAGER_ADDRESS env var.`);
180
148
  }
181
149
  return mapped;
182
150
  }
183
151
  function getLpAddresses(chainId) {
184
- const source = isBetaModeEnabled() ? LP_ROUTER_BY_CHAIN_BETA : LP_ROUTER_BY_CHAIN;
185
- const matched = source[chainId];
152
+ const matched = LP_ROUTER_BY_CHAIN[chainId];
186
153
  if (!matched) {
187
154
  throw new Error(`Liquidity router config not found for chainId=${chainId}.`);
188
155
  }
@@ -13,10 +13,7 @@ export const getTradeFlowTool = {
13
13
  const { client, address } = await resolveClient();
14
14
  const chainId = getChainId();
15
15
  const query = { chainId, poolId: args.poolId, limit: args.limit ?? 20 };
16
- const accessToken = typeof client?.getAccessToken === "function"
17
- ? (await client.getAccessToken()) || ""
18
- : "";
19
- const result = await client.api.getTradeFlow({ ...query, address, accessToken });
16
+ const result = await client.account.getTradeFlow(query, address);
20
17
  const enhancedData = (result?.data || []).map((flow) => ({
21
18
  ...flow,
22
19
  typeDesc: getTradeFlowTypeDesc(flow.type)
@@ -38,6 +38,12 @@ function formatUnixTimestamp(timestamp) {
38
38
  return String(timestamp);
39
39
  return `${timestamp.toString()} (${new Date(numeric * 1000).toISOString()})`;
40
40
  }
41
+ async function withdrawCompat(client, params) {
42
+ if (typeof client?.account?.withdraw === "function") {
43
+ return client.account.withdraw(params);
44
+ }
45
+ return client.account.updateAndWithdraw(params.receiver, params.poolId, params.isQuoteToken, params.amount, params.chainId);
46
+ }
41
47
  export const accountDepositTool = {
42
48
  name: "account_deposit",
43
49
  description: "[ACCOUNT] Deposit funds from wallet into the MYX trading account.",
@@ -136,7 +142,13 @@ export const accountWithdrawTool = {
136
142
  throw new Error(`Account has locked funds until releaseTime=${formatUnixTimestamp(releaseTime)}. ` +
137
143
  `Retry after unlock or reduce withdraw amount.`);
138
144
  }
139
- const raw = await client.account.updateAndWithdraw(address, args.poolId, Boolean(args.isQuoteToken), amount, chainId);
145
+ const raw = await withdrawCompat(client, {
146
+ receiver: address,
147
+ poolId: args.poolId,
148
+ isQuoteToken: Boolean(args.isQuoteToken),
149
+ amount,
150
+ chainId,
151
+ });
140
152
  const data = await finalizeMutationResult(raw, signer, "account_withdraw");
141
153
  const preflight = {
142
154
  requestedAmountRaw: amountRaw.toString(),
@@ -52,7 +52,11 @@ export const cancelOrdersTool = {
52
52
  if (targetOrderIds.length === 0) {
53
53
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", message: "No matching open orders found to cancel." }) }] };
54
54
  }
55
- const raw = await client.order.cancelAllOrders(targetOrderIds, chainId);
55
+ const raw = args.cancelAll
56
+ ? await client.order.cancelAllOrders(targetOrderIds, chainId)
57
+ : targetOrderIds.length === 1
58
+ ? await client.order.cancelOrder(targetOrderIds[0], chainId)
59
+ : await client.order.cancelOrders(targetOrderIds, chainId);
56
60
  const result = await finalizeMutationResult(raw, signer, "cancel_orders");
57
61
  return {
58
62
  content: [{
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { resolveClient, getChainId } from "../auth/resolveClient.js";
2
+ import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
3
3
  import { resolvePool } from "../services/marketService.js";
4
4
  import { parseUserUnits } from "../utils/units.js";
5
5
  import { ethers } from "ethers";
@@ -49,7 +49,11 @@ export const checkAccountReadyTool = {
49
49
  lockedMarginRaw = toBigIntOrZero(marginInfo.data.lockedMargin);
50
50
  accountWalletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
51
51
  }
52
- const walletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
52
+ const walletRes = await client.account.getWalletQuoteTokenBalance({
53
+ chainId,
54
+ address,
55
+ tokenAddress: getQuoteToken(),
56
+ });
53
57
  const walletBalanceRaw = BigInt(walletRes?.data || "0");
54
58
  const requiredRaw = BigInt(parseUserUnits(args.collateralAmount, quoteDecimals, "required"));
55
59
  const isReady = (availableMarginBalanceRaw >= requiredRaw) || (availableMarginBalanceRaw + walletBalanceRaw >= requiredRaw);
@@ -6,6 +6,7 @@ import { SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
6
6
  import { verifyTradeOutcome } from "../utils/verification.js";
7
7
  import { mapDirection, mapOrderType, mapTriggerType } from "../utils/mappings.js";
8
8
  import { parseUserUnits } from "../utils/units.js";
9
+ import { isZeroAddress, normalizeAddress } from "../utils/address.js";
9
10
  const FULL_CLOSE_MARKERS = new Set(["ALL", "FULL", "MAX"]);
10
11
  const INTEGER_RE = /^\d+$/;
11
12
  function wantsFullCloseMarker(input) {
@@ -29,6 +30,20 @@ function resolvePositionRaw(position, rawFields, humanFields, decimals, label) {
29
30
  return "";
30
31
  return parseUserUnits(human, decimals, label);
31
32
  }
33
+ function resolveQuoteExecutionFeeToken(input, quoteToken) {
34
+ const quoteTokenNormalized = normalizeAddress(quoteToken, "quoteToken");
35
+ const raw = String(input ?? "").trim();
36
+ if (!raw)
37
+ return quoteTokenNormalized;
38
+ if (isZeroAddress(raw)) {
39
+ throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteTokenNormalized}.`);
40
+ }
41
+ const normalized = normalizeAddress(raw, "executionFeeToken");
42
+ if (normalized.toLowerCase() !== quoteTokenNormalized.toLowerCase()) {
43
+ throw new Error(`executionFeeToken must equal the pool quoteToken ${quoteTokenNormalized}. Native token and non-quote tokens are not supported in MCP trade flows.`);
44
+ }
45
+ return quoteTokenNormalized;
46
+ }
32
47
  export const closePositionTool = {
33
48
  name: "close_position",
34
49
  description: "[TRADE] Create a decrease order (close or reduce position) using SDK-native parameters.",
@@ -39,13 +54,13 @@ export const closePositionTool = {
39
54
  triggerType: z.union([z.number(), z.string()]).optional().describe("Trigger type: 0/NONE, 1/GTE, 2/LTE"),
40
55
  direction: z.union([z.number(), z.string()]).describe("Position direction: 0/LONG or 1/SHORT"),
41
56
  collateralAmount: z.union([z.string(), z.number()]).describe("Collateral amount (human/raw). Also supports ALL/FULL/MAX to use live position collateral raw."),
42
- size: z.union([z.string(), z.number()]).describe("Position size (human/raw). Also supports ALL/FULL/MAX for exact full-close raw size."),
57
+ size: z.union([z.string(), z.number()]).describe("Position size as base asset quantity (human/raw), NOT USD notional. Also supports ALL/FULL/MAX for exact full-close raw size."),
43
58
  price: z.union([z.string(), z.number()]).describe("Price (human or 30-dec raw units)"),
44
- timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.4-beta.4 supports IOC only. Use 0 or 'IOC'."),
45
59
  postOnly: z.coerce.boolean().describe("Post-only flag"),
46
60
  slippagePct: z.coerce.string().default("50").describe(SLIPPAGE_PCT_4DP_DESC),
47
- executionFeeToken: z.string().describe("Execution fee token address"),
61
+ executionFeeToken: z.string().optional().describe("Optional. Must equal the pool quoteToken address. Defaults to the pool quoteToken."),
48
62
  leverage: z.coerce.number().describe("Leverage"),
63
+ verify: z.coerce.boolean().optional().describe("If true, wait for backend order-index verification after chain confirmation. Default false for faster responses."),
49
64
  },
50
65
  handler: async (args) => {
51
66
  try {
@@ -93,7 +108,8 @@ export const closePositionTool = {
93
108
  direction: mapDirection(preparedArgs.direction),
94
109
  orderType: mapOrderType(preparedArgs.orderType),
95
110
  triggerType: preparedArgs.triggerType !== undefined ? mapTriggerType(preparedArgs.triggerType) : undefined,
96
- executionFeeToken: poolData.quoteToken || preparedArgs.executionFeeToken
111
+ executionFeeToken: resolveQuoteExecutionFeeToken(preparedArgs.executionFeeToken, String(poolData.quoteToken ?? "")),
112
+ timeInForce: 0,
97
113
  };
98
114
  const positionsRes = await client.position.listPositions(address);
99
115
  const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
@@ -113,10 +129,11 @@ export const closePositionTool = {
113
129
  const data = await finalizeMutationResult(raw, signer, "close_position");
114
130
  const txHash = data.confirmation?.txHash;
115
131
  let verification = null;
116
- if (txHash) {
132
+ const shouldVerify = Boolean(preparedArgs.verify ?? false);
133
+ if (txHash && shouldVerify) {
117
134
  verification = await verifyTradeOutcome(client, address, preparedArgs.poolId, txHash);
118
135
  }
119
- const payload = { ...data, verification };
136
+ const payload = { ...data, verification, verificationSkipped: !!txHash && !shouldVerify };
120
137
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
121
138
  }
122
139
  catch (error) {
@@ -7,7 +7,7 @@ import { verifyTradeOutcome } from "../utils/verification.js";
7
7
  import { mapDirection, mapOrderType, mapTriggerType } from "../utils/mappings.js";
8
8
  import { extractErrorMessage } from "../utils/errorMessage.js";
9
9
  import { parseUserUnits } from "../utils/units.js";
10
- import { isZeroAddress } from "../utils/address.js";
10
+ import { isZeroAddress, normalizeAddress } from "../utils/address.js";
11
11
  const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
12
12
  const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
13
13
  function pow10(decimals) {
@@ -18,6 +18,20 @@ function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimal
18
18
  const denominator = pow10(baseDecimals + 30);
19
19
  return numerator / denominator;
20
20
  }
21
+ function resolveQuoteExecutionFeeToken(input, quoteToken) {
22
+ const quoteTokenNormalized = normalizeAddress(quoteToken, "quoteToken");
23
+ const raw = String(input ?? "").trim();
24
+ if (!raw)
25
+ return quoteTokenNormalized;
26
+ if (isZeroAddress(raw)) {
27
+ throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteTokenNormalized}.`);
28
+ }
29
+ const normalized = normalizeAddress(raw, "executionFeeToken");
30
+ if (normalized.toLowerCase() !== quoteTokenNormalized.toLowerCase()) {
31
+ throw new Error(`executionFeeToken must equal the pool quoteToken ${quoteTokenNormalized}. Native token and non-quote tokens are not supported in MCP trade flows.`);
32
+ }
33
+ return quoteTokenNormalized;
34
+ }
21
35
  export const executeTradeTool = {
22
36
  name: "execute_trade",
23
37
  description: "[TRADE] Create an increase order (open or add to position) using SDK-native parameters.",
@@ -30,12 +44,11 @@ export const executeTradeTool = {
30
44
  triggerType: z.union([z.number(), z.string()]).optional().describe("0=None (Market), 1=GTE, 2=LTE. e.g. 'GTE'."),
31
45
  direction: z.union([z.number(), z.string()]).describe("0/LONG/BUY or 1/SHORT/SELL."),
32
46
  collateralAmount: z.union([z.string(), z.number()]).describe("Collateral. e.g. '100' or 'raw:100000000' (6 decimals for USDC)."),
33
- size: z.union([z.string(), z.number()]).describe("Notional size in base tokens. e.g. '0.5' BTC or 'raw:50000000'."),
47
+ size: z.union([z.string(), z.number()]).describe("Base asset quantity, NOT USD notional. e.g. '0.5' BTC or 'raw:50000000'."),
34
48
  price: z.union([z.string(), z.number()]).describe("Execution or Limit price. e.g. '65000' or 'raw:...'"),
35
- timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.4-beta.4 supports IOC only. Use 0 or 'IOC'."),
36
49
  postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
37
50
  slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
38
- executionFeeToken: z.string().optional().describe("Address of token to pay gas/execution fees (typically USDC). Default is pool quoteToken."),
51
+ executionFeeToken: z.string().optional().describe("Optional. Must equal the pool quoteToken address. Defaults to the pool quoteToken."),
39
52
  leverage: z.coerce.number().positive().describe("Leverage multiplier, e.g., 10 for 10x."),
40
53
  tpSize: z.union([z.string(), z.number()]).optional().describe("Take Profit size. Use '0' to disable."),
41
54
  tpPrice: z.union([z.string(), z.number()]).optional().describe("Take Profit trigger price."),
@@ -45,6 +58,7 @@ export const executeTradeTool = {
45
58
  assetClass: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup assetClass (default from pool config or 1)."),
46
59
  riskTier: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup riskTier (default from pool config or 1)."),
47
60
  marketId: z.string().describe("Specific Market Config Hash. Fetch via get_pool_metadata (resolve poolId first with find_pool/list_pools)."),
61
+ verify: z.coerce.boolean().optional().describe("If true, wait for backend order-index verification after chain confirmation. Default false for faster responses."),
48
62
  },
49
63
  handler: async (args) => {
50
64
  try {
@@ -70,11 +84,7 @@ export const executeTradeTool = {
70
84
  const mappedOrderType = mapOrderType(args.orderType);
71
85
  const mappedTriggerType = args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined;
72
86
  const slippagePctNormalized = normalizeSlippagePct4dp(args.slippagePct);
73
- if (args.executionFeeToken !== undefined && args.executionFeeToken !== null && String(args.executionFeeToken).trim() !== "") {
74
- if (isZeroAddress(args.executionFeeToken)) {
75
- throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${poolData.quoteToken}.`);
76
- }
77
- }
87
+ const executionFeeToken = resolveQuoteExecutionFeeToken(args.executionFeeToken, String(poolData.quoteToken ?? ""));
78
88
  const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
79
89
  const sizeRaw = parseUserUnits(args.size, baseDecimals, "size");
80
90
  const priceRaw = parseUserUnits(args.price, 30, "price");
@@ -142,13 +152,13 @@ export const executeTradeTool = {
142
152
  triggerType: mappedTriggerType,
143
153
  // Normalize positionId
144
154
  positionId: normalizedPositionId,
145
- // Enforce executionFeeToken as quoteToken
146
- executionFeeToken: poolData.quoteToken || args.executionFeeToken,
155
+ executionFeeToken,
147
156
  collateralAmount: `raw:${collateralRaw}`,
148
157
  size: `raw:${sizeRaw}`,
149
158
  price: `raw:${priceRaw}`,
150
159
  tradingFee: `raw:${tradingFeeRaw}`,
151
160
  slippagePct: slippagePctNormalized,
161
+ timeInForce: 0,
152
162
  };
153
163
  if (args.tpSize !== undefined) {
154
164
  mappedArgs.tpSize = `raw:${parseUserUnits(args.tpSize, baseDecimals, "tpSize")}`;
@@ -166,17 +176,23 @@ export const executeTradeTool = {
166
176
  const data = await finalizeMutationResult(raw, signer, "execute_trade");
167
177
  const txHash = data.confirmation?.txHash;
168
178
  let verification = null;
169
- if (txHash) {
179
+ const shouldVerify = Boolean(args.verify ?? false);
180
+ if (txHash && shouldVerify) {
170
181
  verification = await verifyTradeOutcome(client, address, args.poolId, txHash);
171
182
  }
172
183
  const payload = {
173
184
  ...data,
174
185
  verification,
186
+ verificationSkipped: !!txHash && !shouldVerify,
175
187
  preflight: {
176
188
  normalized: {
177
189
  collateralAmountRaw: collateralRaw,
178
190
  sizeRaw,
179
191
  priceRaw30: priceRaw,
192
+ sizeSemantics: "base_quantity",
193
+ impliedNotionalQuoteRaw: computeQuoteNotionalRaw(BigInt(sizeRaw), BigInt(priceRaw), baseDecimals, quoteDecimals).toString(),
194
+ executionFeeToken,
195
+ timeInForce: mappedArgs.timeInForce,
180
196
  tradingFeeRaw,
181
197
  tpSizeRaw: mappedArgs.tpSize?.replace(/^raw:/i, "") ?? null,
182
198
  tpPriceRaw30: mappedArgs.tpPrice?.replace(/^raw:/i, "") ?? null,
@@ -15,7 +15,7 @@ export const getAllTickersTool = {
15
15
  // Fallback for networks/environments where getAllTickers endpoint is unavailable.
16
16
  const chainId = getChainId();
17
17
  const poolList = await getPoolList(client, chainId);
18
- const pools = Array.isArray(poolList?.data) ? poolList.data : (Array.isArray(poolList) ? poolList : []);
18
+ const pools = Array.isArray(poolList) ? poolList : (Array.isArray(poolList?.data) ? poolList.data : []);
19
19
  const poolIds = pools.map((p) => p?.poolId ?? p?.pool_id).filter((id) => !!id);
20
20
  if (poolIds.length === 0) {
21
21
  throw new Error("Failed to fetch all tickers and no pools were available for fallback query.");
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ import { searchMarketCompat } from "../services/marketService.js";
3
4
  import { normalizeAddress } from "../utils/address.js";
4
5
  import { extractErrorMessage } from "../utils/errorMessage.js";
5
6
  function collectRows(input) {
@@ -14,10 +15,10 @@ function collectRows(input) {
14
15
  function isNonEmptyObject(value) {
15
16
  return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
16
17
  }
17
- async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
18
+ async function loadMarketRows(client, chainId) {
18
19
  let rows = [];
19
20
  try {
20
- const searchRes = await client.markets.searchMarket({ chainId, keyword: "", limit: 2000 });
21
+ const searchRes = await searchMarketCompat(client, { chainId, keyword: "", limit: 2000 });
21
22
  rows = collectRows(searchRes?.data ?? searchRes);
22
23
  }
23
24
  catch {
@@ -32,8 +33,15 @@ async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
32
33
  rows = [];
33
34
  }
34
35
  }
35
- const normalized = baseAddress.toLowerCase();
36
- const matched = rows.find((row) => String(row?.baseToken ?? row?.baseAddress ?? "").trim().toLowerCase() === normalized);
36
+ return rows;
37
+ }
38
+ function findMarketRowByBaseAddress(rows, baseAddress) {
39
+ const normalizedBaseAddress = baseAddress.toLowerCase();
40
+ return rows.find((row) => String(row?.baseToken ?? row?.baseAddress ?? "").trim().toLowerCase() === normalizedBaseAddress);
41
+ }
42
+ async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
43
+ const rows = await loadMarketRows(client, chainId);
44
+ const matched = findMarketRowByBaseAddress(rows, baseAddress);
37
45
  if (!matched)
38
46
  return null;
39
47
  const poolId = String(matched?.poolId ?? matched?.pool_id ?? "").trim();
@@ -83,6 +91,12 @@ async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
83
91
  source: "market_search_fallback",
84
92
  };
85
93
  }
94
+ async function resolvePoolIdByBaseAddress(client, chainId, baseAddress) {
95
+ const rows = await loadMarketRows(client, chainId);
96
+ const matched = findMarketRowByBaseAddress(rows, baseAddress);
97
+ const poolId = String(matched?.poolId ?? matched?.pool_id ?? "").trim();
98
+ return poolId || null;
99
+ }
86
100
  function buildReadErrorPayload(args, messageLike, code = "SDK_READ_ERROR") {
87
101
  const chainId = args.chainId ?? getChainId();
88
102
  const message = extractErrorMessage(messageLike, "Failed to read base token detail.");
@@ -113,7 +127,16 @@ export const getBaseDetailTool = {
113
127
  const { client } = await resolveClient();
114
128
  const chainId = args.chainId ?? getChainId();
115
129
  const baseAddress = normalizeAddress(args.baseAddress, "baseAddress");
116
- const result = await client.markets.getBaseDetail({ chainId, baseAddress });
130
+ const poolId = await resolvePoolIdByBaseAddress(client, chainId, baseAddress);
131
+ if (!poolId) {
132
+ const fallback = await findBaseDetailFromMarkets(client, chainId, baseAddress);
133
+ if (fallback) {
134
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: fallback }, null, 2) }] };
135
+ }
136
+ const body = buildReadErrorPayload({ ...args, baseAddress }, "Could not resolve poolId from baseAddress.", "NOT_FOUND");
137
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }], isError: true };
138
+ }
139
+ const result = await client.markets.getBaseDetail({ chainId, poolId });
117
140
  const hasCode = !!result && typeof result === "object" && !Array.isArray(result) && Object.prototype.hasOwnProperty.call(result, "code");
118
141
  const code = hasCode ? Number(result.code) : 0;
119
142
  const payload = hasCode ? result.data : result;
@@ -3,9 +3,10 @@ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { COMMON_LP_AMOUNT_DECIMALS } from "@myx-trade/sdk";
4
4
  import { Contract, formatUnits } from "ethers";
5
5
  import { extractErrorMessage } from "../utils/errorMessage.js";
6
- import { getPoolList } from "../services/marketService.js";
7
- import { getPoolInfo } from "../services/poolService.js";
6
+ import { getFreshOraclePrice, getPoolList } from "../services/marketService.js";
7
+ import { getLpPrice, getPoolInfo } from "../services/poolService.js";
8
8
  const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
9
+ const INTEGER_RE = /^\d+$/;
9
10
  const ERC20_BALANCE_ABI = ["function balanceOf(address owner) view returns (uint256)"];
10
11
  function collectRows(input) {
11
12
  if (Array.isArray(input))
@@ -127,6 +128,71 @@ async function readErc20Balance(provider, tokenAddress, holder) {
127
128
  const balance = await contract.balanceOf(holder);
128
129
  return BigInt(balance).toString();
129
130
  }
131
+ function pow10(decimals) {
132
+ if (!Number.isInteger(decimals) || decimals < 0) {
133
+ throw new Error(`Invalid decimals: ${decimals}`);
134
+ }
135
+ return 10n ** BigInt(decimals);
136
+ }
137
+ function parseIntegerRaw(value) {
138
+ const text = String(value ?? "").trim();
139
+ if (!INTEGER_RE.test(text))
140
+ return null;
141
+ try {
142
+ return BigInt(text);
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ function formatRawValue(value, decimals) {
149
+ if (!value || !INTEGER_RE.test(value))
150
+ return null;
151
+ try {
152
+ return formatUnits(value, decimals);
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ function computeLpValueQuoteRaw(lpBalanceRaw, lpPriceQuoteRaw30, quoteDecimals) {
159
+ const balance = parseIntegerRaw(lpBalanceRaw);
160
+ const price = parseIntegerRaw(lpPriceQuoteRaw30);
161
+ if (balance === null || price === null)
162
+ return null;
163
+ const valueRaw = (balance * price * pow10(quoteDecimals)) /
164
+ pow10(COMMON_LP_AMOUNT_DECIMALS + 30);
165
+ return valueRaw.toString();
166
+ }
167
+ function computeUnderlyingTokenRawFromExchangeRate(lpBalanceRaw, exchangeRateRaw18, tokenDecimals) {
168
+ const balance = parseIntegerRaw(lpBalanceRaw);
169
+ const exchangeRate = parseIntegerRaw(exchangeRateRaw18);
170
+ if (balance === null || exchangeRate === null)
171
+ return null;
172
+ const underlyingRaw = (balance * exchangeRate * pow10(tokenDecimals)) /
173
+ pow10(COMMON_LP_AMOUNT_DECIMALS + 18);
174
+ return underlyingRaw.toString();
175
+ }
176
+ function computeQuoteValueFromBaseAmountRaw(baseAmountRaw, basePriceRaw30, baseDecimals, quoteDecimals) {
177
+ const baseAmount = parseIntegerRaw(baseAmountRaw);
178
+ const basePrice = parseIntegerRaw(basePriceRaw30);
179
+ if (baseAmount === null || basePrice === null)
180
+ return null;
181
+ const quoteValueRaw = (baseAmount * basePrice * pow10(quoteDecimals)) /
182
+ pow10(baseDecimals + 30);
183
+ return quoteValueRaw.toString();
184
+ }
185
+ function sumRawValues(...values) {
186
+ let total = 0n;
187
+ let found = false;
188
+ for (const value of values) {
189
+ if (!value || !INTEGER_RE.test(value))
190
+ continue;
191
+ total += BigInt(value);
192
+ found = true;
193
+ }
194
+ return found ? total.toString() : null;
195
+ }
130
196
  function collectAddressCandidates(...values) {
131
197
  const unique = new Set();
132
198
  for (const value of values) {
@@ -205,6 +271,9 @@ export const getMyLpHoldingsTool = {
205
271
  const warnings = [];
206
272
  let totalBaseLpRaw = 0n;
207
273
  let totalQuoteLpRaw = 0n;
274
+ let totalBaseEstimatedValueQuoteRaw = 0n;
275
+ let totalQuoteEstimatedValueQuoteRaw = 0n;
276
+ const valuationBuckets = new Map();
208
277
  for (const row of scannedRows) {
209
278
  const poolId = normalizePoolId(row?.poolId ?? row?.pool_id);
210
279
  let basePoolToken = readAddress(row?.basePoolToken ?? row?.base_pool_token);
@@ -259,6 +328,140 @@ export const getMyLpHoldingsTool = {
259
328
  continue;
260
329
  totalBaseLpRaw += BigInt(baseLpRaw);
261
330
  totalQuoteLpRaw += BigInt(quoteLpRaw);
331
+ const baseDecimals = Number(detail?.baseDecimals ?? row?.baseDecimals ?? row?.base_decimals ?? 18);
332
+ const quoteDecimals = Number(detail?.quoteDecimals ?? row?.quoteDecimals ?? row?.quote_decimals ?? 6);
333
+ async function ensurePoolInfoLoaded() {
334
+ if (poolInfo)
335
+ return poolInfo;
336
+ poolInfo = await getPoolInfo(poolId, chainId, client);
337
+ return poolInfo;
338
+ }
339
+ let baseLpPriceQuoteRaw = null;
340
+ let quoteLpPriceQuoteRaw = null;
341
+ let baseEstimatedUnderlyingRaw = null;
342
+ let quoteEstimatedUnderlyingRaw = null;
343
+ let baseEstimatedValueQuoteRaw = null;
344
+ let quoteEstimatedValueQuoteRaw = null;
345
+ let baseValueSource = null;
346
+ let quoteValueSource = null;
347
+ let baseOraclePriceRaw30 = null;
348
+ const baseValuationNotes = [];
349
+ const quoteValuationNotes = [];
350
+ if (BigInt(baseLpRaw) > 0n) {
351
+ try {
352
+ const raw = String(await getLpPrice("BASE", poolId, chainId) ?? "").trim();
353
+ if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
354
+ baseLpPriceQuoteRaw = raw;
355
+ baseEstimatedValueQuoteRaw = computeLpValueQuoteRaw(baseLpRaw, raw, quoteDecimals);
356
+ baseValueSource = "sdk.getLpPrice(BASE)";
357
+ }
358
+ }
359
+ catch (error) {
360
+ baseValuationNotes.push(`failed to fetch BASE LP price (${extractErrorMessage(error)})`);
361
+ }
362
+ if (!baseEstimatedValueQuoteRaw) {
363
+ try {
364
+ const info = await ensurePoolInfoLoaded();
365
+ const raw = String(info?.basePool?.poolTokenPrice ?? "").trim();
366
+ if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
367
+ baseLpPriceQuoteRaw = raw;
368
+ baseEstimatedValueQuoteRaw = computeLpValueQuoteRaw(baseLpRaw, raw, quoteDecimals);
369
+ baseValueSource = "poolInfo.basePool.poolTokenPrice";
370
+ }
371
+ }
372
+ catch (error) {
373
+ baseValuationNotes.push(`failed to load pool info for BASE valuation (${extractErrorMessage(error)})`);
374
+ }
375
+ }
376
+ if (!baseEstimatedValueQuoteRaw) {
377
+ try {
378
+ const info = await ensurePoolInfoLoaded();
379
+ const exchangeRateRaw = String(info?.basePool?.exchangeRate ?? "").trim();
380
+ baseEstimatedUnderlyingRaw = computeUnderlyingTokenRawFromExchangeRate(baseLpRaw, exchangeRateRaw, baseDecimals);
381
+ if (baseEstimatedUnderlyingRaw && BigInt(baseEstimatedUnderlyingRaw) > 0n) {
382
+ const oracle = await getFreshOraclePrice(client, poolId, chainId);
383
+ baseOraclePriceRaw30 = String(oracle?.price ?? "").trim();
384
+ baseEstimatedValueQuoteRaw = computeQuoteValueFromBaseAmountRaw(baseEstimatedUnderlyingRaw, baseOraclePriceRaw30, baseDecimals, quoteDecimals);
385
+ if (baseEstimatedValueQuoteRaw) {
386
+ baseValueSource = "poolInfo.basePool.exchangeRate * oraclePrice";
387
+ }
388
+ }
389
+ }
390
+ catch (error) {
391
+ baseValuationNotes.push(`failed to estimate BASE LP value from exchangeRate/oracle (${extractErrorMessage(error)})`);
392
+ }
393
+ }
394
+ if (!baseEstimatedValueQuoteRaw) {
395
+ warnings.push(`pool ${poolId}: BASE LP value unavailable (${baseValuationNotes.join("; ") || "no valuation source"})`);
396
+ }
397
+ }
398
+ if (BigInt(quoteLpRaw) > 0n) {
399
+ try {
400
+ const raw = String(await getLpPrice("QUOTE", poolId, chainId) ?? "").trim();
401
+ if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
402
+ quoteLpPriceQuoteRaw = raw;
403
+ quoteEstimatedValueQuoteRaw = computeLpValueQuoteRaw(quoteLpRaw, raw, quoteDecimals);
404
+ quoteValueSource = "sdk.getLpPrice(QUOTE)";
405
+ }
406
+ }
407
+ catch (error) {
408
+ quoteValuationNotes.push(`failed to fetch QUOTE LP price (${extractErrorMessage(error)})`);
409
+ }
410
+ if (!quoteEstimatedValueQuoteRaw) {
411
+ try {
412
+ const info = await ensurePoolInfoLoaded();
413
+ const raw = String(info?.quotePool?.poolTokenPrice ?? "").trim();
414
+ if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
415
+ quoteLpPriceQuoteRaw = raw;
416
+ quoteEstimatedValueQuoteRaw = computeLpValueQuoteRaw(quoteLpRaw, raw, quoteDecimals);
417
+ quoteValueSource = "poolInfo.quotePool.poolTokenPrice";
418
+ }
419
+ }
420
+ catch (error) {
421
+ quoteValuationNotes.push(`failed to load pool info for QUOTE valuation (${extractErrorMessage(error)})`);
422
+ }
423
+ }
424
+ if (!quoteEstimatedValueQuoteRaw) {
425
+ try {
426
+ const info = await ensurePoolInfoLoaded();
427
+ const exchangeRateRaw = String(info?.quotePool?.exchangeRate ?? "").trim();
428
+ quoteEstimatedUnderlyingRaw = computeUnderlyingTokenRawFromExchangeRate(quoteLpRaw, exchangeRateRaw, quoteDecimals);
429
+ if (quoteEstimatedUnderlyingRaw && BigInt(quoteEstimatedUnderlyingRaw) > 0n) {
430
+ quoteEstimatedValueQuoteRaw = quoteEstimatedUnderlyingRaw;
431
+ quoteValueSource = "poolInfo.quotePool.exchangeRate";
432
+ }
433
+ }
434
+ catch (error) {
435
+ quoteValuationNotes.push(`failed to estimate QUOTE LP value from exchangeRate (${extractErrorMessage(error)})`);
436
+ }
437
+ }
438
+ if (!quoteEstimatedValueQuoteRaw) {
439
+ warnings.push(`pool ${poolId}: QUOTE LP value unavailable (${quoteValuationNotes.join("; ") || "no valuation source"})`);
440
+ }
441
+ }
442
+ const estimatedValueQuoteRaw = sumRawValues(baseEstimatedValueQuoteRaw, quoteEstimatedValueQuoteRaw);
443
+ if (baseEstimatedValueQuoteRaw) {
444
+ totalBaseEstimatedValueQuoteRaw += BigInt(baseEstimatedValueQuoteRaw);
445
+ }
446
+ if (quoteEstimatedValueQuoteRaw) {
447
+ totalQuoteEstimatedValueQuoteRaw += BigInt(quoteEstimatedValueQuoteRaw);
448
+ }
449
+ if (baseEstimatedValueQuoteRaw || quoteEstimatedValueQuoteRaw) {
450
+ const bucketKey = `${quoteSymbol ?? "QUOTE"}:${quoteDecimals}`;
451
+ const bucket = valuationBuckets.get(bucketKey) ?? {
452
+ quoteSymbol,
453
+ quoteDecimals,
454
+ totalBaseEstimatedValueQuoteRaw: 0n,
455
+ totalQuoteEstimatedValueQuoteRaw: 0n,
456
+ };
457
+ if (baseEstimatedValueQuoteRaw) {
458
+ bucket.totalBaseEstimatedValueQuoteRaw += BigInt(baseEstimatedValueQuoteRaw);
459
+ }
460
+ if (quoteEstimatedValueQuoteRaw) {
461
+ bucket.totalQuoteEstimatedValueQuoteRaw += BigInt(quoteEstimatedValueQuoteRaw);
462
+ }
463
+ valuationBuckets.set(bucketKey, bucket);
464
+ }
262
465
  items.push({
263
466
  poolId,
264
467
  symbol: toSymbol(row),
@@ -271,8 +474,26 @@ export const getMyLpHoldingsTool = {
271
474
  quotePoolToken: quotePoolToken ?? null,
272
475
  baseLpBalanceRaw: baseLpRaw,
273
476
  baseLpBalance: formatUnits(baseLpRaw, COMMON_LP_AMOUNT_DECIMALS),
477
+ baseLpPriceQuoteRaw,
478
+ baseLpPriceQuote: formatRawValue(baseLpPriceQuoteRaw, 30),
479
+ baseEstimatedUnderlyingRaw,
480
+ baseEstimatedUnderlying: formatRawValue(baseEstimatedUnderlyingRaw, baseDecimals),
481
+ baseOraclePriceRaw30,
482
+ baseOraclePrice: formatRawValue(baseOraclePriceRaw30, 30),
483
+ baseEstimatedValueQuoteRaw,
484
+ baseEstimatedValueQuote: formatRawValue(baseEstimatedValueQuoteRaw, quoteDecimals),
485
+ baseValueSource,
274
486
  quoteLpBalanceRaw: quoteLpRaw,
275
487
  quoteLpBalance: formatUnits(quoteLpRaw, COMMON_LP_AMOUNT_DECIMALS),
488
+ quoteLpPriceQuoteRaw,
489
+ quoteLpPriceQuote: formatRawValue(quoteLpPriceQuoteRaw, 30),
490
+ quoteEstimatedUnderlyingRaw,
491
+ quoteEstimatedUnderlying: formatRawValue(quoteEstimatedUnderlyingRaw, quoteDecimals),
492
+ quoteEstimatedValueQuoteRaw,
493
+ quoteEstimatedValueQuote: formatRawValue(quoteEstimatedValueQuoteRaw, quoteDecimals),
494
+ quoteValueSource,
495
+ estimatedValueQuoteRaw,
496
+ estimatedValueQuote: formatRawValue(estimatedValueQuoteRaw, quoteDecimals),
276
497
  hasAnyLp,
277
498
  });
278
499
  }
@@ -282,6 +503,20 @@ export const getMyLpHoldingsTool = {
282
503
  return symbolCompare;
283
504
  return String(left.poolId ?? "").localeCompare(String(right.poolId ?? ""));
284
505
  });
506
+ const valuationSummaryByQuote = Array.from(valuationBuckets.values()).map((bucket) => {
507
+ const totalEstimatedValueQuoteRaw = bucket.totalBaseEstimatedValueQuoteRaw + bucket.totalQuoteEstimatedValueQuoteRaw;
508
+ return {
509
+ quoteSymbol: bucket.quoteSymbol,
510
+ quoteDecimals: bucket.quoteDecimals,
511
+ totalBaseEstimatedValueQuoteRaw: bucket.totalBaseEstimatedValueQuoteRaw.toString(),
512
+ totalBaseEstimatedValueQuote: formatUnits(bucket.totalBaseEstimatedValueQuoteRaw, bucket.quoteDecimals),
513
+ totalQuoteEstimatedValueQuoteRaw: bucket.totalQuoteEstimatedValueQuoteRaw.toString(),
514
+ totalQuoteEstimatedValueQuote: formatUnits(bucket.totalQuoteEstimatedValueQuoteRaw, bucket.quoteDecimals),
515
+ totalEstimatedValueQuoteRaw: totalEstimatedValueQuoteRaw.toString(),
516
+ totalEstimatedValueQuote: formatUnits(totalEstimatedValueQuoteRaw, bucket.quoteDecimals),
517
+ };
518
+ });
519
+ const singleQuoteSummary = valuationSummaryByQuote.length === 1 ? valuationSummaryByQuote[0] : null;
285
520
  const payload = {
286
521
  meta: {
287
522
  address,
@@ -298,6 +533,13 @@ export const getMyLpHoldingsTool = {
298
533
  totalBaseLp: formatUnits(totalBaseLpRaw, COMMON_LP_AMOUNT_DECIMALS),
299
534
  totalQuoteLpRaw: totalQuoteLpRaw.toString(),
300
535
  totalQuoteLp: formatUnits(totalQuoteLpRaw, COMMON_LP_AMOUNT_DECIMALS),
536
+ totalBaseEstimatedValueQuoteRaw: totalBaseEstimatedValueQuoteRaw.toString(),
537
+ totalBaseEstimatedValueQuote: singleQuoteSummary?.totalBaseEstimatedValueQuote ?? null,
538
+ totalQuoteEstimatedValueQuoteRaw: totalQuoteEstimatedValueQuoteRaw.toString(),
539
+ totalQuoteEstimatedValueQuote: singleQuoteSummary?.totalQuoteEstimatedValueQuote ?? null,
540
+ totalEstimatedValueQuoteRaw: (totalBaseEstimatedValueQuoteRaw + totalQuoteEstimatedValueQuoteRaw).toString(),
541
+ totalEstimatedValueQuote: singleQuoteSummary?.totalEstimatedValueQuote ?? null,
542
+ valuationSummaryByQuote,
301
543
  },
302
544
  items,
303
545
  };
@@ -21,7 +21,8 @@ export const listPoolsTool = {
21
21
  getPoolList(client),
22
22
  client.markets.getPoolSymbolAll().catch(() => ({ data: [] }))
23
23
  ]);
24
- const poolsRaw = collectRows(poolListRes?.data ?? poolListRes);
24
+ const poolsSource = Array.isArray(poolListRes) ? poolListRes : poolListRes?.data ?? poolListRes;
25
+ const poolsRaw = collectRows(poolsSource);
25
26
  const symbolsRaw = collectRows(symbolsRes?.data ?? symbolsRes);
26
27
  const symbolMap = new Map(symbolsRaw
27
28
  .filter((row) => row?.poolId || row?.pool_id)
@@ -227,7 +227,7 @@ function validateTpSlPriceSemantics(direction, entryPriceInput, tpPriceInput, sl
227
227
  }
228
228
  async function cancelTpSlByIntent(client, address, signer, chainId, args) {
229
229
  if (args.orderId) {
230
- const raw = await client.order.cancelAllOrders([String(args.orderId)], chainId);
230
+ const raw = await client.order.cancelOrder(String(args.orderId), chainId);
231
231
  const data = await finalizeMutationResult(raw, signer, "manage_tp_sl_delete");
232
232
  return {
233
233
  content: [{
@@ -264,7 +264,9 @@ async function cancelTpSlByIntent(client, address, signer, chainId, args) {
264
264
  }]
265
265
  };
266
266
  }
267
- const raw = await client.order.cancelAllOrders(orderIds, chainId);
267
+ const raw = orderIds.length === 1
268
+ ? await client.order.cancelOrder(orderIds[0], chainId)
269
+ : await client.order.cancelOrders(orderIds, chainId);
268
270
  const data = await finalizeMutationResult(raw, signer, "manage_tp_sl_delete");
269
271
  return {
270
272
  content: [{
@@ -31,6 +31,14 @@ function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimal
31
31
  const denominator = pow10(baseDecimals + 30);
32
32
  return numerator / denominator;
33
33
  }
34
+ function divRoundUp(numerator, denominator) {
35
+ if (denominator <= 0n) {
36
+ throw new Error("denominator must be > 0.");
37
+ }
38
+ if (numerator <= 0n)
39
+ return 0n;
40
+ return (numerator + denominator - 1n) / denominator;
41
+ }
34
42
  async function getRequiredApprovalSpendRaw(client, marketId, args, chainId) {
35
43
  const networkFeeText = String(await client.utils.getNetworkFee(marketId, chainId) ?? "").trim();
36
44
  if (!/^\d+$/.test(networkFeeText)) {
@@ -58,6 +66,20 @@ function pickMarketDetail(res) {
58
66
  return res;
59
67
  return null;
60
68
  }
69
+ function resolveQuoteExecutionFeeToken(input, quoteToken) {
70
+ const quoteTokenNormalized = normalizeAddress(quoteToken, "quoteToken");
71
+ const raw = String(input ?? "").trim();
72
+ if (!raw)
73
+ return quoteTokenNormalized;
74
+ if (isZeroAddress(raw)) {
75
+ throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteTokenNormalized}.`);
76
+ }
77
+ const normalized = normalizeAddress(raw, "executionFeeToken");
78
+ if (normalized.toLowerCase() !== quoteTokenNormalized.toLowerCase()) {
79
+ throw new Error(`executionFeeToken must equal the pool quoteToken ${quoteTokenNormalized}. Native token and non-quote tokens are not supported in MCP trade flows.`);
80
+ }
81
+ return quoteTokenNormalized;
82
+ }
61
83
  export const openPositionSimpleTool = {
62
84
  name: "open_position_simple",
63
85
  description: "[TRADE] High-level open position helper. Computes size/price/tradingFee and submits an increase order. Human units by default; use 'raw:' prefix for raw units.",
@@ -79,13 +101,13 @@ export const openPositionSimpleTool = {
79
101
  size: z.coerce
80
102
  .string()
81
103
  .optional()
82
- .describe("Position size. e.g. '0.5' BTC. If omitted, computed from collateral*leverage/price."),
104
+ .describe("Position size as base asset quantity, NOT USD notional. e.g. '0.5' BTC. If omitted, computed from collateral*leverage/price."),
83
105
  slippagePct: z.coerce
84
106
  .string()
85
107
  .optional()
86
108
  .describe(`${SLIPPAGE_PCT_4DP_DESC} Default 50 (=0.5%).`),
87
109
  postOnly: z.coerce.boolean().optional().describe("Post-only (default false)."),
88
- executionFeeToken: z.string().optional().describe("Execution fee token address (default quoteToken)."),
110
+ executionFeeToken: z.string().optional().describe("Optional. Must equal the pool quoteToken address. Defaults to the pool quoteToken."),
89
111
  assetClass: z.coerce.number().int().nonnegative().optional().describe("Fee query assetClass (default 1)."),
90
112
  riskTier: z.coerce.number().int().nonnegative().optional().describe("Fee query riskTier (default 1)."),
91
113
  tradingFee: z.coerce
@@ -103,6 +125,7 @@ export const openPositionSimpleTool = {
103
125
  .optional()
104
126
  .describe("Deprecated compatibility flag. SDK now handles deposit deltas during order creation."),
105
127
  dryRun: z.coerce.boolean().optional().describe("If true, only compute params; do not send a transaction."),
128
+ verify: z.coerce.boolean().optional().describe("If true, wait for backend order-index verification after chain confirmation. Default false for faster responses."),
106
129
  },
107
130
  handler: async (args) => {
108
131
  try {
@@ -142,12 +165,7 @@ export const openPositionSimpleTool = {
142
165
  const orderType = mapOrderType(args.orderType ?? 0);
143
166
  const postOnly = Boolean(args.postOnly ?? false);
144
167
  const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "50");
145
- if (args.executionFeeToken !== undefined && args.executionFeeToken !== null && String(args.executionFeeToken).trim() !== "") {
146
- if (isZeroAddress(args.executionFeeToken)) {
147
- throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteToken}.`);
148
- }
149
- }
150
- const executionFeeToken = normalizeAddress(args.executionFeeToken || quoteToken, "executionFeeToken");
168
+ const executionFeeToken = resolveQuoteExecutionFeeToken(args.executionFeeToken, quoteToken);
151
169
  // 4) Determine reference price (30 decimals)
152
170
  let price30;
153
171
  let priceMeta = { source: "user", publishTime: null, oracleType: null, human: null };
@@ -166,6 +184,7 @@ export const openPositionSimpleTool = {
166
184
  const collateralInput = String(args.collateralAmount ?? "").trim();
167
185
  let collateralRaw = "";
168
186
  let collateralRawBig = 0n;
187
+ let collateralMeta = { source: collateralInput ? "user" : "computed" };
169
188
  if (collateralInput) {
170
189
  collateralRaw = parseUserUnits(collateralInput, quoteDecimals, "collateralAmount");
171
190
  collateralRawBig = asBigint(collateralRaw, "collateralAmount");
@@ -195,12 +214,25 @@ export const openPositionSimpleTool = {
195
214
  sizeMeta = { source: "computed", notionalQuoteRaw: notionalQuoteRaw.toString() };
196
215
  }
197
216
  const sizeRawBig = asBigint(sizeRaw, "size");
217
+ const leverageBig = BigInt(args.leverage);
218
+ const impliedNotionalQuoteRaw = computeQuoteNotionalRaw(sizeRawBig, price30Big, baseDecimals, quoteDecimals);
198
219
  if (!collateralInput) {
199
- const notionalQuoteRaw = (sizeRawBig * price30Big * pow10(quoteDecimals)) / pow10(baseDecimals + 30);
200
- const suggestedCollateralRaw = notionalQuoteRaw / BigInt(args.leverage);
201
- const suggestedCollateralHuman = formatUnits(suggestedCollateralRaw, quoteDecimals);
202
- const notionalHuman = formatUnits(notionalQuoteRaw, quoteDecimals);
203
- throw new Error(`collateralAmount is required for open_position_simple. Based on size=${userSize || formatUnits(sizeRawBig, baseDecimals)}, price=${priceMeta.human ?? formatUnits(price30Big, 30)}, leverage=${args.leverage}, implied order value is ≈${notionalHuman} quote and suggested collateralAmount is ≈${suggestedCollateralHuman}.`);
220
+ collateralRawBig = divRoundUp(impliedNotionalQuoteRaw, leverageBig);
221
+ if (collateralRawBig <= 0n) {
222
+ throw new Error("Computed collateralAmount is 0. Reduce size, reduce leverage, or check price precision.");
223
+ }
224
+ collateralRaw = collateralRawBig.toString();
225
+ collateralMeta = {
226
+ source: "computed",
227
+ impliedNotionalQuoteRaw: impliedNotionalQuoteRaw.toString(),
228
+ };
229
+ }
230
+ else {
231
+ const maxNotionalFromCollateralRaw = collateralRawBig * leverageBig;
232
+ if (maxNotionalFromCollateralRaw < impliedNotionalQuoteRaw) {
233
+ const requiredCollateralRaw = divRoundUp(impliedNotionalQuoteRaw, leverageBig);
234
+ throw new Error(`collateralAmount is insufficient for open_position_simple. Based on size=${userSize || formatUnits(sizeRawBig, baseDecimals)}, price=${priceMeta.human ?? formatUnits(price30Big, 30)}, leverage=${args.leverage}, implied order value is ≈${formatUnits(impliedNotionalQuoteRaw, quoteDecimals)} quote, provided collateralAmount is ${formatUnits(collateralRawBig, quoteDecimals)}, and required collateralAmount is at least ≈${formatUnits(requiredCollateralRaw, quoteDecimals)}.`);
235
+ }
204
236
  }
205
237
  const maxTradeAmountHuman = String(process.env.MAX_TRADE_AMOUNT ?? "").trim();
206
238
  if (maxTradeAmountHuman) {
@@ -267,6 +299,7 @@ export const openPositionSimpleTool = {
267
299
  tradingFeeMeta,
268
300
  autoDeposit: Boolean(args.autoDeposit ?? false),
269
301
  priceMeta,
302
+ collateralMeta,
270
303
  sizeMeta,
271
304
  tpPrice: args.tpPrice ? parseUserPrice30(args.tpPrice, "tpPrice") : null,
272
305
  tpSize: args.tpSize ? parseUserUnits(args.tpSize, baseDecimals, "tpSize") : (args.tpPrice ? sizeRaw : null),
@@ -338,8 +371,32 @@ export const openPositionSimpleTool = {
338
371
  const raw = await openPosition(client, address, openArgs);
339
372
  const data = await finalizeMutationResult(raw, signer, "open_position_simple");
340
373
  const txHash = data.confirmation?.txHash;
341
- const verification = txHash ? await verifyTradeOutcome(client, address, poolId, txHash) : null;
342
- const payload = { prepared: prep, approval, ...data, verification };
374
+ const shouldVerify = Boolean(args.verify ?? false);
375
+ const verification = txHash && shouldVerify ? await verifyTradeOutcome(client, address, poolId, txHash) : null;
376
+ const payload = {
377
+ prepared: prep,
378
+ approval,
379
+ ...data,
380
+ verification,
381
+ verificationSkipped: !!txHash && !shouldVerify,
382
+ preflight: {
383
+ normalized: {
384
+ collateralAmountRaw: collateralRaw,
385
+ sizeRaw,
386
+ priceRaw30: price30,
387
+ sizeSemantics: "base_quantity",
388
+ impliedNotionalQuoteRaw: impliedNotionalQuoteRaw.toString(),
389
+ executionFeeToken,
390
+ timeInForce: openArgs.timeInForce,
391
+ tradingFeeRaw: String(tradingFeeRaw),
392
+ tpSizeRaw: openArgs.tpSize?.replace(/^raw:/i, "") ?? null,
393
+ tpPriceRaw30: openArgs.tpPrice?.replace(/^raw:/i, "") ?? null,
394
+ slSizeRaw: openArgs.slSize?.replace(/^raw:/i, "") ?? null,
395
+ slPriceRaw30: openArgs.slPrice?.replace(/^raw:/i, "") ?? null,
396
+ },
397
+ tradingFeeMeta,
398
+ },
399
+ };
343
400
  return {
344
401
  content: [
345
402
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "3.0.31",
3
+ "version": "3.1.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@modelcontextprotocol/sdk": "^1.27.1",
24
- "@myx-trade/sdk": "^1.0.4-beta.4",
24
+ "@myx-trade/sdk": "^1.0.4",
25
25
  "dotenv": "^17.3.1",
26
26
  "ethers": "^6.13.1",
27
27
  "zod": "^4.3.6"