@catalyst-team/poly-mcp 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +264 -21
  2. package/dist/errors.d.ts +11 -0
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +13 -2
  5. package/dist/errors.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +98 -5
  8. package/dist/index.js.map +1 -1
  9. package/dist/sdk-instance.d.ts +27 -0
  10. package/dist/sdk-instance.d.ts.map +1 -0
  11. package/dist/sdk-instance.js +64 -0
  12. package/dist/sdk-instance.js.map +1 -0
  13. package/dist/server.d.ts +13 -1
  14. package/dist/server.d.ts.map +1 -1
  15. package/dist/server.js +29 -27
  16. package/dist/server.js.map +1 -1
  17. package/dist/tools/guide.d.ts.map +1 -1
  18. package/dist/tools/guide.js +159 -1
  19. package/dist/tools/guide.js.map +1 -1
  20. package/dist/tools/index.d.ts +8 -4
  21. package/dist/tools/index.d.ts.map +1 -1
  22. package/dist/tools/index.js +20 -4
  23. package/dist/tools/index.js.map +1 -1
  24. package/dist/tools/insider-detection.d.ts +175 -0
  25. package/dist/tools/insider-detection.d.ts.map +1 -0
  26. package/dist/tools/insider-detection.js +654 -0
  27. package/dist/tools/insider-detection.js.map +1 -0
  28. package/dist/tools/insider-signals.d.ts +56 -0
  29. package/dist/tools/insider-signals.d.ts.map +1 -0
  30. package/dist/tools/insider-signals.js +170 -0
  31. package/dist/tools/insider-signals.js.map +1 -0
  32. package/dist/tools/market.d.ts +25 -1
  33. package/dist/tools/market.d.ts.map +1 -1
  34. package/dist/tools/market.js +504 -12
  35. package/dist/tools/market.js.map +1 -1
  36. package/dist/tools/onchain.d.ts +240 -0
  37. package/dist/tools/onchain.d.ts.map +1 -0
  38. package/dist/tools/onchain.js +610 -0
  39. package/dist/tools/onchain.js.map +1 -0
  40. package/dist/tools/order.d.ts.map +1 -1
  41. package/dist/tools/order.js +13 -6
  42. package/dist/tools/order.js.map +1 -1
  43. package/dist/tools/trade.d.ts +15 -0
  44. package/dist/tools/trade.d.ts.map +1 -1
  45. package/dist/tools/trade.js +216 -39
  46. package/dist/tools/trade.js.map +1 -1
  47. package/dist/tools/trader.d.ts +4 -1
  48. package/dist/tools/trader.d.ts.map +1 -1
  49. package/dist/tools/trader.js +316 -4
  50. package/dist/tools/trader.js.map +1 -1
  51. package/dist/tools/wallet-classification.d.ts +166 -0
  52. package/dist/tools/wallet-classification.d.ts.map +1 -0
  53. package/dist/tools/wallet-classification.js +455 -0
  54. package/dist/tools/wallet-classification.js.map +1 -0
  55. package/dist/tools/wallet.d.ts +56 -7
  56. package/dist/tools/wallet.d.ts.map +1 -1
  57. package/dist/tools/wallet.js +141 -20
  58. package/dist/tools/wallet.js.map +1 -1
  59. package/dist/types.d.ts +269 -10
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/wallet-manager.d.ts +67 -0
  62. package/dist/wallet-manager.d.ts.map +1 -0
  63. package/dist/wallet-manager.js +180 -0
  64. package/dist/wallet-manager.js.map +1 -0
  65. package/docs/01-mcp.md +554 -32
  66. package/docs/02-wallet-deep-research.md +344 -0
  67. package/docs/e2e-02/00-gap-analysis.md +211 -0
  68. package/docs/e2e-02/01-test-scenarios.md +530 -0
  69. package/docs/e2e-02/02-implementation-plan.md +190 -0
  70. package/docs/e2e-02/README.md +102 -0
  71. package/docs/reports/simonbanza-strategy-analysis-2025-12-25.md +420 -0
  72. package/docs/reports/smart-money-analysis-2025-12-23-cn.md +840 -0
  73. package/docs/reports/smart-money-trading-strategies-2025-12-25.md +440 -0
  74. package/docs/reports/weekly/01-v2.5.md +352 -0
  75. package/docs/reports/weekly/01.md +402 -0
  76. package/docs/reports/weekly/02-deep.md +558 -0
  77. package/docs/reports/weekly/02.md +505 -0
  78. package/docs/reports/weekly/03.md +437 -0
  79. package/docs/reports/weekly/04.md +418 -0
  80. package/docs/reports/weekly/05.md +485 -0
  81. package/docs/reports/weekly/06.md +436 -0
  82. package/docs/reports/weekly/07.md +381 -0
  83. package/docs/reports/weekly/08.md +502 -0
  84. package/docs/reports/weekly/09.md +441 -0
  85. package/docs/reports/weekly/10.md +511 -0
  86. package/docs/reports/weekly/README.md +188 -0
  87. package/docs/reports/weekly/prompt-v2.5.md +1019 -0
  88. package/docs/reports/weekly/prompt-v3.md +432 -0
  89. package/docs/reports/weekly/prompt.md +841 -0
  90. package/package.json +3 -2
  91. package/src/errors.ts +13 -2
  92. package/src/index.ts +286 -1
  93. package/src/sdk-instance.ts +78 -0
  94. package/src/server.ts +30 -28
  95. package/src/tools/guide.ts +160 -1
  96. package/src/tools/index.ts +65 -0
  97. package/src/tools/insider-detection.ts +899 -0
  98. package/src/tools/insider-signals.ts +213 -0
  99. package/src/tools/market.ts +569 -12
  100. package/src/tools/onchain.ts +738 -0
  101. package/src/tools/order.ts +25 -12
  102. package/src/tools/trade.ts +265 -53
  103. package/src/tools/trader.ts +350 -4
  104. package/src/tools/wallet-classification.ts +587 -0
  105. package/src/tools/wallet.ts +172 -23
  106. package/src/types.ts +294 -11
  107. package/src/wallet-manager.ts +209 -0
@@ -0,0 +1,738 @@
1
+ /**
2
+ * Onchain Tools - MCP tools for CTF (Conditional Token Framework) operations
3
+ *
4
+ * These tools use the OnchainService from poly-sdk:
5
+ * - Split: USDC -> YES + NO tokens
6
+ * - Merge: YES + NO tokens -> USDC
7
+ * - Redeem: Winning tokens -> USDC (after market resolution)
8
+ * - Position balance queries
9
+ * - Market resolution status
10
+ * - Gas estimation
11
+ *
12
+ * ## Token ID Auto-Discovery
13
+ *
14
+ * Polymarket CLOB markets use custom ERC-1155 token IDs that differ from
15
+ * standard CTF calculated positionIds. These tools auto-discover tokenIds
16
+ * from the CLOB API when not explicitly provided.
17
+ *
18
+ * ## Dynamic Outcome Names
19
+ *
20
+ * Markets can have different outcome names:
21
+ * - Yes/No (standard)
22
+ * - Up/Down (crypto short-term)
23
+ * - Team A/Team B (sports)
24
+ *
25
+ * The tools handle all outcome types dynamically.
26
+ */
27
+
28
+ import {
29
+ OnchainService,
30
+ type TokenIds,
31
+ type ResolvedMarketTokens,
32
+ } from '@catalyst-team/poly-sdk';
33
+ import type { ToolDefinition } from '../types.js';
34
+ import { wrapError, McpToolError, ErrorCode, validateConditionId } from '../errors.js';
35
+ import { getWalletManager } from '../wallet-manager.js';
36
+ import { getMarketService } from '../sdk-instance.js';
37
+
38
+ /**
39
+ * Resolved market tokens with tokenIds for CTF operations
40
+ */
41
+ interface ResolvedTokens {
42
+ tokenIds: TokenIds;
43
+ primaryOutcome: string;
44
+ secondaryOutcome: string;
45
+ }
46
+
47
+ /**
48
+ * Auto-discover tokenIds using poly-sdk's MarketService
49
+ *
50
+ * Uses MarketService.resolveMarketTokens() which fetches actual tokenIds from CLOB API.
51
+ *
52
+ * @param conditionId - Market condition ID
53
+ * @returns Resolved tokens with IDs and outcome names, or null if not found
54
+ */
55
+ async function resolveMarketTokens(conditionId: string): Promise<ResolvedTokens | null> {
56
+ try {
57
+ const marketService = getMarketService();
58
+ const resolved = await marketService.resolveMarketTokens(conditionId);
59
+
60
+ if (!resolved) {
61
+ return null;
62
+ }
63
+
64
+ // Convert to CTFClient's TokenIds format (legacy naming)
65
+ return {
66
+ tokenIds: {
67
+ yesTokenId: resolved.primaryTokenId,
68
+ noTokenId: resolved.secondaryTokenId,
69
+ },
70
+ primaryOutcome: resolved.primaryOutcome,
71
+ secondaryOutcome: resolved.secondaryOutcome,
72
+ };
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ // ===== Tool Definitions =====
79
+
80
+ export const onchainToolDefinitions: ToolDefinition[] = [
81
+ {
82
+ name: 'ctf_split',
83
+ description: 'Split USDC into YES + NO tokens for a market. Requires USDC.e (bridged USDC) balance.',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ conditionId: {
88
+ type: 'string',
89
+ description: 'Market condition ID (0x...)',
90
+ },
91
+ amount: {
92
+ type: 'string',
93
+ description: 'Amount of USDC to split (e.g., "100" for 100 USDC)',
94
+ },
95
+ },
96
+ required: ['conditionId', 'amount'],
97
+ },
98
+ },
99
+ {
100
+ name: 'ctf_merge',
101
+ description: `Merge primary + secondary tokens back to USDC.
102
+
103
+ TokenIds are auto-discovered from CLOB API if not provided.
104
+ Works with any outcome types (Yes/No, Up/Down, Team A/B, etc.).`,
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ conditionId: {
109
+ type: 'string',
110
+ description: 'Market condition ID (0x...)',
111
+ },
112
+ amount: {
113
+ type: 'string',
114
+ description: 'Number of token pairs to merge (e.g., "100" for 100 pairs)',
115
+ },
116
+ tokenIds: {
117
+ type: 'object',
118
+ description: 'Optional: Explicit token IDs {yesTokenId: string, noTokenId: string}. Auto-discovered if not provided.',
119
+ },
120
+ },
121
+ required: ['conditionId', 'amount'],
122
+ },
123
+ },
124
+ {
125
+ name: 'ctf_redeem',
126
+ description: `Redeem winning tokens after market resolution.
127
+
128
+ TokenIds are auto-discovered from CLOB API if not provided.
129
+ Outcome names are dynamic (Yes/No, Up/Down, Team A/B, etc.).`,
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {
133
+ conditionId: {
134
+ type: 'string',
135
+ description: 'Market condition ID (0x...)',
136
+ },
137
+ tokenIds: {
138
+ type: 'object',
139
+ description: 'Optional: Explicit token IDs {yesTokenId: string, noTokenId: string}. Auto-discovered if not provided.',
140
+ },
141
+ outcome: {
142
+ type: 'string',
143
+ description: 'Optional: Specific outcome to redeem (e.g., "Yes", "Up", "Team A"). Uses market\'s actual outcome names.',
144
+ },
145
+ },
146
+ required: ['conditionId'],
147
+ },
148
+ },
149
+ {
150
+ name: 'get_position_balance',
151
+ description: `Get token balances for a market position.
152
+
153
+ TokenIds are auto-discovered from CLOB API if not provided.
154
+ Returns balances for both primary and secondary outcome tokens.`,
155
+ inputSchema: {
156
+ type: 'object',
157
+ properties: {
158
+ conditionId: {
159
+ type: 'string',
160
+ description: 'Market condition ID (0x...)',
161
+ },
162
+ tokenIds: {
163
+ type: 'object',
164
+ description: 'Optional: Explicit token IDs {yesTokenId: string, noTokenId: string}. Auto-discovered if not provided.',
165
+ },
166
+ },
167
+ required: ['conditionId'],
168
+ },
169
+ },
170
+ {
171
+ name: 'get_market_resolution',
172
+ description: 'Check if a market is resolved and get payout information.',
173
+ inputSchema: {
174
+ type: 'object',
175
+ properties: {
176
+ conditionId: {
177
+ type: 'string',
178
+ description: 'Market condition ID (0x...)',
179
+ },
180
+ },
181
+ required: ['conditionId'],
182
+ },
183
+ },
184
+ {
185
+ name: 'check_ctf_ready',
186
+ description: 'Check if wallet is ready for CTF trading (balances, approvals, gas).',
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ amount: {
191
+ type: 'string',
192
+ description: 'Amount of USDC to check readiness for (e.g., "100")',
193
+ },
194
+ },
195
+ required: ['amount'],
196
+ },
197
+ },
198
+ {
199
+ name: 'estimate_gas',
200
+ description: 'Estimate gas cost for a CTF operation (split or merge).',
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: {
204
+ operation: {
205
+ type: 'string',
206
+ enum: ['split', 'merge'],
207
+ description: 'Type of operation to estimate',
208
+ },
209
+ conditionId: {
210
+ type: 'string',
211
+ description: 'Market condition ID (0x...)',
212
+ },
213
+ amount: {
214
+ type: 'string',
215
+ description: 'Amount for the operation',
216
+ },
217
+ },
218
+ required: ['operation', 'conditionId', 'amount'],
219
+ },
220
+ },
221
+ {
222
+ name: 'get_gas_price',
223
+ description: 'Get current gas price on Polygon network.',
224
+ inputSchema: {
225
+ type: 'object',
226
+ properties: {},
227
+ },
228
+ },
229
+ ];
230
+
231
+ // ===== Input Types =====
232
+
233
+ interface CtfSplitInput {
234
+ conditionId: string;
235
+ amount: string;
236
+ }
237
+
238
+ interface CtfMergeInput {
239
+ conditionId: string;
240
+ amount: string;
241
+ tokenIds?: TokenIds;
242
+ }
243
+
244
+ interface CtfRedeemInput {
245
+ conditionId: string;
246
+ tokenIds?: TokenIds;
247
+ /** Dynamic outcome name - can be Yes/No, Up/Down, Team A/B, etc. */
248
+ outcome?: string;
249
+ }
250
+
251
+ /**
252
+ * Convert user outcome to SDK format (uppercase)
253
+ *
254
+ * The SDK expects uppercase outcome names.
255
+ * This function normalizes any outcome name to uppercase.
256
+ */
257
+ function toSdkOutcome(outcome?: string): string | undefined {
258
+ if (!outcome) return undefined;
259
+ return outcome.toUpperCase();
260
+ }
261
+
262
+ interface GetPositionBalanceInput {
263
+ conditionId: string;
264
+ tokenIds?: TokenIds;
265
+ }
266
+
267
+ interface GetMarketResolutionInput {
268
+ conditionId: string;
269
+ }
270
+
271
+ interface CheckCtfReadyInput {
272
+ amount: string;
273
+ }
274
+
275
+ interface EstimateGasInput {
276
+ operation: 'split' | 'merge';
277
+ conditionId: string;
278
+ amount: string;
279
+ }
280
+
281
+ // ===== Helper: Get OnchainService =====
282
+
283
+ // Cache OnchainService instances by wallet address to avoid re-creating
284
+ const onchainServiceCache = new Map<string, OnchainService>();
285
+
286
+ function getOnchainService(): OnchainService {
287
+ const walletManager = getWalletManager();
288
+ const activeWallet = walletManager.getActiveWallet();
289
+
290
+ if (!activeWallet) {
291
+ throw new McpToolError(
292
+ ErrorCode.AUTH_REQUIRED,
293
+ 'No wallet configured. Set POLY_PRIVATE_KEY or POLY_WALLETS environment variable.'
294
+ );
295
+ }
296
+
297
+ // Check cache first
298
+ const cacheKey = activeWallet.address.toLowerCase();
299
+ const cached = onchainServiceCache.get(cacheKey);
300
+ if (cached) {
301
+ return cached;
302
+ }
303
+
304
+ // Create new service for active wallet
305
+ const service = new OnchainService({
306
+ privateKey: activeWallet.wallet.privateKey,
307
+ rpcUrl: process.env.POLY_RPC_URL || 'https://polygon-rpc.com',
308
+ });
309
+
310
+ onchainServiceCache.set(cacheKey, service);
311
+ return service;
312
+ }
313
+
314
+ // ===== Handler Functions =====
315
+
316
+ /**
317
+ * Split USDC into YES + NO tokens
318
+ */
319
+ export async function handleCtfSplit(input: CtfSplitInput) {
320
+ validateConditionId(input.conditionId);
321
+
322
+ if (!input.amount || parseFloat(input.amount) <= 0) {
323
+ throw new McpToolError(
324
+ ErrorCode.INVALID_INPUT,
325
+ 'Amount must be a positive number'
326
+ );
327
+ }
328
+
329
+ try {
330
+ const service = getOnchainService();
331
+
332
+ // Check if can split
333
+ const canSplitCheck = await service.canSplit(input.amount);
334
+ if (!canSplitCheck.canSplit) {
335
+ return {
336
+ success: false,
337
+ error: canSplitCheck.reason,
338
+ wallet: service.getAddress(),
339
+ conditionId: input.conditionId,
340
+ amount: input.amount,
341
+ };
342
+ }
343
+
344
+ // Execute split
345
+ const result = await service.split(input.conditionId, input.amount);
346
+
347
+ return {
348
+ success: result.success,
349
+ wallet: service.getAddress(),
350
+ conditionId: input.conditionId,
351
+ amount: input.amount,
352
+ yesTokens: result.yesTokens,
353
+ noTokens: result.noTokens,
354
+ txHash: result.txHash,
355
+ gasUsed: result.gasUsed,
356
+ explorerUrl: `https://polygonscan.com/tx/${result.txHash}`,
357
+ message: `Split ${input.amount} USDC into ${result.yesTokens} YES + ${result.noTokens} NO tokens`,
358
+ };
359
+ } catch (err) {
360
+ throw wrapError(err);
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Merge primary + secondary tokens back to USDC
366
+ *
367
+ * Automatically discovers tokenIds from CLOB API if not provided.
368
+ */
369
+ export async function handleCtfMerge(input: CtfMergeInput) {
370
+ validateConditionId(input.conditionId);
371
+
372
+ if (!input.amount || parseFloat(input.amount) <= 0) {
373
+ throw new McpToolError(
374
+ ErrorCode.INVALID_INPUT,
375
+ 'Amount must be a positive number'
376
+ );
377
+ }
378
+
379
+ try {
380
+ const service = getOnchainService();
381
+
382
+ // Auto-discover tokenIds if not provided
383
+ let tokenIds = input.tokenIds;
384
+ let resolvedTokens: ResolvedTokens | null = null;
385
+ let tokenIdsSource: 'provided' | 'auto-discovered' | 'calculated' = 'provided';
386
+
387
+ if (!tokenIds) {
388
+ resolvedTokens = await resolveMarketTokens(input.conditionId);
389
+ if (resolvedTokens) {
390
+ tokenIds = resolvedTokens.tokenIds;
391
+ tokenIdsSource = 'auto-discovered';
392
+ } else {
393
+ tokenIdsSource = 'calculated';
394
+ }
395
+ }
396
+
397
+ // Check if can merge
398
+ let canMergeCheck;
399
+ if (tokenIds) {
400
+ canMergeCheck = await service.canMergeWithTokenIds(
401
+ input.conditionId,
402
+ tokenIds,
403
+ input.amount
404
+ );
405
+ } else {
406
+ canMergeCheck = await service.canMerge(input.conditionId, input.amount);
407
+ }
408
+
409
+ if (!canMergeCheck.canMerge) {
410
+ // Replace hardcoded YES/NO in error message with dynamic outcome names
411
+ let errorMessage = canMergeCheck.reason || 'Insufficient tokens';
412
+ if (resolvedTokens) {
413
+ errorMessage = errorMessage
414
+ .replace(/YES tokens/g, `${resolvedTokens.primaryOutcome} tokens`)
415
+ .replace(/NO tokens/g, `${resolvedTokens.secondaryOutcome} tokens`);
416
+ }
417
+
418
+ return {
419
+ success: false,
420
+ error: errorMessage,
421
+ wallet: service.getAddress(),
422
+ conditionId: input.conditionId,
423
+ amount: input.amount,
424
+ tokenIdsSource,
425
+ outcomeNames: resolvedTokens
426
+ ? { primary: resolvedTokens.primaryOutcome, secondary: resolvedTokens.secondaryOutcome }
427
+ : undefined,
428
+ hint: tokenIdsSource === 'calculated'
429
+ ? 'TokenIds could not be auto-discovered. This market may require explicit tokenIds.'
430
+ : undefined,
431
+ };
432
+ }
433
+
434
+ // Execute merge
435
+ let result;
436
+ if (tokenIds) {
437
+ result = await service.mergeByTokenIds(
438
+ input.conditionId,
439
+ tokenIds,
440
+ input.amount
441
+ );
442
+ } else {
443
+ result = await service.merge(input.conditionId, input.amount);
444
+ }
445
+
446
+ // Use dynamic outcome names if available
447
+ const primaryName = resolvedTokens?.primaryOutcome ?? 'YES';
448
+ const secondaryName = resolvedTokens?.secondaryOutcome ?? 'NO';
449
+
450
+ return {
451
+ success: result.success,
452
+ wallet: service.getAddress(),
453
+ conditionId: input.conditionId,
454
+ amount: input.amount,
455
+ usdcReceived: result.usdcReceived,
456
+ txHash: result.txHash,
457
+ gasUsed: result.gasUsed,
458
+ explorerUrl: `https://polygonscan.com/tx/${result.txHash}`,
459
+ tokenIdsSource,
460
+ outcomeNames: resolvedTokens
461
+ ? { primary: resolvedTokens.primaryOutcome, secondary: resolvedTokens.secondaryOutcome }
462
+ : undefined,
463
+ message: `Merged ${input.amount} ${primaryName} + ${input.amount} ${secondaryName} tokens into ${result.usdcReceived} USDC`,
464
+ };
465
+ } catch (err) {
466
+ throw wrapError(err);
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Redeem winning tokens after market resolution
472
+ *
473
+ * Automatically discovers tokenIds from CLOB API if not provided.
474
+ */
475
+ export async function handleCtfRedeem(input: CtfRedeemInput) {
476
+ validateConditionId(input.conditionId);
477
+
478
+ try {
479
+ const service = getOnchainService();
480
+
481
+ // Check market resolution first
482
+ const resolution = await service.getMarketResolution(input.conditionId);
483
+ if (!resolution.isResolved) {
484
+ return {
485
+ success: false,
486
+ error: 'Market is not yet resolved. Cannot redeem tokens.',
487
+ wallet: service.getAddress(),
488
+ conditionId: input.conditionId,
489
+ isResolved: false,
490
+ };
491
+ }
492
+
493
+ // Auto-discover tokenIds if not provided
494
+ let tokenIds = input.tokenIds;
495
+ let resolvedTokens: ResolvedTokens | null = null;
496
+ let tokenIdsSource: 'provided' | 'auto-discovered' | 'calculated' = 'provided';
497
+
498
+ if (!tokenIds) {
499
+ resolvedTokens = await resolveMarketTokens(input.conditionId);
500
+ if (resolvedTokens) {
501
+ tokenIds = resolvedTokens.tokenIds;
502
+ tokenIdsSource = 'auto-discovered';
503
+ } else {
504
+ tokenIdsSource = 'calculated';
505
+ }
506
+ }
507
+
508
+ // Execute redeem - convert outcome format for SDK
509
+ const sdkOutcome = toSdkOutcome(input.outcome);
510
+ let result;
511
+ if (tokenIds) {
512
+ result = await service.redeemByTokenIds(
513
+ input.conditionId,
514
+ tokenIds,
515
+ sdkOutcome
516
+ );
517
+ } else {
518
+ // Fallback to calculated positionIds (may fail for CLOB markets)
519
+ result = await service.redeem(input.conditionId, sdkOutcome);
520
+ }
521
+
522
+ // Use dynamic outcome names if available
523
+ const primaryName = resolvedTokens?.primaryOutcome ?? 'YES';
524
+ const secondaryName = resolvedTokens?.secondaryOutcome ?? 'NO';
525
+ const winningOutcomeName = resolution.winningOutcome === 'YES'
526
+ ? primaryName
527
+ : resolution.winningOutcome === 'NO'
528
+ ? secondaryName
529
+ : resolution.winningOutcome;
530
+
531
+ return {
532
+ success: result.success,
533
+ wallet: service.getAddress(),
534
+ conditionId: input.conditionId,
535
+ winningOutcome: winningOutcomeName,
536
+ tokensRedeemed: result.tokensRedeemed,
537
+ usdcReceived: result.usdcReceived,
538
+ txHash: result.txHash,
539
+ gasUsed: result.gasUsed,
540
+ explorerUrl: `https://polygonscan.com/tx/${result.txHash}`,
541
+ tokenIdsSource,
542
+ outcomeNames: resolvedTokens
543
+ ? { primary: resolvedTokens.primaryOutcome, secondary: resolvedTokens.secondaryOutcome }
544
+ : undefined,
545
+ message: `Redeemed ${result.tokensRedeemed} winning ${winningOutcomeName} tokens for ${result.usdcReceived} USDC`,
546
+ };
547
+ } catch (err) {
548
+ throw wrapError(err);
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Get token balances for a market position
554
+ *
555
+ * Automatically discovers tokenIds from CLOB API if not provided.
556
+ */
557
+ export async function handleGetPositionBalance(input: GetPositionBalanceInput) {
558
+ validateConditionId(input.conditionId);
559
+
560
+ try {
561
+ const service = getOnchainService();
562
+
563
+ // Auto-discover tokenIds if not provided
564
+ let tokenIds = input.tokenIds;
565
+ let resolvedTokens: ResolvedTokens | null = null;
566
+ let tokenIdsSource: 'provided' | 'auto-discovered' | 'calculated' = 'provided';
567
+
568
+ if (!tokenIds) {
569
+ resolvedTokens = await resolveMarketTokens(input.conditionId);
570
+ if (resolvedTokens) {
571
+ tokenIds = resolvedTokens.tokenIds;
572
+ tokenIdsSource = 'auto-discovered';
573
+ } else {
574
+ tokenIdsSource = 'calculated';
575
+ }
576
+ }
577
+
578
+ let balance;
579
+ if (tokenIds) {
580
+ balance = await service.getPositionBalanceByTokenIds(
581
+ input.conditionId,
582
+ tokenIds
583
+ );
584
+ } else {
585
+ balance = await service.getPositionBalance(input.conditionId);
586
+ }
587
+
588
+ // Use dynamic outcome names if available
589
+ const primaryName = resolvedTokens?.primaryOutcome ?? 'yes';
590
+ const secondaryName = resolvedTokens?.secondaryOutcome ?? 'no';
591
+
592
+ return {
593
+ wallet: service.getAddress(),
594
+ conditionId: input.conditionId,
595
+ balances: {
596
+ [primaryName.toLowerCase()]: balance.yesBalance,
597
+ [secondaryName.toLowerCase()]: balance.noBalance,
598
+ },
599
+ positionIds: {
600
+ [primaryName.toLowerCase()]: balance.yesPositionId,
601
+ [secondaryName.toLowerCase()]: balance.noPositionId,
602
+ },
603
+ outcomeNames: resolvedTokens
604
+ ? { primary: resolvedTokens.primaryOutcome, secondary: resolvedTokens.secondaryOutcome }
605
+ : undefined,
606
+ tokenIdsSource,
607
+ hasPosition: parseFloat(balance.yesBalance) > 0 || parseFloat(balance.noBalance) > 0,
608
+ };
609
+ } catch (err) {
610
+ throw wrapError(err);
611
+ }
612
+ }
613
+
614
+ /**
615
+ * Check if market is resolved
616
+ */
617
+ export async function handleGetMarketResolution(input: GetMarketResolutionInput) {
618
+ validateConditionId(input.conditionId);
619
+
620
+ try {
621
+ const service = getOnchainService();
622
+ const resolution = await service.getMarketResolution(input.conditionId);
623
+
624
+ return {
625
+ conditionId: input.conditionId,
626
+ isResolved: resolution.isResolved,
627
+ winningOutcome: resolution.winningOutcome,
628
+ payoutNumerators: resolution.payoutNumerators,
629
+ payoutDenominator: resolution.payoutDenominator,
630
+ message: resolution.isResolved
631
+ ? `Market resolved: ${resolution.winningOutcome} wins`
632
+ : 'Market is not yet resolved',
633
+ };
634
+ } catch (err) {
635
+ throw wrapError(err);
636
+ }
637
+ }
638
+
639
+ /**
640
+ * Check if wallet is ready for CTF trading
641
+ */
642
+ export async function handleCheckCtfReady(input: CheckCtfReadyInput) {
643
+ if (!input.amount || parseFloat(input.amount) <= 0) {
644
+ throw new McpToolError(
645
+ ErrorCode.INVALID_INPUT,
646
+ 'Amount must be a positive number'
647
+ );
648
+ }
649
+
650
+ try {
651
+ const service = getOnchainService();
652
+ const status = await service.checkReadyForCTF(input.amount);
653
+
654
+ return {
655
+ wallet: service.getAddress(),
656
+ ready: status.ready,
657
+ balances: {
658
+ usdcE: status.usdcEBalance,
659
+ nativeUsdc: status.nativeUsdcBalance,
660
+ matic: status.maticBalance,
661
+ },
662
+ tradingReady: status.tradingReady,
663
+ issues: status.issues,
664
+ suggestion: status.suggestion,
665
+ message: status.ready
666
+ ? `Ready to trade with ${input.amount} USDC`
667
+ : `Not ready: ${status.issues.join(', ')}`,
668
+ };
669
+ } catch (err) {
670
+ throw wrapError(err);
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Estimate gas for CTF operation
676
+ */
677
+ export async function handleEstimateGas(input: EstimateGasInput) {
678
+ validateConditionId(input.conditionId);
679
+
680
+ if (!input.amount || parseFloat(input.amount) <= 0) {
681
+ throw new McpToolError(
682
+ ErrorCode.INVALID_INPUT,
683
+ 'Amount must be a positive number'
684
+ );
685
+ }
686
+
687
+ if (!['split', 'merge'].includes(input.operation)) {
688
+ throw new McpToolError(
689
+ ErrorCode.INVALID_INPUT,
690
+ 'Operation must be "split" or "merge"'
691
+ );
692
+ }
693
+
694
+ try {
695
+ const service = getOnchainService();
696
+
697
+ let estimate;
698
+ if (input.operation === 'split') {
699
+ estimate = await service.estimateSplitGas(input.conditionId, input.amount);
700
+ } else {
701
+ estimate = await service.estimateMergeGas(input.conditionId, input.amount);
702
+ }
703
+
704
+ return {
705
+ operation: input.operation,
706
+ conditionId: input.conditionId,
707
+ amount: input.amount,
708
+ gas: {
709
+ gasUnits: estimate.gasUnits,
710
+ gasPriceGwei: estimate.gasPriceGwei,
711
+ costMatic: estimate.costMatic,
712
+ costUsdc: estimate.costUsdc,
713
+ maticPrice: estimate.maticPrice,
714
+ },
715
+ message: `Estimated gas for ${input.operation}: ${estimate.costMatic} MATIC (~$${estimate.costUsdc})`,
716
+ };
717
+ } catch (err) {
718
+ throw wrapError(err);
719
+ }
720
+ }
721
+
722
+ /**
723
+ * Get current gas price
724
+ */
725
+ export async function handleGetGasPrice() {
726
+ try {
727
+ const service = getOnchainService();
728
+ const gasPrice = await service.getGasPrice();
729
+
730
+ return {
731
+ gwei: gasPrice.gwei,
732
+ wei: gasPrice.wei,
733
+ message: `Current gas price: ${gasPrice.gwei} Gwei`,
734
+ };
735
+ } catch (err) {
736
+ throw wrapError(err);
737
+ }
738
+ }