@catalyst-team/poly-mcp 0.1.1 → 0.1.2

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 +240 -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,654 @@
1
+ /**
2
+ * Insider Detection Tools - MCP tools for detecting suspicious insider wallets
3
+ *
4
+ * These tools enable Agent to analyze wallets for insider trading patterns.
5
+ * Uses algorithms from @catalyst-team/smart-money package.
6
+ *
7
+ * Key features:
8
+ * - Analyze individual wallet for insider characteristics
9
+ * - Scan market trades for suspicious wallets
10
+ * - Get political markets with insider activity summary
11
+ * - Persistent storage in ~/.polymarket/insider-candidates.json
12
+ *
13
+ * @see docs/plans/03-insider-politic-markets/
14
+ */
15
+ import { calculateInsiderScore, getInsiderLevelColor, getInsiderLevelDescription, isNewWallet, hasNoHistory, isSingleSidedBet, isLargePosition, isTimingSensitive, hasShortDepositWindow, hasLowPriceSensitivity, calculatePriceStandardDeviation, calculateReturnMultiple, categorizePoliticalMarket, INSIDER_THRESHOLDS, } from '@catalyst-team/smart-money';
16
+ import { wrapError, McpToolError, ErrorCode } from '../errors.js';
17
+ import * as fs from 'fs/promises';
18
+ import * as path from 'path';
19
+ import * as os from 'os';
20
+ // ============================================================================
21
+ // Storage
22
+ // ============================================================================
23
+ const STORAGE_DIR = path.join(os.homedir(), '.polymarket');
24
+ const CANDIDATES_FILE = path.join(STORAGE_DIR, 'insider-candidates.json');
25
+ async function loadCandidates() {
26
+ try {
27
+ await fs.mkdir(STORAGE_DIR, { recursive: true });
28
+ const data = await fs.readFile(CANDIDATES_FILE, 'utf-8');
29
+ return JSON.parse(data);
30
+ }
31
+ catch {
32
+ return {
33
+ version: 1,
34
+ candidates: {},
35
+ metadata: {
36
+ lastScanAt: 0,
37
+ totalCandidates: 0,
38
+ highScoreCount: 0,
39
+ },
40
+ };
41
+ }
42
+ }
43
+ async function saveCandidates(store) {
44
+ await fs.mkdir(STORAGE_DIR, { recursive: true });
45
+ await fs.writeFile(CANDIDATES_FILE, JSON.stringify(store, null, 2));
46
+ }
47
+ // ============================================================================
48
+ // Tool Definitions
49
+ // ============================================================================
50
+ export const insiderDetectionToolDefinitions = [
51
+ {
52
+ name: 'analyze_wallet_insider',
53
+ description: 'Analyze a wallet for insider trading characteristics. Returns InsiderScore (0-100) with detailed breakdown of features like new wallet, single-sided bet, large position, short deposit window, etc. Based on Venezuela/Greenland case studies.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ address: {
58
+ type: 'string',
59
+ description: 'Wallet address (0x...)',
60
+ },
61
+ targetMarket: {
62
+ type: 'string',
63
+ description: 'Optional: conditionId of target market for timing analysis',
64
+ },
65
+ eventTimestamp: {
66
+ type: 'number',
67
+ description: 'Optional: Unix timestamp of event for timing sensitivity',
68
+ },
69
+ saveCandidate: {
70
+ type: 'boolean',
71
+ description: 'Whether to save high-score candidates (default: true)',
72
+ },
73
+ },
74
+ required: ['address'],
75
+ },
76
+ },
77
+ {
78
+ name: 'scan_insider_wallets',
79
+ description: 'Scan recent trades in a market to detect suspicious insider wallets. Returns list of wallets with InsiderScore >= minScore threshold.',
80
+ inputSchema: {
81
+ type: 'object',
82
+ properties: {
83
+ conditionId: {
84
+ type: 'string',
85
+ description: 'Market condition ID to scan',
86
+ },
87
+ minScore: {
88
+ type: 'number',
89
+ description: 'Minimum InsiderScore threshold (default: 60)',
90
+ },
91
+ limit: {
92
+ type: 'number',
93
+ description: 'Maximum trades to analyze (default: 100)',
94
+ },
95
+ },
96
+ required: ['conditionId'],
97
+ },
98
+ },
99
+ {
100
+ name: 'get_insider_candidates',
101
+ description: 'Get list of detected insider candidates from local storage. Supports filtering by score and sorting.',
102
+ inputSchema: {
103
+ type: 'object',
104
+ properties: {
105
+ minScore: {
106
+ type: 'number',
107
+ description: 'Minimum InsiderScore (default: 0)',
108
+ },
109
+ maxScore: {
110
+ type: 'number',
111
+ description: 'Maximum InsiderScore (default: 100)',
112
+ },
113
+ sortBy: {
114
+ type: 'string',
115
+ enum: ['score', 'analyzedAt', 'potentialProfit'],
116
+ description: 'Sort field (default: score)',
117
+ },
118
+ sortOrder: {
119
+ type: 'string',
120
+ enum: ['asc', 'desc'],
121
+ description: 'Sort order (default: desc)',
122
+ },
123
+ limit: {
124
+ type: 'number',
125
+ description: 'Maximum candidates to return (default: 50)',
126
+ },
127
+ },
128
+ },
129
+ },
130
+ {
131
+ name: 'get_political_markets',
132
+ description: 'Get political markets with insider activity summary. Filters markets by political keywords (election, geopolitics, policy, leadership) and shows insider activity level.',
133
+ inputSchema: {
134
+ type: 'object',
135
+ properties: {
136
+ category: {
137
+ type: 'string',
138
+ enum: [
139
+ 'election',
140
+ 'geopolitics',
141
+ 'policy',
142
+ 'leadership',
143
+ 'international',
144
+ 'all',
145
+ ],
146
+ description: 'Political category filter (default: all)',
147
+ },
148
+ active: {
149
+ type: 'boolean',
150
+ description: 'Only active markets (default: true)',
151
+ },
152
+ limit: {
153
+ type: 'number',
154
+ description: 'Maximum markets to return (default: 20)',
155
+ },
156
+ sortBy: {
157
+ type: 'string',
158
+ enum: ['volume', 'insiderActivity', 'newest'],
159
+ description: 'Sort field (default: volume)',
160
+ },
161
+ },
162
+ },
163
+ },
164
+ ];
165
+ // ============================================================================
166
+ // Helper Functions
167
+ // ============================================================================
168
+ /**
169
+ * Determine market type from market title/description
170
+ */
171
+ function determineMarketType(title, description) {
172
+ const result = categorizePoliticalMarket(title, description);
173
+ if (result.isPolitical) {
174
+ return 'political';
175
+ }
176
+ const text = `${title} ${description || ''}`.toLowerCase();
177
+ if (text.includes('btc') ||
178
+ text.includes('eth') ||
179
+ text.includes('bitcoin') ||
180
+ text.includes('crypto') ||
181
+ text.includes('sol') ||
182
+ text.includes('xrp')) {
183
+ return 'crypto';
184
+ }
185
+ if (text.includes('nfl') ||
186
+ text.includes('nba') ||
187
+ text.includes('soccer') ||
188
+ text.includes('football') ||
189
+ text.includes('match')) {
190
+ return 'sports';
191
+ }
192
+ return 'other';
193
+ }
194
+ /**
195
+ * Calculate characteristics from trader data
196
+ */
197
+ async function calculateCharacteristics(sdk, address, marketInfo) {
198
+ // Get trader profile
199
+ const profile = await sdk.wallets.getWalletProfile(address);
200
+ // Get trader activity
201
+ const activityResult = await sdk.wallets.getWalletActivity(address, { limit: 500 });
202
+ const activity = activityResult.activities || [];
203
+ // Get positions
204
+ const positionsResult = await sdk.dataApi.getPositions(address, { limit: 500 });
205
+ const positions = positionsResult || [];
206
+ // Calculate wallet age
207
+ const now = Date.now();
208
+ const firstActivity = activity.length > 0
209
+ ? Math.min(...activity.map((a) => a.timestamp))
210
+ : now;
211
+ const walletAgeDays = Math.floor((now - firstActivity) / (1000 * 60 * 60 * 24));
212
+ // Calculate trade count
213
+ const trades = activity.filter((a) => a.type === 'TRADE');
214
+ const totalTradeCount = trades.length;
215
+ // Calculate YES bet ratio
216
+ const yesTrades = trades.filter((t) => t.outcome?.toLowerCase() === 'yes');
217
+ const yesBetRatio = totalTradeCount > 0 ? yesTrades.length / totalTradeCount : 0.5;
218
+ // Calculate max single trade
219
+ const maxSingleTradeUsd = trades.reduce((max, t) => Math.max(max, t.usdcValue || 0), 0);
220
+ // Calculate total volume
221
+ const totalVolume = trades.reduce((sum, t) => sum + (t.usdcValue || 0), 0);
222
+ // Get unique markets
223
+ const markets = [...new Set(trades.map((t) => t.conditionId).filter(Boolean))];
224
+ // Calculate deposit to trade time (if available)
225
+ const deposits = activity.filter((a) => a.type === 'SPLIT' || a.type === 'CONVERSION');
226
+ const firstTrade = trades.length > 0 ? trades[trades.length - 1] : null;
227
+ const firstDeposit = deposits.length > 0 ? deposits[deposits.length - 1] : null;
228
+ const depositToTradeMinutes = firstDeposit && firstTrade
229
+ ? Math.max(0, (firstTrade.timestamp - firstDeposit.timestamp) / (1000 * 60))
230
+ : undefined;
231
+ // Calculate price standard deviation
232
+ const buyPrices = trades
233
+ .filter((t) => t.side === 'BUY')
234
+ .map((t) => t.price)
235
+ .filter((p) => p !== undefined && p > 0);
236
+ const priceStandardDeviation = buyPrices.length > 1 ? calculatePriceStandardDeviation(buyPrices) : undefined;
237
+ // Check for failed trades (rough heuristic: very low prices followed by success)
238
+ const hasFailedTrades = trades.some((t) => t.price && t.price < 0.02);
239
+ const successAfterFailure = hasFailedTrades && trades.some((t) => t.price && t.price > 0.05);
240
+ // Calculate return multiple from positions
241
+ const avgPositionPrice = positions.reduce((sum, p) => sum + (p.avgPrice || 0.5), 0) / Math.max(positions.length, 1);
242
+ const returnMultiple = calculateReturnMultiple(avgPositionPrice || 0.5);
243
+ // Determine market type
244
+ const primaryMarketTitle = positions[0]?.title || trades[0]?.marketTitle || '';
245
+ const marketType = determineMarketType(primaryMarketTitle);
246
+ // Calculate timing sensitivity (if event timestamp provided)
247
+ let hoursBeforeEvent;
248
+ if (marketInfo?.eventTimestamp && firstTrade) {
249
+ hoursBeforeEvent =
250
+ (marketInfo.eventTimestamp - firstTrade.timestamp) / (1000 * 60 * 60);
251
+ }
252
+ // Build characteristics
253
+ const characteristics = {
254
+ isNewWallet: isNewWallet(walletAgeDays),
255
+ hasNoHistory: hasNoHistory(totalTradeCount),
256
+ singleSidedBet: isSingleSidedBet(yesBetRatio),
257
+ largePosition: isLargePosition(maxSingleTradeUsd, totalVolume),
258
+ timingSensitive: isTimingSensitive(hoursBeforeEvent),
259
+ shortDepositWindow: hasShortDepositWindow(depositToTradeMinutes),
260
+ lowPriceSensitivity: hasLowPriceSensitivity(priceStandardDeviation),
261
+ twoPhasePattern: hasFailedTrades && successAfterFailure,
262
+ walletAgeDays,
263
+ totalTradeCount,
264
+ maxSingleTradeUsd,
265
+ yesBetRatio,
266
+ hoursBeforeEvent,
267
+ depositToTradeMinutes,
268
+ priceStandardDeviation,
269
+ hasFailedTrades,
270
+ successAfterFailure,
271
+ returnMultiple,
272
+ marketType,
273
+ };
274
+ // Build suspicious trades list
275
+ const suspiciousTrades = trades.slice(0, 20).map((t) => ({
276
+ timestamp: t.timestamp,
277
+ conditionId: t.conditionId || '',
278
+ marketTitle: t.marketTitle || '',
279
+ side: (t.side || 'BUY'),
280
+ outcome: t.outcome || '',
281
+ size: t.size || 0,
282
+ price: t.price || 0,
283
+ usdcValue: t.usdcValue || 0,
284
+ }));
285
+ return {
286
+ characteristics,
287
+ suspiciousTrades,
288
+ totalVolume,
289
+ markets,
290
+ };
291
+ }
292
+ // ============================================================================
293
+ // Handlers
294
+ // ============================================================================
295
+ /**
296
+ * Analyze a wallet for insider characteristics
297
+ */
298
+ export async function handleAnalyzeWalletInsider(sdk, input) {
299
+ try {
300
+ // Validate address
301
+ if (!input.address || !input.address.startsWith('0x')) {
302
+ throw new McpToolError(ErrorCode.INVALID_INPUT, 'Invalid wallet address. Must start with 0x');
303
+ }
304
+ const { characteristics, suspiciousTrades, totalVolume, markets } = await calculateCharacteristics(sdk, input.address, {
305
+ conditionId: input.targetMarket,
306
+ eventTimestamp: input.eventTimestamp,
307
+ });
308
+ // Calculate score
309
+ const scoreResult = calculateInsiderScore({ characteristics });
310
+ // Build candidate
311
+ const now = Date.now();
312
+ const candidate = {
313
+ address: input.address.toLowerCase(),
314
+ insiderScore: scoreResult.score,
315
+ insiderLevel: scoreResult.level,
316
+ characteristics,
317
+ suspiciousTrades: suspiciousTrades.map((t) => ({
318
+ ...t,
319
+ suspiciousReasons: [],
320
+ potentialReturn: 0,
321
+ returnMultiple: characteristics.returnMultiple,
322
+ })),
323
+ markets,
324
+ totalVolume,
325
+ potentialProfit: totalVolume * (characteristics.returnMultiple - 1),
326
+ firstSeen: now,
327
+ lastActivity: suspiciousTrades[0]?.timestamp || now,
328
+ walletAge: characteristics.walletAgeDays,
329
+ tags: [],
330
+ analyzedAt: now,
331
+ analyzedBy: 'agent',
332
+ };
333
+ // Save if high score and saveCandidate is not false
334
+ if (input.saveCandidate !== false && scoreResult.score >= INSIDER_THRESHOLDS.high) {
335
+ const store = await loadCandidates();
336
+ store.candidates[candidate.address] = candidate;
337
+ store.metadata.lastScanAt = now;
338
+ store.metadata.totalCandidates = Object.keys(store.candidates).length;
339
+ store.metadata.highScoreCount = Object.values(store.candidates).filter((c) => c.insiderScore >= INSIDER_THRESHOLDS.critical).length;
340
+ await saveCandidates(store);
341
+ }
342
+ return {
343
+ address: candidate.address,
344
+ insiderScore: scoreResult.score,
345
+ level: scoreResult.level,
346
+ levelColor: getInsiderLevelColor(scoreResult.level),
347
+ levelDescription: getInsiderLevelDescription(scoreResult.level),
348
+ breakdown: {
349
+ baseScore: scoreResult.breakdown.baseScore,
350
+ bonusScore: scoreResult.breakdown.bonusScore,
351
+ features: scoreResult.breakdown.features.map((f) => ({
352
+ name: f.name,
353
+ weight: f.weight,
354
+ matched: f.matched,
355
+ contribution: f.contribution,
356
+ })),
357
+ bonuses: scoreResult.breakdown.bonuses.map((b) => ({
358
+ name: b.name,
359
+ value: b.value,
360
+ matched: b.matched,
361
+ })),
362
+ },
363
+ characteristics: {
364
+ walletAgeDays: characteristics.walletAgeDays,
365
+ totalTradeCount: characteristics.totalTradeCount,
366
+ maxSingleTradeUsd: characteristics.maxSingleTradeUsd,
367
+ yesBetRatio: Math.round(characteristics.yesBetRatio * 100) + '%',
368
+ depositToTradeMinutes: characteristics.depositToTradeMinutes,
369
+ priceStandardDeviation: characteristics.priceStandardDeviation
370
+ ? Math.round(characteristics.priceStandardDeviation * 1000) / 1000
371
+ : undefined,
372
+ returnMultiple: Math.round(characteristics.returnMultiple * 100) / 100 + 'x',
373
+ marketType: characteristics.marketType,
374
+ },
375
+ summary: {
376
+ totalVolume: Math.round(totalVolume * 100) / 100,
377
+ potentialProfit: Math.round(candidate.potentialProfit * 100) / 100,
378
+ markets: markets.length,
379
+ recentTrades: suspiciousTrades.length,
380
+ },
381
+ saved: input.saveCandidate !== false && scoreResult.score >= INSIDER_THRESHOLDS.high,
382
+ };
383
+ }
384
+ catch (err) {
385
+ throw wrapError(err);
386
+ }
387
+ }
388
+ /**
389
+ * Scan market trades for insider wallets
390
+ */
391
+ export async function handleScanInsiderWallets(sdk, input) {
392
+ try {
393
+ const minScore = input.minScore ?? INSIDER_THRESHOLDS.high;
394
+ const limit = input.limit ?? 100;
395
+ // Get market trades
396
+ const trades = await sdk.dataApi.getTradesByMarket(input.conditionId, limit);
397
+ // Get unique wallet addresses (use proxyWallet from Trade type)
398
+ const wallets = [
399
+ ...new Set(trades.map((t) => t.proxyWallet).filter((w) => Boolean(w))),
400
+ ];
401
+ // Analyze each wallet
402
+ const candidates = [];
403
+ for (const address of wallets.slice(0, 20)) {
404
+ // Limit to 20 wallets to avoid rate limiting
405
+ try {
406
+ const result = await handleAnalyzeWalletInsider(sdk, {
407
+ address,
408
+ targetMarket: input.conditionId,
409
+ saveCandidate: true,
410
+ });
411
+ if (result.insiderScore >= minScore) {
412
+ candidates.push({
413
+ address: result.address,
414
+ insiderScore: result.insiderScore,
415
+ level: result.level,
416
+ levelColor: result.levelColor,
417
+ summary: {
418
+ totalVolume: result.summary.totalVolume,
419
+ potentialProfit: result.summary.potentialProfit,
420
+ yesBetRatio: result.characteristics.yesBetRatio,
421
+ marketType: result.characteristics.marketType,
422
+ },
423
+ });
424
+ }
425
+ }
426
+ catch {
427
+ // Skip wallets that fail analysis
428
+ continue;
429
+ }
430
+ }
431
+ // Sort by score descending
432
+ candidates.sort((a, b) => b.insiderScore - a.insiderScore);
433
+ return {
434
+ conditionId: input.conditionId,
435
+ tradesAnalyzed: trades.length,
436
+ walletsScanned: Math.min(wallets.length, 20),
437
+ totalWallets: wallets.length,
438
+ minScoreThreshold: minScore,
439
+ candidates,
440
+ highScoreCount: candidates.filter((c) => c.insiderScore >= INSIDER_THRESHOLDS.critical).length,
441
+ mediumScoreCount: candidates.filter((c) => c.insiderScore >= INSIDER_THRESHOLDS.high &&
442
+ c.insiderScore < INSIDER_THRESHOLDS.critical).length,
443
+ };
444
+ }
445
+ catch (err) {
446
+ throw wrapError(err);
447
+ }
448
+ }
449
+ /**
450
+ * Get insider candidates from storage
451
+ */
452
+ export async function handleGetInsiderCandidates(_sdk, input) {
453
+ try {
454
+ const store = await loadCandidates();
455
+ let candidates = Object.values(store.candidates);
456
+ // Filter by score
457
+ const minScore = input.minScore ?? 0;
458
+ const maxScore = input.maxScore ?? 100;
459
+ candidates = candidates.filter((c) => c.insiderScore >= minScore && c.insiderScore <= maxScore);
460
+ // Sort
461
+ const sortBy = input.sortBy ?? 'score';
462
+ const sortOrder = input.sortOrder ?? 'desc';
463
+ candidates.sort((a, b) => {
464
+ let cmp = 0;
465
+ switch (sortBy) {
466
+ case 'score':
467
+ cmp = a.insiderScore - b.insiderScore;
468
+ break;
469
+ case 'analyzedAt':
470
+ cmp = a.analyzedAt - b.analyzedAt;
471
+ break;
472
+ case 'potentialProfit':
473
+ cmp = a.potentialProfit - b.potentialProfit;
474
+ break;
475
+ }
476
+ return sortOrder === 'desc' ? -cmp : cmp;
477
+ });
478
+ // Limit
479
+ const limit = input.limit ?? 50;
480
+ candidates = candidates.slice(0, limit);
481
+ return {
482
+ candidates: candidates.map((c) => ({
483
+ address: c.address,
484
+ displayName: c.displayName,
485
+ insiderScore: c.insiderScore,
486
+ level: c.insiderLevel,
487
+ levelColor: getInsiderLevelColor(c.insiderLevel),
488
+ levelDescription: getInsiderLevelDescription(c.insiderLevel),
489
+ totalVolume: Math.round(c.totalVolume * 100) / 100,
490
+ potentialProfit: Math.round(c.potentialProfit * 100) / 100,
491
+ markets: c.markets.length,
492
+ walletAgeDays: c.walletAge,
493
+ analyzedAt: new Date(c.analyzedAt).toISOString(),
494
+ tags: c.tags,
495
+ })),
496
+ totalCount: candidates.length,
497
+ metadata: {
498
+ lastScanAt: store.metadata.lastScanAt
499
+ ? new Date(store.metadata.lastScanAt).toISOString()
500
+ : null,
501
+ totalCandidates: store.metadata.totalCandidates,
502
+ highScoreCount: store.metadata.highScoreCount,
503
+ },
504
+ filters: {
505
+ minScore,
506
+ maxScore,
507
+ sortBy,
508
+ sortOrder,
509
+ limit,
510
+ },
511
+ };
512
+ }
513
+ catch (err) {
514
+ throw wrapError(err);
515
+ }
516
+ }
517
+ /**
518
+ * Get political markets with insider activity
519
+ */
520
+ export async function handleGetPoliticalMarkets(sdk, input) {
521
+ try {
522
+ const category = input.category ?? 'all';
523
+ const active = input.active ?? true;
524
+ const limit = input.limit ?? 20;
525
+ // Search for political markets
526
+ const politicalTerms = [
527
+ 'trump',
528
+ 'biden',
529
+ 'election',
530
+ 'president',
531
+ 'congress',
532
+ 'russia',
533
+ 'ukraine',
534
+ 'china',
535
+ 'taiwan',
536
+ 'war',
537
+ 'sanction',
538
+ 'policy',
539
+ 'fed',
540
+ ];
541
+ const allMarkets = [];
542
+ // Fetch active markets sorted by 24h volume
543
+ const fetchedMarkets = await sdk.gammaApi.getMarkets({
544
+ active,
545
+ closed: false,
546
+ order: 'volume24hr',
547
+ ascending: false,
548
+ limit: 500,
549
+ });
550
+ // Filter by political terms
551
+ for (const m of fetchedMarkets) {
552
+ const questionLower = m.question.toLowerCase();
553
+ const slugLower = m.slug.toLowerCase();
554
+ const descLower = (m.description || '').toLowerCase();
555
+ // Check if any political term matches
556
+ const matchesTerm = politicalTerms.some((term) => questionLower.includes(term) ||
557
+ slugLower.includes(term) ||
558
+ descLower.includes(term));
559
+ if (!matchesTerm) {
560
+ continue;
561
+ }
562
+ // Check if already added
563
+ if (allMarkets.some((x) => x.conditionId === m.conditionId)) {
564
+ continue;
565
+ }
566
+ // Categorize
567
+ const result = categorizePoliticalMarket(m.question, m.description);
568
+ if (!result.isPolitical) {
569
+ continue;
570
+ }
571
+ // Filter by category if specified
572
+ if (category !== 'all' && result.category !== category) {
573
+ continue;
574
+ }
575
+ allMarkets.push({
576
+ conditionId: m.conditionId,
577
+ title: m.question,
578
+ slug: m.slug,
579
+ category: result.category,
580
+ matchedFigures: result.matchedFigures,
581
+ matchedRegions: result.matchedRegions,
582
+ confidence: result.confidence,
583
+ currentPrice: {
584
+ yes: m.outcomePrices?.[0] ?? 0.5,
585
+ no: m.outcomePrices?.[1] ?? 0.5,
586
+ },
587
+ volume24h: m.volume24hr || 0,
588
+ active: m.active,
589
+ });
590
+ }
591
+ // Sort
592
+ const sortBy = input.sortBy ?? 'volume';
593
+ allMarkets.sort((a, b) => {
594
+ switch (sortBy) {
595
+ case 'volume':
596
+ return b.volume24h - a.volume24h;
597
+ case 'insiderActivity':
598
+ return b.confidence - a.confidence; // Proxy for now
599
+ case 'newest':
600
+ return 0; // Would need endDate
601
+ default:
602
+ return 0;
603
+ }
604
+ });
605
+ // Limit
606
+ const markets = allMarkets.slice(0, limit);
607
+ // Load insider candidates for activity summary
608
+ const store = await loadCandidates();
609
+ const candidateMarkets = new Set(Object.values(store.candidates).flatMap((c) => c.markets));
610
+ return {
611
+ markets: markets.map((m) => ({
612
+ conditionId: m.conditionId,
613
+ title: m.title,
614
+ slug: m.slug,
615
+ category: m.category,
616
+ categoryName: m.category
617
+ ? {
618
+ election: '选举',
619
+ geopolitics: '地缘政治',
620
+ policy: '政策',
621
+ leadership: '领导人变动',
622
+ international: '国际关系',
623
+ other: '其他政治',
624
+ }[m.category]
625
+ : null,
626
+ matchedFigures: m.matchedFigures,
627
+ matchedRegions: m.matchedRegions,
628
+ confidence: Math.round(m.confidence * 100) + '%',
629
+ currentPrice: {
630
+ yes: Math.round(m.currentPrice.yes * 100) + '¢',
631
+ no: Math.round(m.currentPrice.no * 100) + '¢',
632
+ },
633
+ volume24h: Math.round(m.volume24h),
634
+ hasInsiderActivity: candidateMarkets.has(m.conditionId),
635
+ active: m.active,
636
+ })),
637
+ totalCount: markets.length,
638
+ filters: {
639
+ category,
640
+ active,
641
+ sortBy,
642
+ limit,
643
+ },
644
+ insiderSummary: {
645
+ totalCandidates: store.metadata.totalCandidates,
646
+ marketsWithInsiderActivity: markets.filter((m) => candidateMarkets.has(m.conditionId)).length,
647
+ },
648
+ };
649
+ }
650
+ catch (err) {
651
+ throw wrapError(err);
652
+ }
653
+ }
654
+ //# sourceMappingURL=insider-detection.js.map