@darksol/terminal 0.13.0 → 0.13.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/README.md CHANGED
@@ -57,7 +57,7 @@ darksol bridge status 0xTxHash...
57
57
  darksol bridge chains
58
58
 
59
59
  # Cross-DEX arbitrage
60
- darksol arb scan --chain base # one-shot price comparison
60
+ darksol arb scan --chain base # AI-scored DEX price comparison
61
61
  darksol arb monitor --chain base --execute # real-time block-by-block scanning
62
62
  darksol arb config # set thresholds, dry-run, DEXes
63
63
  darksol arb add-endpoint base wss://your-quicknode # faster with WSS endpoints
@@ -65,6 +65,12 @@ darksol arb add-pair WETH AERO # add pairs to scan
65
65
  darksol arb stats --days 7 # PnL history
66
66
  darksol arb info # setup guide + risk warnings
67
67
 
68
+ # AI arbitrage intelligence
69
+ darksol arb ai # strategy briefing + recommendations
70
+ darksol arb discover --chain base # AI pair discovery
71
+ darksol arb tune --chain base # AI threshold optimization
72
+ darksol arb learn --chain base # learn from history patterns
73
+
68
74
  # Set up your agent identity
69
75
  darksol soul
70
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -17,6 +17,7 @@ import { executeSwap } from './trading/swap.js';
17
17
  import { snipeToken, watchSnipe } from './trading/snipe.js';
18
18
  import { createDCA, listDCA, cancelDCA, runDCA } from './trading/dca.js';
19
19
  import { arbScan, arbMonitor, arbExecute, arbStats, arbConfig, arbAddEndpoint, arbAddPair, arbRemovePair, arbInfo } from './trading/arb.js';
20
+ import { aiDiscoverPairs, aiTuneThresholds, aiStrategyBriefing, aiLearn } from './trading/arb-ai.js';
20
21
  import { executeLifiSwap, executeLifiBridge, checkBridgeStatus, showSupportedChains } from './services/lifi.js';
21
22
  import { topMovers, tokenDetail, compareTokens } from './services/market.js';
22
23
  import { oracleFlip, oracleDice, oracleNumber, oracleShuffle, oracleHealth } from './services/oracle.js';
@@ -400,6 +401,30 @@ export function cli(argv) {
400
401
  .description('How arbitrage works, setup guide, and risk warnings')
401
402
  .action(() => arbInfo());
402
403
 
404
+ arb
405
+ .command('ai')
406
+ .description('AI strategy briefing — assessment, recommendations, next actions')
407
+ .option('-c, --chain <chain>', 'Target chain', 'base')
408
+ .action((opts) => aiStrategyBriefing({ chain: opts.chain }));
409
+
410
+ arb
411
+ .command('discover')
412
+ .description('AI-powered pair discovery — find new opportunities, drop dead pairs')
413
+ .option('-c, --chain <chain>', 'Target chain', 'base')
414
+ .action((opts) => aiDiscoverPairs({ chain: opts.chain }));
415
+
416
+ arb
417
+ .command('tune')
418
+ .description('AI threshold tuning — optimize min profit, trade size, gas ceiling')
419
+ .option('-c, --chain <chain>', 'Target chain', 'base')
420
+ .action((opts) => aiTuneThresholds({ chain: opts.chain }));
421
+
422
+ arb
423
+ .command('learn')
424
+ .description('Run AI learning cycle — analyze history and update patterns')
425
+ .option('-c, --chain <chain>', 'Target chain', 'base')
426
+ .action((opts) => aiLearn({ chain: opts.chain }));
427
+
403
428
  const auto = program
404
429
  .command('auto')
405
430
  .description('Autonomous trader mode - goal-based automated execution');
@@ -0,0 +1,827 @@
1
+ /**
2
+ * arb-ai.js — AI-Powered Arbitrage Intelligence
3
+ *
4
+ * Layers AI decision-making on top of the mechanical arb scanner.
5
+ * Uses the configured LLM provider to:
6
+ * 1. Discover promising pairs to scan
7
+ * 2. Score opportunities beyond raw math
8
+ * 3. Tune thresholds dynamically based on history
9
+ * 4. Learn from past results (what worked, what didn't)
10
+ * 5. Provide natural-language strategy briefings
11
+ */
12
+
13
+ import { LLMEngine } from '../llm/engine.js';
14
+ import { getConfig } from '../config/store.js';
15
+ import { theme } from '../ui/theme.js';
16
+ import { spinner, kvDisplay, success, error, warn, info, card } from '../ui/components.js';
17
+ import { showSection } from '../ui/banner.js';
18
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
19
+ import { join } from 'path';
20
+ import { homedir } from 'os';
21
+
22
+ // ═══════════════════════════════════════════════════════════════
23
+ // PATHS & CONSTANTS
24
+ // ═══════════════════════════════════════════════════════════════
25
+
26
+ const DARKSOL_DIR = join(homedir(), '.darksol');
27
+ const ARB_HISTORY_PATH = join(DARKSOL_DIR, 'arb-history.json');
28
+ const ARB_LEARNINGS_PATH = join(DARKSOL_DIR, 'arb-learnings.json');
29
+ const ARB_AI_LOG_PATH = join(DARKSOL_DIR, 'arb-ai-log.json');
30
+
31
+ function ensureDir() {
32
+ if (!existsSync(DARKSOL_DIR)) mkdirSync(DARKSOL_DIR, { recursive: true });
33
+ }
34
+
35
+ // ═══════════════════════════════════════════════════════════════
36
+ // LEARNING STORE — persistent cross-session intelligence
37
+ // ═══════════════════════════════════════════════════════════════
38
+
39
+ function loadLearnings() {
40
+ ensureDir();
41
+ if (!existsSync(ARB_LEARNINGS_PATH)) return getDefaultLearnings();
42
+ try {
43
+ return JSON.parse(readFileSync(ARB_LEARNINGS_PATH, 'utf-8'));
44
+ } catch {
45
+ return getDefaultLearnings();
46
+ }
47
+ }
48
+
49
+ function saveLearnings(learnings) {
50
+ ensureDir();
51
+ writeFileSync(ARB_LEARNINGS_PATH, JSON.stringify(learnings, null, 2));
52
+ }
53
+
54
+ function getDefaultLearnings() {
55
+ return {
56
+ version: 1,
57
+ updatedAt: new Date().toISOString(),
58
+ // Which pairs have historically produced profitable opportunities
59
+ profitablePairs: [],
60
+ // Which pairs consistently waste gas with no real spread
61
+ deadPairs: [],
62
+ // Best performing DEX combos (e.g. "uniswapV3→aerodrome on base")
63
+ bestDexCombos: [],
64
+ // Time-of-day patterns (hour → avg opportunity count)
65
+ hourlyPatterns: {},
66
+ // Chain performance ranking
67
+ chainRanking: [],
68
+ // Threshold recommendations from AI analysis
69
+ recommendedThresholds: {
70
+ minProfitUsd: 0.50,
71
+ maxTradeSize: 1.0,
72
+ gasCeiling: 0.01,
73
+ },
74
+ // AI-generated strategy notes (natural language)
75
+ strategyNotes: [],
76
+ // Total sessions analyzed
77
+ sessionsAnalyzed: 0,
78
+ };
79
+ }
80
+
81
+ function loadHistory() {
82
+ if (!existsSync(ARB_HISTORY_PATH)) return [];
83
+ try {
84
+ return JSON.parse(readFileSync(ARB_HISTORY_PATH, 'utf-8'));
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+
90
+ function logAiAction(action) {
91
+ ensureDir();
92
+ let log = [];
93
+ if (existsSync(ARB_AI_LOG_PATH)) {
94
+ try { log = JSON.parse(readFileSync(ARB_AI_LOG_PATH, 'utf-8')); } catch {}
95
+ }
96
+ log.push({ ts: new Date().toISOString(), ...action });
97
+ if (log.length > 500) log.splice(0, log.length - 500);
98
+ writeFileSync(ARB_AI_LOG_PATH, JSON.stringify(log, null, 2));
99
+ }
100
+
101
+ // ═══════════════════════════════════════════════════════════════
102
+ // LLM INITIALIZATION
103
+ // ═══════════════════════════════════════════════════════════════
104
+
105
+ async function getEngine() {
106
+ const engine = new LLMEngine({
107
+ temperature: 0.3, // low temp for analytical work
108
+ });
109
+ await engine.init();
110
+ return engine;
111
+ }
112
+
113
+ // ═══════════════════════════════════════════════════════════════
114
+ // SYSTEM PROMPTS
115
+ // ═══════════════════════════════════════════════════════════════
116
+
117
+ const ARB_AI_SYSTEM = `You are DARKSOL Terminal's arbitrage intelligence engine. You analyze cross-DEX arbitrage data and provide actionable insights.
118
+
119
+ You have access to:
120
+ - Historical arb scan results (opportunities found, spreads, gas costs, net profit/loss)
121
+ - Learning data (which pairs/DEXs/times perform best)
122
+ - Current market conditions
123
+
124
+ Your job is to:
125
+ 1. Identify patterns in arb performance data
126
+ 2. Recommend which pairs to focus on and which to drop
127
+ 3. Suggest optimal thresholds (min profit, trade size, gas ceiling)
128
+ 4. Score individual opportunities beyond raw math (consider liquidity depth, token risk, MEV likelihood)
129
+ 5. Provide clear, actionable strategy briefings
130
+
131
+ IMPORTANT CONSTRAINTS:
132
+ - Be honest about limitations — DEX arb is competitive and most simple arb is front-run
133
+ - Never hallucinate token addresses or contract details
134
+ - Base recommendations on actual data, not speculation
135
+ - Always factor in gas costs and MEV risk
136
+ - Flag honeypot tokens or suspicious liquidity patterns
137
+
138
+ RESPONSE FORMAT:
139
+ Always respond with valid JSON. No markdown, no prose outside the JSON structure.`;
140
+
141
+ // ═══════════════════════════════════════════════════════════════
142
+ // AI PAIR DISCOVERY
143
+ // ═══════════════════════════════════════════════════════════════
144
+
145
+ /**
146
+ * Use AI to analyze history and suggest new pairs to scan.
147
+ * Also identifies dead pairs that waste gas.
148
+ */
149
+ export async function aiDiscoverPairs(opts = {}) {
150
+ showSection('AI PAIR DISCOVERY');
151
+
152
+ const spin = spinner('Analyzing arb history for patterns...').start();
153
+
154
+ try {
155
+ const history = loadHistory();
156
+ const learnings = loadLearnings();
157
+ const chain = opts.chain || getConfig('chain') || 'base';
158
+
159
+ if (history.length < 5) {
160
+ spin.fail('Not enough history');
161
+ info('Run at least 5 arb scans first: darksol arb scan');
162
+ info('The AI needs data to find patterns.');
163
+ return null;
164
+ }
165
+
166
+ // Summarize history for the LLM (don't send raw data — too large)
167
+ const summary = summarizeHistory(history, chain);
168
+
169
+ const engine = await getEngine();
170
+ engine.setSystemPrompt(ARB_AI_SYSTEM);
171
+
172
+ const prompt = `Analyze this arb scan history summary and recommend pair strategy.
173
+
174
+ CHAIN: ${chain}
175
+ HISTORY SUMMARY:
176
+ ${JSON.stringify(summary, null, 2)}
177
+
178
+ CURRENT LEARNINGS:
179
+ ${JSON.stringify({
180
+ profitablePairs: learnings.profitablePairs.slice(0, 10),
181
+ deadPairs: learnings.deadPairs.slice(0, 10),
182
+ bestDexCombos: learnings.bestDexCombos.slice(0, 5),
183
+ }, null, 2)}
184
+
185
+ Respond with JSON:
186
+ {
187
+ "addPairs": [{"tokenA": "SYMBOL", "tokenB": "SYMBOL", "reason": "why this pair"}],
188
+ "removePairs": [{"tokenA": "SYMBOL", "tokenB": "SYMBOL", "reason": "why drop it"}],
189
+ "focusPairs": [{"tokenA": "SYMBOL", "tokenB": "SYMBOL", "priority": 1-5, "reason": "why focus"}],
190
+ "insights": ["insight 1", "insight 2"],
191
+ "confidence": 0.0-1.0
192
+ }`;
193
+
194
+ spin.text = 'AI analyzing patterns...';
195
+ const response = await engine.chat(prompt, { skipContext: true });
196
+
197
+ let analysis;
198
+ try {
199
+ // Extract JSON from response (handle markdown wrapping)
200
+ const jsonStr = response.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
201
+ analysis = JSON.parse(jsonStr);
202
+ } catch {
203
+ spin.fail('AI returned invalid JSON');
204
+ error('Could not parse AI response. Try again.');
205
+ return null;
206
+ }
207
+
208
+ spin.succeed('AI analysis complete');
209
+ console.log('');
210
+
211
+ // Display results
212
+ if (analysis.addPairs?.length > 0) {
213
+ console.log(theme.gold(' 📈 Suggested New Pairs:'));
214
+ for (const p of analysis.addPairs) {
215
+ console.log(` ${theme.success('+')} ${theme.bright(p.tokenA + '/' + p.tokenB)} — ${theme.dim(p.reason)}`);
216
+ }
217
+ console.log('');
218
+ }
219
+
220
+ if (analysis.removePairs?.length > 0) {
221
+ console.log(theme.gold(' 📉 Suggested Removals:'));
222
+ for (const p of analysis.removePairs) {
223
+ console.log(` ${theme.error('−')} ${theme.bright(p.tokenA + '/' + p.tokenB)} — ${theme.dim(p.reason)}`);
224
+ }
225
+ console.log('');
226
+ }
227
+
228
+ if (analysis.focusPairs?.length > 0) {
229
+ console.log(theme.gold(' 🎯 Focus Priority:'));
230
+ for (const p of analysis.focusPairs) {
231
+ const stars = '★'.repeat(p.priority) + '☆'.repeat(5 - p.priority);
232
+ console.log(` ${theme.gold(stars)} ${theme.bright(p.tokenA + '/' + p.tokenB)} — ${theme.dim(p.reason)}`);
233
+ }
234
+ console.log('');
235
+ }
236
+
237
+ if (analysis.insights?.length > 0) {
238
+ console.log(theme.gold(' 💡 Insights:'));
239
+ for (const insight of analysis.insights) {
240
+ console.log(` ${theme.dim('•')} ${insight}`);
241
+ }
242
+ console.log('');
243
+ }
244
+
245
+ console.log(theme.dim(` AI confidence: ${((analysis.confidence || 0) * 100).toFixed(0)}%`));
246
+ console.log('');
247
+
248
+ // Update learnings
249
+ if (analysis.focusPairs) {
250
+ learnings.profitablePairs = analysis.focusPairs.map(p => `${p.tokenA}/${p.tokenB}`);
251
+ }
252
+ if (analysis.removePairs) {
253
+ learnings.deadPairs = [
254
+ ...new Set([...learnings.deadPairs, ...analysis.removePairs.map(p => `${p.tokenA}/${p.tokenB}`)]),
255
+ ].slice(0, 50);
256
+ }
257
+ learnings.updatedAt = new Date().toISOString();
258
+ learnings.sessionsAnalyzed++;
259
+ saveLearnings(learnings);
260
+
261
+ logAiAction({ type: 'discover_pairs', chain, result: analysis });
262
+
263
+ return analysis;
264
+
265
+ } catch (err) {
266
+ spin.fail('AI analysis failed');
267
+ error(err.message);
268
+ return null;
269
+ }
270
+ }
271
+
272
+ // ═══════════════════════════════════════════════════════════════
273
+ // AI OPPORTUNITY SCORING
274
+ // ═══════════════════════════════════════════════════════════════
275
+
276
+ /**
277
+ * Score an array of raw arb opportunities using AI.
278
+ * Adds risk assessment, MEV likelihood, and go/no-go recommendation.
279
+ */
280
+ export async function aiScoreOpportunities(opportunities, opts = {}) {
281
+ if (!opportunities || opportunities.length === 0) return [];
282
+
283
+ const engine = await getEngine();
284
+ engine.setSystemPrompt(ARB_AI_SYSTEM);
285
+
286
+ const learnings = loadLearnings();
287
+
288
+ // Only send top opportunities to save tokens
289
+ const top = opportunities
290
+ .sort((a, b) => b.netProfitUsd - a.netProfitUsd)
291
+ .slice(0, 10);
292
+
293
+ const oppData = top.map(o => ({
294
+ pair: o.pair,
295
+ buyDex: o.buyDexName,
296
+ sellDex: o.sellDexName,
297
+ spread: o.spread,
298
+ netProfitUsd: o.netProfitUsd,
299
+ gasCostUsd: o.gasCostUsd,
300
+ chain: o.chain,
301
+ amountInEth: o.amountInEth,
302
+ }));
303
+
304
+ const prompt = `Score these arb opportunities. Consider MEV risk, liquidity depth, token legitimacy, and historical patterns.
305
+
306
+ OPPORTUNITIES:
307
+ ${JSON.stringify(oppData, null, 2)}
308
+
309
+ LEARNED PATTERNS:
310
+ - Profitable pairs: ${learnings.profitablePairs.join(', ') || 'none yet'}
311
+ - Dead pairs: ${learnings.deadPairs.join(', ') || 'none yet'}
312
+ - Best DEX combos: ${learnings.bestDexCombos.join(', ') || 'none yet'}
313
+
314
+ Respond with JSON:
315
+ {
316
+ "scored": [
317
+ {
318
+ "pair": "TOKEN/TOKEN",
319
+ "riskScore": 1-10,
320
+ "mevLikelihood": "low|medium|high",
321
+ "recommendation": "execute|skip|watch",
322
+ "reason": "why",
323
+ "adjustedProfitUsd": 0.00
324
+ }
325
+ ],
326
+ "summary": "one-line overall assessment"
327
+ }`;
328
+
329
+ try {
330
+ const response = await engine.chat(prompt, { skipContext: true });
331
+ const jsonStr = response.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
332
+ const scoring = JSON.parse(jsonStr);
333
+
334
+ logAiAction({ type: 'score', count: top.length, result: scoring });
335
+ return scoring;
336
+ } catch {
337
+ return null;
338
+ }
339
+ }
340
+
341
+ // ═══════════════════════════════════════════════════════════════
342
+ // AI THRESHOLD TUNING
343
+ // ═══════════════════════════════════════════════════════════════
344
+
345
+ /**
346
+ * Analyze history and recommend optimal thresholds.
347
+ */
348
+ export async function aiTuneThresholds(opts = {}) {
349
+ showSection('AI THRESHOLD TUNING');
350
+
351
+ const spin = spinner('Analyzing performance data...').start();
352
+
353
+ try {
354
+ const history = loadHistory();
355
+ const learnings = loadLearnings();
356
+ const chain = opts.chain || getConfig('chain') || 'base';
357
+
358
+ if (history.length < 10) {
359
+ spin.fail('Need more data');
360
+ info('Run at least 10 arb scans before tuning. Current: ' + history.length);
361
+ return null;
362
+ }
363
+
364
+ const summary = summarizeHistory(history, chain);
365
+ const engine = await getEngine();
366
+ engine.setSystemPrompt(ARB_AI_SYSTEM);
367
+
368
+ const prompt = `Analyze this arb performance data and recommend optimal thresholds.
369
+
370
+ CHAIN: ${chain}
371
+ PERFORMANCE SUMMARY:
372
+ ${JSON.stringify(summary, null, 2)}
373
+
374
+ CURRENT THRESHOLDS:
375
+ ${JSON.stringify(learnings.recommendedThresholds, null, 2)}
376
+
377
+ Consider:
378
+ - What minimum profit threshold filters noise without missing real opportunities?
379
+ - What trade size balances risk vs reward?
380
+ - What gas ceiling is appropriate for ${chain}?
381
+ - What cooldown prevents overtrading?
382
+
383
+ Respond with JSON:
384
+ {
385
+ "recommended": {
386
+ "minProfitUsd": 0.00,
387
+ "maxTradeSize": 0.00,
388
+ "gasCeiling": 0.00,
389
+ "cooldownMs": 0
390
+ },
391
+ "changes": [
392
+ {"field": "minProfitUsd", "from": 0.00, "to": 0.00, "reason": "why"}
393
+ ],
394
+ "reasoning": "overall explanation",
395
+ "confidence": 0.0-1.0
396
+ }`;
397
+
398
+ spin.text = 'AI evaluating thresholds...';
399
+ const response = await engine.chat(prompt, { skipContext: true });
400
+
401
+ let tuning;
402
+ try {
403
+ const jsonStr = response.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
404
+ tuning = JSON.parse(jsonStr);
405
+ } catch {
406
+ spin.fail('AI returned invalid response');
407
+ return null;
408
+ }
409
+
410
+ spin.succeed('Threshold analysis complete');
411
+ console.log('');
412
+
413
+ // Display recommendations
414
+ if (tuning.changes?.length > 0) {
415
+ console.log(theme.gold(' 🔧 Recommended Changes:'));
416
+ for (const c of tuning.changes) {
417
+ const arrow = c.to > c.from ? theme.success('↑') : theme.error('↓');
418
+ console.log(` ${arrow} ${theme.bright(c.field)}: ${c.from} → ${theme.gold(String(c.to))}`);
419
+ console.log(` ${theme.dim(c.reason)}`);
420
+ }
421
+ console.log('');
422
+ }
423
+
424
+ if (tuning.reasoning) {
425
+ console.log(theme.gold(' 📝 Reasoning:'));
426
+ console.log(` ${theme.dim(tuning.reasoning)}`);
427
+ console.log('');
428
+ }
429
+
430
+ console.log(theme.dim(` AI confidence: ${((tuning.confidence || 0) * 100).toFixed(0)}%`));
431
+ console.log('');
432
+
433
+ // Save recommended thresholds to learnings
434
+ if (tuning.recommended) {
435
+ learnings.recommendedThresholds = tuning.recommended;
436
+ learnings.updatedAt = new Date().toISOString();
437
+ saveLearnings(learnings);
438
+ }
439
+
440
+ logAiAction({ type: 'tune_thresholds', chain, result: tuning });
441
+
442
+ return tuning;
443
+
444
+ } catch (err) {
445
+ spin.fail('Threshold analysis failed');
446
+ error(err.message);
447
+ return null;
448
+ }
449
+ }
450
+
451
+ // ═══════════════════════════════════════════════════════════════
452
+ // AI STRATEGY BRIEFING
453
+ // ═══════════════════════════════════════════════════════════════
454
+
455
+ /**
456
+ * Generate a natural-language strategy briefing based on all available data.
457
+ */
458
+ export async function aiStrategyBriefing(opts = {}) {
459
+ showSection('AI STRATEGY BRIEFING');
460
+
461
+ const spin = spinner('Generating strategy briefing...').start();
462
+
463
+ try {
464
+ const history = loadHistory();
465
+ const learnings = loadLearnings();
466
+ const chain = opts.chain || getConfig('chain') || 'base';
467
+ const summary = summarizeHistory(history, chain);
468
+
469
+ const engine = await getEngine();
470
+ engine.setSystemPrompt(ARB_AI_SYSTEM);
471
+
472
+ const prompt = `Generate a strategy briefing for DEX arbitrage on ${chain}.
473
+
474
+ PERFORMANCE DATA:
475
+ ${JSON.stringify(summary, null, 2)}
476
+
477
+ LEARNED PATTERNS:
478
+ ${JSON.stringify({
479
+ profitablePairs: learnings.profitablePairs,
480
+ deadPairs: learnings.deadPairs,
481
+ bestDexCombos: learnings.bestDexCombos,
482
+ hourlyPatterns: learnings.hourlyPatterns,
483
+ chainRanking: learnings.chainRanking,
484
+ recommendedThresholds: learnings.recommendedThresholds,
485
+ sessionsAnalyzed: learnings.sessionsAnalyzed,
486
+ strategyNotes: learnings.strategyNotes.slice(-5),
487
+ }, null, 2)}
488
+
489
+ Write a concise strategy briefing. Include:
490
+ 1. Current state assessment (how are we doing?)
491
+ 2. Top recommendations (what should we change?)
492
+ 3. Risk warnings (what could go wrong?)
493
+ 4. Next actions (what should the user do right now?)
494
+
495
+ Respond with JSON:
496
+ {
497
+ "assessment": "current state in 1-2 sentences",
498
+ "performance": {"totalScans": 0, "profitableOpps": 0, "executedTrades": 0, "estimatedPnl": 0},
499
+ "recommendations": ["rec 1", "rec 2", "rec 3"],
500
+ "risks": ["risk 1", "risk 2"],
501
+ "nextActions": ["action 1", "action 2"],
502
+ "confidence": 0.0-1.0
503
+ }`;
504
+
505
+ spin.text = 'AI drafting briefing...';
506
+ const response = await engine.chat(prompt, { skipContext: true });
507
+
508
+ let briefing;
509
+ try {
510
+ const jsonStr = response.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
511
+ briefing = JSON.parse(jsonStr);
512
+ } catch {
513
+ spin.fail('AI returned invalid response');
514
+ return null;
515
+ }
516
+
517
+ spin.succeed('Briefing ready');
518
+ console.log('');
519
+
520
+ // Display briefing
521
+ console.log(theme.gold(' 📋 Assessment:'));
522
+ console.log(` ${briefing.assessment}`);
523
+ console.log('');
524
+
525
+ if (briefing.performance) {
526
+ kvDisplay([
527
+ ['Scans', String(briefing.performance.totalScans || 0)],
528
+ ['Profitable Opps', String(briefing.performance.profitableOpps || 0)],
529
+ ['Executed', String(briefing.performance.executedTrades || 0)],
530
+ ['Est. PnL', `$${(briefing.performance.estimatedPnl || 0).toFixed(4)}`],
531
+ ], { title: 'Performance' });
532
+ console.log('');
533
+ }
534
+
535
+ if (briefing.recommendations?.length > 0) {
536
+ console.log(theme.gold(' 💡 Recommendations:'));
537
+ briefing.recommendations.forEach((r, i) => {
538
+ console.log(` ${theme.gold(String(i + 1) + '.')} ${r}`);
539
+ });
540
+ console.log('');
541
+ }
542
+
543
+ if (briefing.risks?.length > 0) {
544
+ console.log(theme.warning(' ⚠ Risks:'));
545
+ briefing.risks.forEach(r => {
546
+ console.log(` ${theme.error('•')} ${r}`);
547
+ });
548
+ console.log('');
549
+ }
550
+
551
+ if (briefing.nextActions?.length > 0) {
552
+ console.log(theme.success(' ▶ Next Actions:'));
553
+ briefing.nextActions.forEach(a => {
554
+ console.log(` ${theme.info('→')} ${a}`);
555
+ });
556
+ console.log('');
557
+ }
558
+
559
+ // Save briefing to learnings
560
+ learnings.strategyNotes.push({
561
+ ts: new Date().toISOString(),
562
+ chain,
563
+ assessment: briefing.assessment,
564
+ recommendations: briefing.recommendations,
565
+ });
566
+ // Keep last 20 briefings
567
+ if (learnings.strategyNotes.length > 20) {
568
+ learnings.strategyNotes = learnings.strategyNotes.slice(-20);
569
+ }
570
+ learnings.updatedAt = new Date().toISOString();
571
+ saveLearnings(learnings);
572
+
573
+ logAiAction({ type: 'briefing', chain, result: briefing });
574
+
575
+ return briefing;
576
+
577
+ } catch (err) {
578
+ spin.fail('Briefing failed');
579
+ error(err.message);
580
+ return null;
581
+ }
582
+ }
583
+
584
+ // ═══════════════════════════════════════════════════════════════
585
+ // AI LEARN — analyze history and update patterns
586
+ // ═══════════════════════════════════════════════════════════════
587
+
588
+ /**
589
+ * Run a learning cycle — analyze recent history and update persistent learnings.
590
+ * Should be run periodically (after a batch of scans or daily).
591
+ */
592
+ export async function aiLearn(opts = {}) {
593
+ showSection('AI LEARNING CYCLE');
594
+
595
+ const spin = spinner('Analyzing recent arb data...').start();
596
+
597
+ try {
598
+ const history = loadHistory();
599
+ const learnings = loadLearnings();
600
+ const chain = opts.chain || getConfig('chain') || 'base';
601
+
602
+ if (history.length < 3) {
603
+ spin.fail('Not enough data to learn from');
604
+ info('Run more scans first.');
605
+ return null;
606
+ }
607
+
608
+ // Extract patterns from raw data (no LLM needed for this)
609
+ const chainHistory = history.filter(h => h.chain === chain);
610
+
611
+ // Hourly pattern analysis
612
+ const hourBuckets = {};
613
+ for (const h of chainHistory) {
614
+ const hour = new Date(h.ts).getHours();
615
+ if (!hourBuckets[hour]) hourBuckets[hour] = { count: 0, profitable: 0 };
616
+ hourBuckets[hour].count++;
617
+ if (h.netProfitUsd > 0) hourBuckets[hour].profitable++;
618
+ }
619
+ learnings.hourlyPatterns = hourBuckets;
620
+
621
+ // Best DEX combos
622
+ const comboCounts = {};
623
+ for (const h of chainHistory.filter(h => h.netProfitUsd > 0)) {
624
+ const combo = `${h.buyDex}→${h.sellDex}`;
625
+ comboCounts[combo] = (comboCounts[combo] || 0) + 1;
626
+ }
627
+ learnings.bestDexCombos = Object.entries(comboCounts)
628
+ .sort((a, b) => b[1] - a[1])
629
+ .slice(0, 10)
630
+ .map(([combo, count]) => `${combo} (${count}x)`);
631
+
632
+ // Pair profitability
633
+ const pairProfits = {};
634
+ for (const h of chainHistory) {
635
+ const pair = h.pair;
636
+ if (!pair) continue;
637
+ if (!pairProfits[pair]) pairProfits[pair] = { total: 0, profitable: 0, totalProfit: 0 };
638
+ pairProfits[pair].total++;
639
+ if (h.netProfitUsd > 0) {
640
+ pairProfits[pair].profitable++;
641
+ pairProfits[pair].totalProfit += h.netProfitUsd;
642
+ }
643
+ }
644
+
645
+ learnings.profitablePairs = Object.entries(pairProfits)
646
+ .filter(([, data]) => data.profitable / data.total > 0.1) // >10% success rate
647
+ .sort((a, b) => b[1].totalProfit - a[1].totalProfit)
648
+ .slice(0, 20)
649
+ .map(([pair]) => pair);
650
+
651
+ learnings.deadPairs = Object.entries(pairProfits)
652
+ .filter(([, data]) => data.total >= 5 && data.profitable === 0) // 5+ scans, never profitable
653
+ .map(([pair]) => pair);
654
+
655
+ // Chain ranking
656
+ const chainCounts = {};
657
+ for (const h of history.filter(h => h.netProfitUsd > 0)) {
658
+ chainCounts[h.chain] = (chainCounts[h.chain] || 0) + 1;
659
+ }
660
+ learnings.chainRanking = Object.entries(chainCounts)
661
+ .sort((a, b) => b[1] - a[1])
662
+ .map(([c, count]) => `${c} (${count} opps)`);
663
+
664
+ learnings.updatedAt = new Date().toISOString();
665
+ learnings.sessionsAnalyzed++;
666
+ saveLearnings(learnings);
667
+
668
+ spin.succeed('Learning cycle complete');
669
+ console.log('');
670
+
671
+ // Display what was learned
672
+ kvDisplay([
673
+ ['Data Points', chainHistory.length.toString()],
674
+ ['Profitable Pairs', learnings.profitablePairs.length.toString()],
675
+ ['Dead Pairs', learnings.deadPairs.length.toString()],
676
+ ['Best DEX Combos', learnings.bestDexCombos.slice(0, 3).join(', ') || 'none yet'],
677
+ ['Chain Ranking', learnings.chainRanking.join(', ') || 'none yet'],
678
+ ['Sessions', learnings.sessionsAnalyzed.toString()],
679
+ ], { title: 'Learned Patterns' });
680
+ console.log('');
681
+
682
+ // Show hourly heatmap
683
+ if (Object.keys(hourBuckets).length > 0) {
684
+ console.log(theme.gold(' 🕐 Hourly Opportunity Heatmap:'));
685
+ const maxCount = Math.max(...Object.values(hourBuckets).map(b => b.profitable));
686
+ for (let h = 0; h < 24; h++) {
687
+ const bucket = hourBuckets[h] || { count: 0, profitable: 0 };
688
+ const bar = maxCount > 0 ? '█'.repeat(Math.ceil((bucket.profitable / maxCount) * 20)) : '';
689
+ const hour = String(h).padStart(2, '0') + ':00';
690
+ const color = bucket.profitable > 0 ? theme.success : theme.dim;
691
+ console.log(` ${theme.dim(hour)} ${color(bar)} ${theme.dim(String(bucket.profitable) + '/' + String(bucket.count))}`);
692
+ }
693
+ console.log('');
694
+ }
695
+
696
+ logAiAction({ type: 'learn', chain, patternsFound: learnings.profitablePairs.length });
697
+
698
+ return learnings;
699
+
700
+ } catch (err) {
701
+ spin.fail('Learning cycle failed');
702
+ error(err.message);
703
+ return null;
704
+ }
705
+ }
706
+
707
+ // ═══════════════════════════════════════════════════════════════
708
+ // AI-ENHANCED MONITOR FILTER
709
+ // ═══════════════════════════════════════════════════════════════
710
+
711
+ /**
712
+ * Quick AI filter for the monitor loop.
713
+ * Uses learnings (no LLM call) to filter opportunities in real-time.
714
+ * This is the fast path — no API calls, pure pattern matching.
715
+ */
716
+ export function aiFilterOpportunity(opportunity) {
717
+ const learnings = loadLearnings();
718
+
719
+ let score = 50; // base score out of 100
720
+
721
+ // Boost if pair is in profitable list
722
+ if (learnings.profitablePairs.includes(opportunity.pair)) {
723
+ score += 20;
724
+ }
725
+
726
+ // Penalize if pair is in dead list
727
+ if (learnings.deadPairs.includes(opportunity.pair)) {
728
+ score -= 40;
729
+ }
730
+
731
+ // Boost if DEX combo is known good
732
+ const combo = `${opportunity.buyDex}→${opportunity.sellDex}`;
733
+ if (learnings.bestDexCombos.some(c => c.startsWith(combo))) {
734
+ score += 15;
735
+ }
736
+
737
+ // Time-of-day boost
738
+ const currentHour = new Date().getHours();
739
+ const hourData = learnings.hourlyPatterns[currentHour];
740
+ if (hourData && hourData.profitable > 0) {
741
+ score += Math.min(10, hourData.profitable * 2);
742
+ }
743
+
744
+ // Apply learned thresholds
745
+ const thresholds = learnings.recommendedThresholds;
746
+ if (thresholds.minProfitUsd && opportunity.netProfitUsd < thresholds.minProfitUsd) {
747
+ score -= 20;
748
+ }
749
+
750
+ // Clamp score
751
+ score = Math.max(0, Math.min(100, score));
752
+
753
+ return {
754
+ score,
755
+ pass: score >= 40,
756
+ reason: score >= 70 ? 'strong pattern match'
757
+ : score >= 40 ? 'acceptable'
758
+ : 'below AI threshold',
759
+ };
760
+ }
761
+
762
+ // ═══════════════════════════════════════════════════════════════
763
+ // HISTORY SUMMARIZER (for LLM context)
764
+ // ═══════════════════════════════════════════════════════════════
765
+
766
+ function summarizeHistory(history, chain) {
767
+ const chainHistory = history.filter(h => h.chain === chain);
768
+ const last7d = chainHistory.filter(h => Date.now() - new Date(h.ts).getTime() < 7 * 86400 * 1000);
769
+
770
+ // Pair frequency
771
+ const pairCounts = {};
772
+ const pairProfits = {};
773
+ for (const h of last7d) {
774
+ const pair = h.pair || 'unknown';
775
+ pairCounts[pair] = (pairCounts[pair] || 0) + 1;
776
+ if (!pairProfits[pair]) pairProfits[pair] = { sum: 0, count: 0 };
777
+ pairProfits[pair].sum += (h.netProfitUsd || 0);
778
+ pairProfits[pair].count++;
779
+ }
780
+
781
+ // DEX frequency
782
+ const dexCounts = {};
783
+ for (const h of last7d) {
784
+ if (h.buyDex) dexCounts[h.buyDex] = (dexCounts[h.buyDex] || 0) + 1;
785
+ if (h.sellDex) dexCounts[h.sellDex] = (dexCounts[h.sellDex] || 0) + 1;
786
+ }
787
+
788
+ // Spread statistics
789
+ const spreads = last7d.map(h => h.spread || 0).filter(s => s > 0);
790
+ const avgSpread = spreads.length > 0 ? spreads.reduce((a, b) => a + b, 0) / spreads.length : 0;
791
+ const maxSpread = Math.max(0, ...spreads);
792
+
793
+ // Profit statistics
794
+ const profits = last7d.map(h => h.netProfitUsd || 0);
795
+ const totalProfit = profits.reduce((a, b) => a + b, 0);
796
+ const profitableCount = profits.filter(p => p > 0).length;
797
+
798
+ // Gas statistics
799
+ const gasCosts = last7d.map(h => h.gasCostUsd || 0);
800
+ const avgGas = gasCosts.length > 0 ? gasCosts.reduce((a, b) => a + b, 0) / gasCosts.length : 0;
801
+
802
+ return {
803
+ chain,
804
+ totalEntries: chainHistory.length,
805
+ last7dEntries: last7d.length,
806
+ pairBreakdown: Object.entries(pairCounts)
807
+ .sort((a, b) => b[1] - a[1])
808
+ .slice(0, 10)
809
+ .map(([pair, count]) => ({
810
+ pair,
811
+ count,
812
+ avgProfit: pairProfits[pair] ? (pairProfits[pair].sum / pairProfits[pair].count).toFixed(4) : '0',
813
+ })),
814
+ dexUsage: dexCounts,
815
+ avgSpread: avgSpread.toFixed(4),
816
+ maxSpread: maxSpread.toFixed(4),
817
+ totalProfitUsd: totalProfit.toFixed(4),
818
+ profitableOppCount: profitableCount,
819
+ avgGasCostUsd: avgGas.toFixed(4),
820
+ types: {
821
+ scans: last7d.filter(h => h.type === 'scan').length,
822
+ executed: last7d.filter(h => h.type === 'executed').length,
823
+ dryRuns: last7d.filter(h => h.type === 'dry_run').length,
824
+ errors: last7d.filter(h => h.type === 'error').length,
825
+ },
826
+ };
827
+ }
@@ -22,6 +22,7 @@ import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/com
22
22
  import { showSection } from '../ui/banner.js';
23
23
  import { resolveToken, getTokenInfo } from './swap.js';
24
24
  import { getDexesForChain, DEX_ADAPTERS } from './arb-dexes.js';
25
+ import { aiFilterOpportunity, aiScoreOpportunities } from './arb-ai.js';
25
26
 
26
27
  // ═══════════════════════════════════════════════════════════════
27
28
  // CONSTANTS & PATHS
@@ -346,21 +347,63 @@ export async function arbScan(opts = {}) {
346
347
  console.log('');
347
348
 
348
349
  const profitable = allOpps.filter(o => o.netProfitUsd >= minProfit);
349
- displayOpportunities(profitable);
350
+
351
+ // Apply AI pattern filter (fast, no API call)
352
+ const aiFiltered = profitable.map(o => {
353
+ const aiResult = aiFilterOpportunity(o);
354
+ return { ...o, aiScore: aiResult.score, aiPass: aiResult.pass, aiReason: aiResult.reason };
355
+ });
356
+ const aiPassed = aiFiltered.filter(o => o.aiPass);
357
+ const aiSkipped = aiFiltered.length - aiPassed.length;
358
+
359
+ displayOpportunities(aiPassed);
360
+
361
+ if (aiSkipped > 0) {
362
+ info(`AI filter skipped ${aiSkipped} low-confidence opportunity(s) — run 'darksol arb learn' to improve accuracy`);
363
+ console.log('');
364
+ }
365
+
366
+ // AI deep scoring for top opportunities (uses LLM)
367
+ if (aiPassed.length > 0 && !opts.skipAi) {
368
+ const scoreSpin = spinner('AI scoring opportunities...').start();
369
+ try {
370
+ const scoring = await aiScoreOpportunities(aiPassed);
371
+ if (scoring?.scored?.length > 0) {
372
+ scoreSpin.succeed('AI risk scoring complete');
373
+ console.log('');
374
+ console.log(theme.gold(' 🧠 AI Risk Assessment:'));
375
+ for (const s of scoring.scored) {
376
+ const riskColor = s.riskScore <= 3 ? theme.success : s.riskScore <= 6 ? theme.warning : theme.error;
377
+ const recColor = s.recommendation === 'execute' ? theme.success : s.recommendation === 'watch' ? theme.warning : theme.error;
378
+ console.log(` ${theme.bright(s.pair)} — risk: ${riskColor(String(s.riskScore) + '/10')} | MEV: ${theme.dim(s.mevLikelihood)} | ${recColor(s.recommendation.toUpperCase())}`);
379
+ console.log(` ${theme.dim(s.reason)}`);
380
+ }
381
+ if (scoring.summary) {
382
+ console.log('');
383
+ console.log(` ${theme.dim('Summary: ' + scoring.summary)}`);
384
+ }
385
+ console.log('');
386
+ } else {
387
+ scoreSpin.succeed('AI scoring returned no results');
388
+ }
389
+ } catch {
390
+ scoreSpin.warn('AI scoring unavailable — showing raw results');
391
+ }
392
+ }
350
393
 
351
394
  // Log everything (including unprofitable) to history
352
395
  for (const o of allOpps) {
353
396
  recordArb({ type: 'scan', ...o });
354
397
  }
355
398
 
356
- if (profitable.length > 0) {
399
+ if (aiPassed.length > 0) {
357
400
  console.log('');
358
401
  console.log(theme.warning(' ⚠ MEV Warning: ') + theme.dim('simple two-tx arb is likely to be front-run.'));
359
402
  console.log(theme.dim(' Use WSS endpoints + Flashbots bundles for reliable execution.'));
360
403
  console.log('');
361
404
  }
362
405
 
363
- return profitable;
406
+ return aiPassed;
364
407
  } catch (err) {
365
408
  spin.fail('Scan failed');
366
409
  error(err.message);
@@ -427,15 +470,20 @@ export async function arbMonitor(opts = {}) {
427
470
  }
428
471
 
429
472
  const profitable = allOpps.filter(o => o.netProfitUsd >= minProfit);
430
- oppsFound += profitable.length;
431
473
 
432
- if (profitable.length > 0) {
433
- blockSpin.succeed(`[Block ${blockNumber}] ${profitable.length} opportunity(s) found`);
434
- displayOpportunities(profitable.slice(0, 5));
474
+ // AI pattern filter (fast, no API call)
475
+ const aiPassed = profitable.filter(o => aiFilterOpportunity(o).pass);
476
+ oppsFound += aiPassed.length;
477
+
478
+ if (aiPassed.length > 0) {
479
+ const skipped = profitable.length - aiPassed.length;
480
+ const skipNote = skipped > 0 ? ` (${skipped} AI-filtered)` : '';
481
+ blockSpin.succeed(`[Block ${blockNumber}] ${aiPassed.length} opportunity(s) found${skipNote}`);
482
+ displayOpportunities(aiPassed.slice(0, 5));
435
483
 
436
484
  // Auto-execute if requested and cooldown satisfied
437
485
  if (execute && !dryRun && Date.now() - lastExecute > (arbCfg.cooldownMs || 5000)) {
438
- const best = profitable.sort((a, b) => b.netProfitUsd - a.netProfitUsd)[0];
486
+ const best = aiPassed.sort((a, b) => b.netProfitUsd - a.netProfitUsd)[0];
439
487
  if (best.netProfitUsd >= minProfit) {
440
488
  await arbExecute({ opportunity: best, dryRun: false, skipConfirm: true });
441
489
  lastExecute = Date.now();
@@ -443,7 +491,7 @@ export async function arbMonitor(opts = {}) {
443
491
  }
444
492
  }
445
493
  } else {
446
- blockSpin.text = `[Block ${blockNumber}] No profitable arb found (${allOpps.length} pairs scanned)`;
494
+ blockSpin.text = `[Block ${blockNumber}] No profitable arb found (${allOpps.length} scanned, ${profitable.length} pre-filter)`;
447
495
  blockSpin.succeed();
448
496
  }
449
497
 
@@ -1390,14 +1390,58 @@ async function cmdArb(args, ws) {
1390
1390
  return {};
1391
1391
  }
1392
1392
 
1393
+ if (sub === 'ai') {
1394
+ try {
1395
+ const { aiStrategyBriefing } = await import('../trading/arb-ai.js');
1396
+ await aiStrategyBriefing({ chain: args[1] || getConfig('chain') || 'base' });
1397
+ } catch (e) {
1398
+ ws.sendLine(` ${ANSI.red}AI briefing failed: ${e.message}${ANSI.reset}`);
1399
+ }
1400
+ return {};
1401
+ }
1402
+
1403
+ if (sub === 'discover') {
1404
+ try {
1405
+ const { aiDiscoverPairs } = await import('../trading/arb-ai.js');
1406
+ await aiDiscoverPairs({ chain: args[1] || getConfig('chain') || 'base' });
1407
+ } catch (e) {
1408
+ ws.sendLine(` ${ANSI.red}Discovery failed: ${e.message}${ANSI.reset}`);
1409
+ }
1410
+ return {};
1411
+ }
1412
+
1413
+ if (sub === 'tune') {
1414
+ try {
1415
+ const { aiTuneThresholds } = await import('../trading/arb-ai.js');
1416
+ await aiTuneThresholds({ chain: args[1] || getConfig('chain') || 'base' });
1417
+ } catch (e) {
1418
+ ws.sendLine(` ${ANSI.red}Tuning failed: ${e.message}${ANSI.reset}`);
1419
+ }
1420
+ return {};
1421
+ }
1422
+
1423
+ if (sub === 'learn') {
1424
+ try {
1425
+ const { aiLearn } = await import('../trading/arb-ai.js');
1426
+ await aiLearn({ chain: args[1] || getConfig('chain') || 'base' });
1427
+ } catch (e) {
1428
+ ws.sendLine(` ${ANSI.red}Learning failed: ${e.message}${ANSI.reset}`);
1429
+ }
1430
+ return {};
1431
+ }
1432
+
1393
1433
  // Default: show arb menu
1394
1434
  ws.sendLine(`${ANSI.gold} ◆ ARBITRAGE${ANSI.reset}`);
1395
1435
  ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1396
- ws.sendLine(` ${ANSI.white}Cross-DEX arbitrage scanner${ANSI.reset}`);
1436
+ ws.sendLine(` ${ANSI.white}AI-powered cross-DEX arbitrage${ANSI.reset}`);
1397
1437
  ws.sendLine('');
1398
1438
 
1399
1439
  ws.sendMenu('arb_action', '◆ Arb Actions', [
1400
- { value: 'arb scan', label: '🔍 Scan', desc: 'One-shot DEX price comparison' },
1440
+ { value: 'arb scan', label: '🔍 Scan', desc: 'AI-scored DEX price comparison' },
1441
+ { value: 'arb ai', label: '🧠 AI Briefing', desc: 'Strategy assessment + recommendations' },
1442
+ { value: 'arb discover', label: '📈 Discover', desc: 'AI pair discovery — find new opportunities' },
1443
+ { value: 'arb tune', label: '🔧 Tune', desc: 'AI threshold optimization' },
1444
+ { value: 'arb learn', label: '📚 Learn', desc: 'Run learning cycle on history' },
1401
1445
  { value: 'arb stats', label: '📊 Stats', desc: 'View arb history & PnL' },
1402
1446
  { value: 'arb info', label: '📖 Guide', desc: 'How arb works, setup tips, risks' },
1403
1447
  { value: 'back', label: '← Back', desc: '' },