@exagent/agent 0.1.0

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/dist/cli.js ADDED
@@ -0,0 +1,1828 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_commander = require("commander");
28
+ var import_dotenv2 = require("dotenv");
29
+ var readline = __toESM(require("readline"));
30
+ var fs = __toESM(require("fs"));
31
+ var path = __toESM(require("path"));
32
+
33
+ // src/runtime.ts
34
+ var import_sdk = require("@exagent/sdk");
35
+
36
+ // src/llm/openai.ts
37
+ var import_openai = __toESM(require("openai"));
38
+
39
+ // src/llm/base.ts
40
+ var BaseLLMAdapter = class {
41
+ config;
42
+ constructor(config) {
43
+ this.config = config;
44
+ }
45
+ getMetadata() {
46
+ return {
47
+ provider: this.config.provider,
48
+ model: this.config.model || "unknown",
49
+ isLocal: this.config.provider === "ollama"
50
+ };
51
+ }
52
+ /**
53
+ * Format model name for display
54
+ */
55
+ getDisplayModel() {
56
+ if (this.config.provider === "ollama") {
57
+ return `Local (${this.config.model || "ollama"})`;
58
+ }
59
+ return this.config.model || this.config.provider;
60
+ }
61
+ };
62
+
63
+ // src/llm/openai.ts
64
+ var OpenAIAdapter = class extends BaseLLMAdapter {
65
+ client;
66
+ constructor(config) {
67
+ super(config);
68
+ if (!config.apiKey && !config.endpoint) {
69
+ throw new Error("OpenAI API key or custom endpoint required");
70
+ }
71
+ this.client = new import_openai.default({
72
+ apiKey: config.apiKey || "not-needed-for-custom",
73
+ baseURL: config.endpoint
74
+ });
75
+ }
76
+ async chat(messages) {
77
+ try {
78
+ const response = await this.client.chat.completions.create({
79
+ model: this.config.model || "gpt-4-turbo-preview",
80
+ messages: messages.map((m) => ({
81
+ role: m.role,
82
+ content: m.content
83
+ })),
84
+ temperature: this.config.temperature,
85
+ max_tokens: this.config.maxTokens
86
+ });
87
+ const choice = response.choices[0];
88
+ if (!choice || !choice.message) {
89
+ throw new Error("No response from OpenAI");
90
+ }
91
+ return {
92
+ content: choice.message.content || "",
93
+ usage: response.usage ? {
94
+ promptTokens: response.usage.prompt_tokens,
95
+ completionTokens: response.usage.completion_tokens,
96
+ totalTokens: response.usage.total_tokens
97
+ } : void 0
98
+ };
99
+ } catch (error) {
100
+ if (error instanceof import_openai.default.APIError) {
101
+ throw new Error(`OpenAI API error: ${error.message}`);
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+ };
107
+
108
+ // src/llm/anthropic.ts
109
+ var AnthropicAdapter = class extends BaseLLMAdapter {
110
+ apiKey;
111
+ baseUrl;
112
+ constructor(config) {
113
+ super(config);
114
+ if (!config.apiKey) {
115
+ throw new Error("Anthropic API key required");
116
+ }
117
+ this.apiKey = config.apiKey;
118
+ this.baseUrl = config.endpoint || "https://api.anthropic.com";
119
+ }
120
+ async chat(messages) {
121
+ const systemMessage = messages.find((m) => m.role === "system");
122
+ const chatMessages = messages.filter((m) => m.role !== "system");
123
+ const body = {
124
+ model: this.config.model || "claude-3-opus-20240229",
125
+ max_tokens: this.config.maxTokens || 4096,
126
+ temperature: this.config.temperature,
127
+ system: systemMessage?.content,
128
+ messages: chatMessages.map((m) => ({
129
+ role: m.role,
130
+ content: m.content
131
+ }))
132
+ };
133
+ const response = await fetch(`${this.baseUrl}/v1/messages`, {
134
+ method: "POST",
135
+ headers: {
136
+ "Content-Type": "application/json",
137
+ "x-api-key": this.apiKey,
138
+ "anthropic-version": "2023-06-01"
139
+ },
140
+ body: JSON.stringify(body)
141
+ });
142
+ if (!response.ok) {
143
+ const error = await response.text();
144
+ throw new Error(`Anthropic API error: ${response.status} - ${error}`);
145
+ }
146
+ const data = await response.json();
147
+ const content = data.content?.map(
148
+ (block) => block.type === "text" ? block.text : ""
149
+ ).join("") || "";
150
+ return {
151
+ content,
152
+ usage: data.usage ? {
153
+ promptTokens: data.usage.input_tokens,
154
+ completionTokens: data.usage.output_tokens,
155
+ totalTokens: data.usage.input_tokens + data.usage.output_tokens
156
+ } : void 0
157
+ };
158
+ }
159
+ };
160
+
161
+ // src/llm/ollama.ts
162
+ var OllamaAdapter = class extends BaseLLMAdapter {
163
+ baseUrl;
164
+ constructor(config) {
165
+ super(config);
166
+ this.baseUrl = config.endpoint || "http://localhost:11434";
167
+ }
168
+ /**
169
+ * Check if Ollama is running and the model is available
170
+ */
171
+ async healthCheck() {
172
+ try {
173
+ const response = await fetch(`${this.baseUrl}/api/tags`);
174
+ if (!response.ok) {
175
+ throw new Error("Ollama server not responding");
176
+ }
177
+ const data = await response.json();
178
+ const models = data.models?.map((m) => m.name) || [];
179
+ if (this.config.model && !models.some((m) => m.startsWith(this.config.model))) {
180
+ console.warn(
181
+ `Model "${this.config.model}" not found locally. Available: ${models.join(", ")}`
182
+ );
183
+ console.warn(`Run: ollama pull ${this.config.model}`);
184
+ }
185
+ } catch (error) {
186
+ throw new Error(
187
+ `Cannot connect to Ollama at ${this.baseUrl}. Make sure Ollama is running (ollama serve) or install it from https://ollama.com`
188
+ );
189
+ }
190
+ }
191
+ async chat(messages) {
192
+ const body = {
193
+ model: this.config.model || "mistral",
194
+ messages: messages.map((m) => ({
195
+ role: m.role,
196
+ content: m.content
197
+ })),
198
+ stream: false,
199
+ options: {
200
+ temperature: this.config.temperature,
201
+ num_predict: this.config.maxTokens
202
+ }
203
+ };
204
+ const response = await fetch(`${this.baseUrl}/api/chat`, {
205
+ method: "POST",
206
+ headers: {
207
+ "Content-Type": "application/json"
208
+ },
209
+ body: JSON.stringify(body)
210
+ });
211
+ if (!response.ok) {
212
+ const error = await response.text();
213
+ throw new Error(`Ollama API error: ${response.status} - ${error}`);
214
+ }
215
+ const data = await response.json();
216
+ return {
217
+ content: data.message?.content || "",
218
+ usage: data.eval_count ? {
219
+ promptTokens: data.prompt_eval_count || 0,
220
+ completionTokens: data.eval_count,
221
+ totalTokens: (data.prompt_eval_count || 0) + data.eval_count
222
+ } : void 0
223
+ };
224
+ }
225
+ getMetadata() {
226
+ return {
227
+ provider: "ollama",
228
+ model: this.config.model || "mistral",
229
+ isLocal: true
230
+ };
231
+ }
232
+ };
233
+
234
+ // src/llm/adapter.ts
235
+ async function createLLMAdapter(config) {
236
+ switch (config.provider) {
237
+ case "openai":
238
+ return new OpenAIAdapter(config);
239
+ case "anthropic":
240
+ return new AnthropicAdapter(config);
241
+ case "ollama":
242
+ const adapter = new OllamaAdapter(config);
243
+ await adapter.healthCheck();
244
+ return adapter;
245
+ case "custom":
246
+ return new OpenAIAdapter({
247
+ ...config,
248
+ endpoint: config.endpoint
249
+ });
250
+ default:
251
+ throw new Error(`Unsupported LLM provider: ${config.provider}`);
252
+ }
253
+ }
254
+
255
+ // src/strategy/loader.ts
256
+ var import_fs = require("fs");
257
+ var import_path = require("path");
258
+ var import_child_process = require("child_process");
259
+ async function loadStrategy(strategyPath) {
260
+ const basePath = strategyPath || process.env.EXAGENT_STRATEGY || "strategy";
261
+ const tsPath = basePath.endsWith(".ts") || basePath.endsWith(".js") ? basePath : `${basePath}.ts`;
262
+ const jsPath = basePath.endsWith(".ts") || basePath.endsWith(".js") ? basePath.replace(".ts", ".js") : `${basePath}.js`;
263
+ const fullTsPath = tsPath.startsWith("/") ? tsPath : (0, import_path.join)(process.cwd(), tsPath);
264
+ const fullJsPath = jsPath.startsWith("/") ? jsPath : (0, import_path.join)(process.cwd(), jsPath);
265
+ if ((0, import_fs.existsSync)(fullTsPath) && fullTsPath.endsWith(".ts")) {
266
+ try {
267
+ const module2 = await loadTypeScriptModule(fullTsPath);
268
+ if (typeof module2.generateSignals !== "function") {
269
+ throw new Error("Strategy must export a generateSignals function");
270
+ }
271
+ console.log(`Loaded custom strategy from ${tsPath}`);
272
+ return module2.generateSignals;
273
+ } catch (error) {
274
+ console.error(`Failed to load strategy from ${tsPath}:`, error);
275
+ throw error;
276
+ }
277
+ }
278
+ if ((0, import_fs.existsSync)(fullJsPath)) {
279
+ try {
280
+ const module2 = await import(fullJsPath);
281
+ if (typeof module2.generateSignals !== "function") {
282
+ throw new Error("Strategy must export a generateSignals function");
283
+ }
284
+ console.log(`Loaded custom strategy from ${jsPath}`);
285
+ return module2.generateSignals;
286
+ } catch (error) {
287
+ console.error(`Failed to load strategy from ${jsPath}:`, error);
288
+ throw error;
289
+ }
290
+ }
291
+ console.log("No custom strategy found, using default (hold) strategy");
292
+ return defaultStrategy;
293
+ }
294
+ async function loadTypeScriptModule(path2) {
295
+ try {
296
+ const tsxPath = require.resolve("tsx");
297
+ const { pathToFileURL } = await import("url");
298
+ const result = await new Promise((resolve, reject) => {
299
+ const child = (0, import_child_process.spawn)(
300
+ process.execPath,
301
+ [
302
+ "--import",
303
+ "tsx/esm",
304
+ "-e",
305
+ `import('${pathToFileURL(path2).href}').then(m => console.log(JSON.stringify({ exports: Object.keys(m) }))).catch(e => console.error('ERROR:', e.message))`
306
+ ],
307
+ {
308
+ cwd: process.cwd(),
309
+ env: process.env,
310
+ stdio: ["pipe", "pipe", "pipe"]
311
+ }
312
+ );
313
+ let stdout = "";
314
+ let stderr = "";
315
+ child.stdout.on("data", (data) => stdout += data.toString());
316
+ child.stderr.on("data", (data) => stderr += data.toString());
317
+ child.on("close", (code) => {
318
+ if (code !== 0 || stderr.includes("ERROR:")) {
319
+ reject(new Error(`Failed to load TypeScript: ${stderr || "Unknown error"}`));
320
+ } else {
321
+ resolve(stdout);
322
+ }
323
+ });
324
+ });
325
+ const tsx = await import("tsx/esm/api");
326
+ const unregister = tsx.register();
327
+ try {
328
+ const module2 = await import(path2);
329
+ return module2;
330
+ } finally {
331
+ unregister();
332
+ }
333
+ } catch (error) {
334
+ if (error.code === "MODULE_NOT_FOUND" || error.message.includes("Cannot find module")) {
335
+ throw new Error(
336
+ `Cannot load TypeScript strategy. Please either:
337
+ 1. Rename your strategy.ts to strategy.js (remove type annotations)
338
+ 2. Or compile it: npx tsc strategy.ts --outDir . --esModuleInterop
339
+ 3. Or install tsx: npm install tsx`
340
+ );
341
+ }
342
+ throw error;
343
+ }
344
+ }
345
+ var defaultStrategy = async (_marketData, _llm, _config) => {
346
+ return [];
347
+ };
348
+
349
+ // src/strategy/templates.ts
350
+ var STRATEGY_TEMPLATES = [
351
+ {
352
+ id: "momentum",
353
+ name: "Momentum Trader",
354
+ description: "Follows price trends and momentum indicators. Buys assets with strong upward momentum.",
355
+ riskLevel: "medium",
356
+ riskWarnings: [
357
+ "Momentum strategies can suffer significant losses during trend reversals",
358
+ "High volatility markets may generate false signals",
359
+ "Past performance does not guarantee future results",
360
+ "This strategy may underperform in sideways markets"
361
+ ],
362
+ systemPrompt: `You are an AI trading analyst specializing in momentum trading strategies.
363
+
364
+ Your role is to analyze market data and identify momentum-based trading opportunities.
365
+
366
+ IMPORTANT CONSTRAINTS:
367
+ - Only recommend trades when there is clear momentum evidence
368
+ - Always consider risk/reward ratios
369
+ - Never recommend more than the configured position size limits
370
+ - Be conservative with confidence scores
371
+
372
+ When analyzing data, look for:
373
+ 1. Price trends (higher highs, higher lows for uptrends)
374
+ 2. Volume confirmation (increasing volume on moves)
375
+ 3. Relative strength vs market benchmarks
376
+
377
+ Respond with JSON in this format:
378
+ {
379
+ "analysis": "Brief market analysis",
380
+ "signals": [
381
+ {
382
+ "action": "buy" | "sell" | "hold",
383
+ "tokenIn": "0x...",
384
+ "tokenOut": "0x...",
385
+ "percentage": 0-100,
386
+ "confidence": 0-1,
387
+ "reasoning": "Why this trade"
388
+ }
389
+ ]
390
+ }`,
391
+ exampleCode: `import { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig } from '@exagent/agent';
392
+
393
+ export const generateSignals: StrategyFunction = async (
394
+ marketData: MarketData,
395
+ llm: LLMAdapter,
396
+ config: AgentConfig
397
+ ): Promise<TradeSignal[]> => {
398
+ const response = await llm.chat([
399
+ { role: 'system', content: MOMENTUM_SYSTEM_PROMPT },
400
+ { role: 'user', content: JSON.stringify({
401
+ prices: marketData.prices,
402
+ balances: formatBalances(marketData.balances),
403
+ portfolioValue: marketData.portfolioValue,
404
+ })}
405
+ ]);
406
+
407
+ // Parse LLM response and convert to TradeSignals
408
+ const parsed = JSON.parse(response.content);
409
+ return parsed.signals.map(convertToTradeSignal);
410
+ };`
411
+ },
412
+ {
413
+ id: "value",
414
+ name: "Value Investor",
415
+ description: "Looks for undervalued assets based on fundamentals. Takes long-term positions.",
416
+ riskLevel: "low",
417
+ riskWarnings: [
418
+ "Value traps can result in prolonged losses",
419
+ "Requires patience - may underperform for extended periods",
420
+ "Fundamental analysis may not apply well to all crypto assets",
421
+ "Market sentiment can override fundamentals for long periods"
422
+ ],
423
+ systemPrompt: `You are an AI trading analyst specializing in value investing.
424
+
425
+ Your role is to identify undervalued assets with strong fundamentals.
426
+
427
+ IMPORTANT CONSTRAINTS:
428
+ - Focus on long-term value, not short-term price movements
429
+ - Only recommend assets with clear value propositions
430
+ - Consider protocol revenue, TVL, active users, developer activity
431
+ - Be very selective - quality over quantity
432
+
433
+ When analyzing, consider:
434
+ 1. Protocol fundamentals (revenue, TVL, user growth)
435
+ 2. Token economics (supply schedule, utility)
436
+ 3. Competitive positioning
437
+ 4. Valuation relative to peers
438
+
439
+ Respond with JSON in this format:
440
+ {
441
+ "analysis": "Brief fundamental analysis",
442
+ "signals": [
443
+ {
444
+ "action": "buy" | "sell" | "hold",
445
+ "tokenIn": "0x...",
446
+ "tokenOut": "0x...",
447
+ "percentage": 0-100,
448
+ "confidence": 0-1,
449
+ "reasoning": "Fundamental thesis"
450
+ }
451
+ ]
452
+ }`,
453
+ exampleCode: `import { StrategyFunction } from '@exagent/agent';
454
+
455
+ export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
456
+ // Value strategy runs less frequently
457
+ const response = await llm.chat([
458
+ { role: 'system', content: VALUE_SYSTEM_PROMPT },
459
+ { role: 'user', content: JSON.stringify(marketData) }
460
+ ]);
461
+
462
+ return parseSignals(response.content);
463
+ };`
464
+ },
465
+ {
466
+ id: "arbitrage",
467
+ name: "Arbitrage Hunter",
468
+ description: "Looks for price discrepancies across DEXs. Requires fast execution.",
469
+ riskLevel: "high",
470
+ riskWarnings: [
471
+ "Arbitrage opportunities are highly competitive - professional bots dominate",
472
+ "Slippage and gas costs can eliminate profits",
473
+ "MEV bots may front-run your transactions",
474
+ "Requires very fast execution and may not be profitable with standard infrastructure",
475
+ "This strategy is generally NOT recommended for beginners"
476
+ ],
477
+ systemPrompt: `You are an AI trading analyst specializing in arbitrage detection.
478
+
479
+ Your role is to identify price discrepancies that may offer arbitrage opportunities.
480
+
481
+ IMPORTANT CONSTRAINTS:
482
+ - Account for gas costs in all calculations
483
+ - Account for slippage (assume 0.3% minimum)
484
+ - Only flag opportunities with >1% net profit potential
485
+ - Consider MEV risk - assume some profit extraction
486
+
487
+ This is an advanced strategy with high competition.
488
+
489
+ Respond with JSON in this format:
490
+ {
491
+ "opportunities": [
492
+ {
493
+ "description": "What the arbitrage is",
494
+ "expectedProfit": "Net profit after costs",
495
+ "confidence": 0-1,
496
+ "warning": "Risks specific to this opportunity"
497
+ }
498
+ ]
499
+ }`,
500
+ exampleCode: `// Note: Pure arbitrage requires specialized infrastructure
501
+ // This template is for educational purposes
502
+
503
+ import { StrategyFunction } from '@exagent/agent';
504
+
505
+ export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
506
+ // Arbitrage requires real-time price feeds from multiple sources
507
+ // Standard LLM-based analysis is too slow for most arbitrage
508
+ console.warn('Arbitrage strategy requires specialized infrastructure');
509
+ return [];
510
+ };`
511
+ },
512
+ {
513
+ id: "custom",
514
+ name: "Custom Strategy",
515
+ description: "Build your own strategy from scratch. Full control over logic and prompts.",
516
+ riskLevel: "extreme",
517
+ riskWarnings: [
518
+ "Custom strategies have no guardrails - you are fully responsible",
519
+ "LLMs can hallucinate or make errors - always validate outputs",
520
+ "Test thoroughly on testnet before using real funds",
521
+ "Consider edge cases: what happens if the LLM returns invalid JSON?",
522
+ "Your prompts and strategy logic are your competitive advantage - protect them",
523
+ "Agents may not behave exactly as expected based on your prompts"
524
+ ],
525
+ systemPrompt: `// Define your own system prompt here
526
+
527
+ You are a trading AI. Analyze the market data and provide trading signals.
528
+
529
+ // Add your specific instructions, constraints, and output format.
530
+
531
+ Respond with JSON:
532
+ {
533
+ "signals": []
534
+ }`,
535
+ exampleCode: `import { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig } from '@exagent/agent';
536
+
537
+ /**
538
+ * Custom Strategy Template
539
+ *
540
+ * Customize this file with your own trading logic and prompts.
541
+ * Your prompts are YOUR intellectual property - we don't store them.
542
+ */
543
+ export const generateSignals: StrategyFunction = async (
544
+ marketData: MarketData,
545
+ llm: LLMAdapter,
546
+ config: AgentConfig
547
+ ): Promise<TradeSignal[]> => {
548
+ // Your custom system prompt (this is your secret sauce)
549
+ const systemPrompt = \`
550
+ Your custom instructions here...
551
+ \`;
552
+
553
+ // Call the LLM with your prompt
554
+ const response = await llm.chat([
555
+ { role: 'system', content: systemPrompt },
556
+ { role: 'user', content: JSON.stringify(marketData) }
557
+ ]);
558
+
559
+ // Parse and return signals
560
+ // IMPORTANT: Validate LLM output before using
561
+ try {
562
+ const parsed = JSON.parse(response.content);
563
+ return parsed.signals || [];
564
+ } catch (e) {
565
+ console.error('Failed to parse LLM response:', e);
566
+ return []; // Safe fallback: no trades
567
+ }
568
+ };`
569
+ }
570
+ ];
571
+ function getAllStrategyTemplates() {
572
+ return STRATEGY_TEMPLATES;
573
+ }
574
+
575
+ // src/trading/executor.ts
576
+ var TradeExecutor = class {
577
+ client;
578
+ config;
579
+ constructor(client, config) {
580
+ this.client = client;
581
+ this.config = config;
582
+ }
583
+ /**
584
+ * Execute a single trade signal
585
+ */
586
+ async execute(signal) {
587
+ if (signal.action === "hold") {
588
+ return { success: true };
589
+ }
590
+ try {
591
+ console.log(`Executing ${signal.action}: ${signal.tokenIn} -> ${signal.tokenOut}`);
592
+ console.log(`Amount: ${signal.amountIn.toString()}, Confidence: ${signal.confidence}`);
593
+ if (!this.validateSignal(signal)) {
594
+ return { success: false, error: "Signal exceeds position limits" };
595
+ }
596
+ const result = await this.client.trade({
597
+ tokenIn: signal.tokenIn,
598
+ tokenOut: signal.tokenOut,
599
+ amountIn: signal.amountIn,
600
+ maxSlippageBps: 100
601
+ // 1% default slippage
602
+ });
603
+ console.log(`Trade executed: ${result.hash}`);
604
+ return { success: true, txHash: result.hash };
605
+ } catch (error) {
606
+ const message = error instanceof Error ? error.message : "Unknown error";
607
+ console.error(`Trade failed: ${message}`);
608
+ return { success: false, error: message };
609
+ }
610
+ }
611
+ /**
612
+ * Execute multiple trade signals
613
+ * Returns results for each signal
614
+ */
615
+ async executeAll(signals) {
616
+ const results = [];
617
+ for (const signal of signals) {
618
+ const result = await this.execute(signal);
619
+ results.push({ signal, ...result });
620
+ if (signals.indexOf(signal) < signals.length - 1) {
621
+ await this.delay(1e3);
622
+ }
623
+ }
624
+ return results;
625
+ }
626
+ /**
627
+ * Validate a signal against config limits
628
+ */
629
+ validateSignal(signal) {
630
+ if (signal.confidence < 0.5) {
631
+ console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
632
+ return false;
633
+ }
634
+ return true;
635
+ }
636
+ delay(ms) {
637
+ return new Promise((resolve) => setTimeout(resolve, ms));
638
+ }
639
+ };
640
+
641
+ // src/trading/risk.ts
642
+ var RiskManager = class {
643
+ config;
644
+ dailyPnL = 0;
645
+ lastResetDate = "";
646
+ constructor(config) {
647
+ this.config = config;
648
+ }
649
+ /**
650
+ * Filter signals through risk checks
651
+ * Returns only signals that pass all guardrails
652
+ */
653
+ filterSignals(signals, marketData) {
654
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
655
+ if (today !== this.lastResetDate) {
656
+ this.dailyPnL = 0;
657
+ this.lastResetDate = today;
658
+ }
659
+ if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
660
+ console.warn("Daily loss limit reached - no new trades");
661
+ return [];
662
+ }
663
+ return signals.filter((signal) => this.validateSignal(signal, marketData));
664
+ }
665
+ /**
666
+ * Validate individual signal against risk limits
667
+ */
668
+ validateSignal(signal, marketData) {
669
+ if (signal.action === "hold") {
670
+ return true;
671
+ }
672
+ const signalValue = this.estimateSignalValue(signal, marketData);
673
+ const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
674
+ if (signalValue > maxPositionValue) {
675
+ console.warn(
676
+ `Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
677
+ );
678
+ return false;
679
+ }
680
+ if (signal.confidence < 0.5) {
681
+ console.warn(`Signal confidence too low: ${signal.confidence}`);
682
+ return false;
683
+ }
684
+ return true;
685
+ }
686
+ /**
687
+ * Check if daily loss limit has been hit
688
+ */
689
+ isDailyLossLimitHit(portfolioValue) {
690
+ const maxLoss = portfolioValue * this.config.maxDailyLossBps / 1e4;
691
+ return this.dailyPnL < -maxLoss;
692
+ }
693
+ /**
694
+ * Estimate USD value of a trade signal
695
+ */
696
+ estimateSignalValue(signal, marketData) {
697
+ const price = marketData.prices[signal.tokenIn] || 0;
698
+ const amount = Number(signal.amountIn) / 1e18;
699
+ return amount * price;
700
+ }
701
+ /**
702
+ * Update daily PnL after a trade
703
+ */
704
+ updatePnL(pnl) {
705
+ this.dailyPnL += pnl;
706
+ }
707
+ /**
708
+ * Get current risk status
709
+ */
710
+ getStatus() {
711
+ return {
712
+ dailyPnL: this.dailyPnL,
713
+ dailyLossLimit: this.config.maxDailyLossBps / 100,
714
+ // As percentage
715
+ isLimitHit: this.dailyPnL < -(this.config.maxDailyLossBps / 100)
716
+ };
717
+ }
718
+ };
719
+
720
+ // src/trading/market.ts
721
+ var MarketDataService = class {
722
+ rpcUrl;
723
+ constructor(rpcUrl) {
724
+ this.rpcUrl = rpcUrl;
725
+ }
726
+ /**
727
+ * Fetch current market data for the agent
728
+ */
729
+ async fetchMarketData(walletAddress, tokenAddresses) {
730
+ const prices = await this.fetchPrices(tokenAddresses);
731
+ const balances = await this.fetchBalances(walletAddress, tokenAddresses);
732
+ const portfolioValue = this.calculatePortfolioValue(balances, prices);
733
+ return {
734
+ timestamp: Date.now(),
735
+ prices,
736
+ balances,
737
+ portfolioValue
738
+ };
739
+ }
740
+ /**
741
+ * Fetch token prices from price oracle
742
+ */
743
+ async fetchPrices(tokenAddresses) {
744
+ const prices = {};
745
+ const knownPrices = {
746
+ // WETH
747
+ "0x4200000000000000000000000000000000000006": 3500,
748
+ // USDC
749
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913": 1,
750
+ // USDbC (bridged USDC)
751
+ "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA": 1
752
+ };
753
+ for (const address of tokenAddresses) {
754
+ prices[address.toLowerCase()] = knownPrices[address.toLowerCase()] || 0;
755
+ }
756
+ return prices;
757
+ }
758
+ /**
759
+ * Fetch token balances from chain
760
+ */
761
+ async fetchBalances(walletAddress, tokenAddresses) {
762
+ const balances = {};
763
+ for (const address of tokenAddresses) {
764
+ balances[address.toLowerCase()] = 0n;
765
+ }
766
+ return balances;
767
+ }
768
+ /**
769
+ * Calculate total portfolio value in USD
770
+ */
771
+ calculatePortfolioValue(balances, prices) {
772
+ let total = 0;
773
+ for (const [address, balance] of Object.entries(balances)) {
774
+ const price = prices[address.toLowerCase()] || 0;
775
+ const amount = Number(balance) / 1e18;
776
+ total += amount * price;
777
+ }
778
+ return total;
779
+ }
780
+ };
781
+
782
+ // src/vault/manager.ts
783
+ var import_viem = require("viem");
784
+ var import_accounts = require("viem/accounts");
785
+ var import_chains = require("viem/chains");
786
+ var ADDRESSES = {
787
+ testnet: {
788
+ vaultFactory: "0x5c099daaE33801a907Bb57011c6749655b55dc75",
789
+ registry: "0xCF48C341e3FebeCA5ECB7eb2535f61A2Ba855d9C",
790
+ usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
791
+ },
792
+ mainnet: {
793
+ vaultFactory: "0x0000000000000000000000000000000000000000",
794
+ // TODO: Deploy
795
+ registry: "0x0000000000000000000000000000000000000000",
796
+ usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
797
+ // Base mainnet USDC
798
+ }
799
+ };
800
+ var VAULT_FACTORY_ABI = [
801
+ {
802
+ type: "function",
803
+ name: "vaults",
804
+ inputs: [{ name: "agentId", type: "uint256" }, { name: "asset", type: "address" }],
805
+ outputs: [{ type: "address" }],
806
+ stateMutability: "view"
807
+ },
808
+ {
809
+ type: "function",
810
+ name: "canCreateVault",
811
+ inputs: [{ name: "creator", type: "address" }],
812
+ outputs: [{ name: "canCreate", type: "bool" }, { name: "reason", type: "string" }],
813
+ stateMutability: "view"
814
+ },
815
+ {
816
+ type: "function",
817
+ name: "createVault",
818
+ inputs: [
819
+ { name: "agentId", type: "uint256" },
820
+ { name: "asset", type: "address" },
821
+ { name: "name", type: "string" },
822
+ { name: "symbol", type: "string" },
823
+ { name: "feeRecipient", type: "address" }
824
+ ],
825
+ outputs: [{ type: "address" }],
826
+ stateMutability: "nonpayable"
827
+ },
828
+ {
829
+ type: "function",
830
+ name: "minimumVeEXARequired",
831
+ inputs: [],
832
+ outputs: [{ type: "uint256" }],
833
+ stateMutability: "view"
834
+ },
835
+ {
836
+ type: "function",
837
+ name: "eXABurnFee",
838
+ inputs: [],
839
+ outputs: [{ type: "uint256" }],
840
+ stateMutability: "view"
841
+ }
842
+ ];
843
+ var VAULT_ABI = [
844
+ {
845
+ type: "function",
846
+ name: "totalAssets",
847
+ inputs: [],
848
+ outputs: [{ type: "uint256" }],
849
+ stateMutability: "view"
850
+ },
851
+ {
852
+ type: "function",
853
+ name: "executeTrade",
854
+ inputs: [
855
+ { name: "tokenIn", type: "address" },
856
+ { name: "tokenOut", type: "address" },
857
+ { name: "amountIn", type: "uint256" },
858
+ { name: "minAmountOut", type: "uint256" },
859
+ { name: "aggregator", type: "address" },
860
+ { name: "swapData", type: "bytes" },
861
+ { name: "deadline", type: "uint256" }
862
+ ],
863
+ outputs: [{ type: "uint256" }],
864
+ stateMutability: "nonpayable"
865
+ }
866
+ ];
867
+ var VaultManager = class {
868
+ config;
869
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
870
+ publicClient;
871
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
872
+ walletClient;
873
+ addresses;
874
+ account;
875
+ chain;
876
+ cachedVaultAddress = null;
877
+ lastVaultCheck = 0;
878
+ VAULT_CACHE_TTL = 6e4;
879
+ // 1 minute
880
+ constructor(config) {
881
+ this.config = config;
882
+ this.addresses = ADDRESSES[config.network];
883
+ this.account = (0, import_accounts.privateKeyToAccount)(config.walletKey);
884
+ this.chain = config.network === "mainnet" ? import_chains.base : import_chains.baseSepolia;
885
+ const rpcUrl = config.network === "mainnet" ? "https://mainnet.base.org" : "https://sepolia.base.org";
886
+ this.publicClient = (0, import_viem.createPublicClient)({
887
+ chain: this.chain,
888
+ transport: (0, import_viem.http)(rpcUrl)
889
+ });
890
+ this.walletClient = (0, import_viem.createWalletClient)({
891
+ account: this.account,
892
+ chain: this.chain,
893
+ transport: (0, import_viem.http)(rpcUrl)
894
+ });
895
+ }
896
+ /**
897
+ * Get the agent's vault policy
898
+ */
899
+ get policy() {
900
+ return this.config.vaultConfig.policy;
901
+ }
902
+ /**
903
+ * Check if vault trading is preferred when a vault exists
904
+ */
905
+ get preferVaultTrading() {
906
+ return this.config.vaultConfig.preferVaultTrading;
907
+ }
908
+ /**
909
+ * Get comprehensive vault status
910
+ */
911
+ async getVaultStatus() {
912
+ const vaultAddress = await this.getVaultAddress();
913
+ const hasVault = vaultAddress !== null;
914
+ let totalAssets = BigInt(0);
915
+ if (hasVault && vaultAddress) {
916
+ try {
917
+ totalAssets = await this.publicClient.readContract({
918
+ address: vaultAddress,
919
+ abi: VAULT_ABI,
920
+ functionName: "totalAssets"
921
+ });
922
+ } catch {
923
+ }
924
+ }
925
+ const [canCreateResult, requirements] = await Promise.all([
926
+ this.publicClient.readContract({
927
+ address: this.addresses.vaultFactory,
928
+ abi: VAULT_FACTORY_ABI,
929
+ functionName: "canCreateVault",
930
+ args: [this.account.address]
931
+ }),
932
+ this.getRequirements()
933
+ ]);
934
+ return {
935
+ hasVault,
936
+ vaultAddress,
937
+ totalAssets,
938
+ canCreateVault: canCreateResult[0],
939
+ cannotCreateReason: canCreateResult[0] ? null : canCreateResult[1],
940
+ requirementsMet: canCreateResult[0] || requirements.isBypassed,
941
+ requirements
942
+ };
943
+ }
944
+ /**
945
+ * Get vault creation requirements
946
+ */
947
+ async getRequirements() {
948
+ const [veXARequired, burnFee] = await Promise.all([
949
+ this.publicClient.readContract({
950
+ address: this.addresses.vaultFactory,
951
+ abi: VAULT_FACTORY_ABI,
952
+ functionName: "minimumVeEXARequired"
953
+ }),
954
+ this.publicClient.readContract({
955
+ address: this.addresses.vaultFactory,
956
+ abi: VAULT_FACTORY_ABI,
957
+ functionName: "eXABurnFee"
958
+ })
959
+ ]);
960
+ const isBypassed = veXARequired === BigInt(0) && burnFee === BigInt(0);
961
+ return { veXARequired, burnFee, isBypassed };
962
+ }
963
+ /**
964
+ * Get the agent's vault address (cached)
965
+ */
966
+ async getVaultAddress() {
967
+ const now = Date.now();
968
+ if (this.cachedVaultAddress && now - this.lastVaultCheck < this.VAULT_CACHE_TTL) {
969
+ return this.cachedVaultAddress;
970
+ }
971
+ const vaultAddress = await this.publicClient.readContract({
972
+ address: this.addresses.vaultFactory,
973
+ abi: VAULT_FACTORY_ABI,
974
+ functionName: "vaults",
975
+ args: [this.config.agentId, this.addresses.usdc]
976
+ });
977
+ this.lastVaultCheck = now;
978
+ if (vaultAddress === "0x0000000000000000000000000000000000000000") {
979
+ this.cachedVaultAddress = null;
980
+ return null;
981
+ }
982
+ this.cachedVaultAddress = vaultAddress;
983
+ return vaultAddress;
984
+ }
985
+ /**
986
+ * Check if the agent should create a vault based on policy and qualification
987
+ */
988
+ async shouldCreateVault() {
989
+ if (this.policy === "disabled") {
990
+ return { should: false, reason: "Vault creation disabled by policy" };
991
+ }
992
+ if (this.policy === "manual") {
993
+ return { should: false, reason: "Vault creation set to manual - waiting for owner instruction" };
994
+ }
995
+ const status = await this.getVaultStatus();
996
+ if (status.hasVault) {
997
+ return { should: false, reason: "Vault already exists" };
998
+ }
999
+ if (!status.canCreateVault) {
1000
+ return { should: false, reason: status.cannotCreateReason || "Requirements not met" };
1001
+ }
1002
+ return { should: true, reason: "Agent is qualified and auto-creation is enabled" };
1003
+ }
1004
+ /**
1005
+ * Create a vault for the agent
1006
+ * @returns Vault address if successful
1007
+ */
1008
+ async createVault() {
1009
+ if (this.policy === "disabled") {
1010
+ return { success: false, error: "Vault creation disabled by policy" };
1011
+ }
1012
+ const existingVault = await this.getVaultAddress();
1013
+ if (existingVault) {
1014
+ return { success: false, error: "Vault already exists", vaultAddress: existingVault };
1015
+ }
1016
+ const status = await this.getVaultStatus();
1017
+ if (!status.canCreateVault) {
1018
+ return { success: false, error: status.cannotCreateReason || "Requirements not met" };
1019
+ }
1020
+ const vaultName = this.config.vaultConfig.defaultName || `${this.config.agentName} Trading Vault`;
1021
+ const vaultSymbol = this.config.vaultConfig.defaultSymbol || `ex${this.config.agentName.replace(/[^a-zA-Z]/g, "").slice(0, 4).toUpperCase()}`;
1022
+ const feeRecipient = this.config.vaultConfig.feeRecipient || this.account.address;
1023
+ try {
1024
+ const hash = await this.walletClient.writeContract({
1025
+ address: this.addresses.vaultFactory,
1026
+ abi: VAULT_FACTORY_ABI,
1027
+ functionName: "createVault",
1028
+ args: [
1029
+ this.config.agentId,
1030
+ this.addresses.usdc,
1031
+ vaultName,
1032
+ vaultSymbol,
1033
+ feeRecipient
1034
+ ],
1035
+ chain: this.chain,
1036
+ account: this.account
1037
+ });
1038
+ const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
1039
+ if (receipt.status !== "success") {
1040
+ return { success: false, error: "Transaction failed", txHash: hash };
1041
+ }
1042
+ const vaultAddress = await this.getVaultAddress();
1043
+ this.cachedVaultAddress = vaultAddress;
1044
+ return {
1045
+ success: true,
1046
+ vaultAddress,
1047
+ txHash: hash
1048
+ };
1049
+ } catch (error) {
1050
+ return {
1051
+ success: false,
1052
+ error: error instanceof Error ? error.message : "Unknown error"
1053
+ };
1054
+ }
1055
+ }
1056
+ /**
1057
+ * Execute a trade through the vault (if it exists and policy allows)
1058
+ * Returns null if should use direct trading instead
1059
+ */
1060
+ async executeVaultTrade(params) {
1061
+ if (!this.preferVaultTrading) {
1062
+ return null;
1063
+ }
1064
+ const vaultAddress = await this.getVaultAddress();
1065
+ if (!vaultAddress) {
1066
+ return null;
1067
+ }
1068
+ const deadline = params.deadline || BigInt(Math.floor(Date.now() / 1e3) + 3600);
1069
+ try {
1070
+ const hash = await this.walletClient.writeContract({
1071
+ address: vaultAddress,
1072
+ abi: VAULT_ABI,
1073
+ functionName: "executeTrade",
1074
+ args: [
1075
+ params.tokenIn,
1076
+ params.tokenOut,
1077
+ params.amountIn,
1078
+ params.minAmountOut,
1079
+ params.aggregator,
1080
+ params.swapData,
1081
+ deadline
1082
+ ],
1083
+ chain: this.chain,
1084
+ account: this.account
1085
+ });
1086
+ return { usedVault: true, txHash: hash };
1087
+ } catch (error) {
1088
+ return {
1089
+ usedVault: true,
1090
+ error: error instanceof Error ? error.message : "Vault trade failed"
1091
+ };
1092
+ }
1093
+ }
1094
+ /**
1095
+ * Run the auto-creation check (call this periodically in the agent loop)
1096
+ * Only creates vault if policy is 'auto_when_qualified'
1097
+ */
1098
+ async checkAndAutoCreateVault() {
1099
+ const shouldCreate = await this.shouldCreateVault();
1100
+ if (!shouldCreate.should) {
1101
+ const status = await this.getVaultStatus();
1102
+ if (status.hasVault) {
1103
+ return { action: "already_exists", vaultAddress: status.vaultAddress, reason: "Vault already exists" };
1104
+ }
1105
+ if (this.policy !== "auto_when_qualified") {
1106
+ return { action: "skipped", reason: shouldCreate.reason };
1107
+ }
1108
+ return { action: "not_qualified", reason: shouldCreate.reason };
1109
+ }
1110
+ const result = await this.createVault();
1111
+ if (result.success) {
1112
+ return {
1113
+ action: "created",
1114
+ vaultAddress: result.vaultAddress,
1115
+ reason: "Vault created automatically"
1116
+ };
1117
+ }
1118
+ return { action: "not_qualified", reason: result.error || "Creation failed" };
1119
+ }
1120
+ };
1121
+
1122
+ // src/runtime.ts
1123
+ var AgentRuntime = class {
1124
+ config;
1125
+ client;
1126
+ llm;
1127
+ strategy;
1128
+ executor;
1129
+ riskManager;
1130
+ marketData;
1131
+ vaultManager;
1132
+ isRunning = false;
1133
+ configHash;
1134
+ lastVaultCheck = 0;
1135
+ VAULT_CHECK_INTERVAL = 3e5;
1136
+ // Check vault status every 5 minutes
1137
+ constructor(config) {
1138
+ this.config = config;
1139
+ }
1140
+ /**
1141
+ * Initialize the agent runtime
1142
+ */
1143
+ async initialize() {
1144
+ console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
1145
+ this.client = new import_sdk.ExagentClient({
1146
+ privateKey: this.config.privateKey,
1147
+ network: this.config.network
1148
+ });
1149
+ console.log(`Wallet: ${this.client.address}`);
1150
+ const agent = await this.client.registry.getAgent(BigInt(this.config.agentId));
1151
+ if (!agent) {
1152
+ throw new Error(`Agent ID ${this.config.agentId} not found on-chain. Please register first.`);
1153
+ }
1154
+ console.log(`Agent verified: ${agent.name}`);
1155
+ await this.ensureWalletLinked();
1156
+ console.log(`Initializing LLM: ${this.config.llm.provider}`);
1157
+ this.llm = await createLLMAdapter(this.config.llm);
1158
+ const llmMeta = this.llm.getMetadata();
1159
+ console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
1160
+ await this.syncConfigHash();
1161
+ this.strategy = await loadStrategy();
1162
+ this.executor = new TradeExecutor(this.client, this.config);
1163
+ this.riskManager = new RiskManager(this.config.trading);
1164
+ this.marketData = new MarketDataService(this.getRpcUrl());
1165
+ await this.initializeVaultManager();
1166
+ console.log("Agent initialized successfully");
1167
+ }
1168
+ /**
1169
+ * Initialize the vault manager based on config
1170
+ */
1171
+ async initializeVaultManager() {
1172
+ const vaultConfig = this.config.vault || { policy: "disabled", preferVaultTrading: false };
1173
+ this.vaultManager = new VaultManager({
1174
+ agentId: BigInt(this.config.agentId),
1175
+ agentName: this.config.name,
1176
+ network: this.config.network,
1177
+ walletKey: this.config.privateKey,
1178
+ vaultConfig
1179
+ });
1180
+ console.log(`Vault policy: ${vaultConfig.policy}`);
1181
+ const status = await this.vaultManager.getVaultStatus();
1182
+ if (status.hasVault) {
1183
+ console.log(`Vault exists: ${status.vaultAddress}`);
1184
+ console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
1185
+ } else {
1186
+ console.log("No vault exists for this agent");
1187
+ if (vaultConfig.policy === "auto_when_qualified") {
1188
+ if (status.canCreateVault) {
1189
+ console.log("Agent is qualified to create vault - will attempt on next check");
1190
+ } else {
1191
+ console.log(`Cannot create vault yet: ${status.cannotCreateReason}`);
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+ /**
1197
+ * Ensure the current wallet is linked to the agent
1198
+ */
1199
+ async ensureWalletLinked() {
1200
+ const agentId = BigInt(this.config.agentId);
1201
+ const address = this.client.address;
1202
+ const isLinked = await this.client.registry.isLinkedWallet(agentId, address);
1203
+ if (!isLinked) {
1204
+ console.log("Wallet not linked, linking now...");
1205
+ const agent = await this.client.registry.getAgent(agentId);
1206
+ if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
1207
+ throw new Error(
1208
+ `Cannot link wallet: ${address} is not the owner of agent ${this.config.agentId}. Owner is ${agent?.owner}`
1209
+ );
1210
+ }
1211
+ await this.client.registry.linkOwnWallet(agentId);
1212
+ console.log("Wallet linked successfully");
1213
+ } else {
1214
+ console.log("Wallet already linked");
1215
+ }
1216
+ }
1217
+ /**
1218
+ * Sync the LLM config hash to chain for epoch tracking
1219
+ * This ensures trades are attributed to the correct config epoch
1220
+ */
1221
+ async syncConfigHash() {
1222
+ const agentId = BigInt(this.config.agentId);
1223
+ const llmMeta = this.llm.getMetadata();
1224
+ this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
1225
+ console.log(`Config hash: ${this.configHash}`);
1226
+ const onChainHash = await this.client.registry.getConfigHash(agentId);
1227
+ if (onChainHash !== this.configHash) {
1228
+ console.log("Config changed, updating on-chain...");
1229
+ await this.client.registry.updateConfig(agentId, this.configHash);
1230
+ const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
1231
+ console.log(`Config updated, new epoch started: ${newEpoch}`);
1232
+ } else {
1233
+ const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
1234
+ console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
1235
+ }
1236
+ }
1237
+ /**
1238
+ * Get the current config hash (for trade execution)
1239
+ */
1240
+ getConfigHash() {
1241
+ return this.configHash;
1242
+ }
1243
+ /**
1244
+ * Start the trading loop
1245
+ */
1246
+ async run() {
1247
+ if (this.isRunning) {
1248
+ throw new Error("Agent is already running");
1249
+ }
1250
+ this.isRunning = true;
1251
+ console.log("Starting trading loop...");
1252
+ console.log(`Interval: ${this.config.trading.tradingIntervalMs}ms`);
1253
+ while (this.isRunning) {
1254
+ try {
1255
+ await this.runCycle();
1256
+ } catch (error) {
1257
+ console.error("Error in trading cycle:", error);
1258
+ }
1259
+ await this.sleep(this.config.trading.tradingIntervalMs);
1260
+ }
1261
+ }
1262
+ /**
1263
+ * Run a single trading cycle
1264
+ */
1265
+ async runCycle() {
1266
+ console.log(`
1267
+ --- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
1268
+ await this.checkVaultAutoCreation();
1269
+ const tokens = this.config.allowedTokens || this.getDefaultTokens();
1270
+ const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
1271
+ console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
1272
+ const signals = await this.strategy(marketData, this.llm, this.config);
1273
+ console.log(`Strategy generated ${signals.length} signals`);
1274
+ const filteredSignals = this.riskManager.filterSignals(signals, marketData);
1275
+ console.log(`${filteredSignals.length} signals passed risk checks`);
1276
+ if (filteredSignals.length > 0) {
1277
+ const results = await this.executor.executeAll(filteredSignals);
1278
+ for (const result of results) {
1279
+ if (result.success) {
1280
+ console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
1281
+ } else {
1282
+ console.warn(`Trade failed: ${result.error}`);
1283
+ }
1284
+ }
1285
+ }
1286
+ }
1287
+ /**
1288
+ * Check for vault auto-creation based on policy
1289
+ */
1290
+ async checkVaultAutoCreation() {
1291
+ const now = Date.now();
1292
+ if (now - this.lastVaultCheck < this.VAULT_CHECK_INTERVAL) {
1293
+ return;
1294
+ }
1295
+ this.lastVaultCheck = now;
1296
+ const result = await this.vaultManager.checkAndAutoCreateVault();
1297
+ switch (result.action) {
1298
+ case "created":
1299
+ console.log(`\u{1F389} Vault created automatically: ${result.vaultAddress}`);
1300
+ break;
1301
+ case "already_exists":
1302
+ break;
1303
+ case "skipped":
1304
+ break;
1305
+ case "not_qualified":
1306
+ console.log(`Vault auto-creation pending: ${result.reason}`);
1307
+ break;
1308
+ }
1309
+ }
1310
+ /**
1311
+ * Stop the trading loop
1312
+ */
1313
+ stop() {
1314
+ console.log("Stopping agent...");
1315
+ this.isRunning = false;
1316
+ }
1317
+ /**
1318
+ * Get RPC URL based on network
1319
+ */
1320
+ getRpcUrl() {
1321
+ if (this.config.network === "mainnet") {
1322
+ return "https://mainnet.base.org";
1323
+ }
1324
+ return "https://sepolia.base.org";
1325
+ }
1326
+ /**
1327
+ * Default tokens to track
1328
+ */
1329
+ getDefaultTokens() {
1330
+ if (this.config.network === "mainnet") {
1331
+ return [
1332
+ "0x4200000000000000000000000000000000000006",
1333
+ // WETH
1334
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
1335
+ // USDC
1336
+ ];
1337
+ }
1338
+ return [
1339
+ "0x4200000000000000000000000000000000000006"
1340
+ // WETH
1341
+ ];
1342
+ }
1343
+ sleep(ms) {
1344
+ return new Promise((resolve) => setTimeout(resolve, ms));
1345
+ }
1346
+ /**
1347
+ * Get current status
1348
+ */
1349
+ getStatus() {
1350
+ const vaultConfig = this.config.vault || { policy: "disabled" };
1351
+ return {
1352
+ isRunning: this.isRunning,
1353
+ agentId: Number(this.config.agentId),
1354
+ wallet: this.client?.address || "not initialized",
1355
+ llm: {
1356
+ provider: this.config.llm.provider,
1357
+ model: this.config.llm.model || "default"
1358
+ },
1359
+ configHash: this.configHash || "not initialized",
1360
+ risk: this.riskManager?.getStatus() || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false },
1361
+ vault: {
1362
+ policy: vaultConfig.policy,
1363
+ hasVault: false,
1364
+ // Updated async via getVaultStatus
1365
+ vaultAddress: null
1366
+ }
1367
+ };
1368
+ }
1369
+ /**
1370
+ * Get detailed vault status (async)
1371
+ */
1372
+ async getVaultStatus() {
1373
+ if (!this.vaultManager) {
1374
+ return null;
1375
+ }
1376
+ return this.vaultManager.getVaultStatus();
1377
+ }
1378
+ /**
1379
+ * Manually trigger vault creation (for 'manual' policy)
1380
+ */
1381
+ async createVault() {
1382
+ if (!this.vaultManager) {
1383
+ return { success: false, error: "Vault manager not initialized" };
1384
+ }
1385
+ const policy = this.config.vault?.policy || "disabled";
1386
+ if (policy === "disabled") {
1387
+ return { success: false, error: "Vault creation is disabled by policy" };
1388
+ }
1389
+ return this.vaultManager.createVault();
1390
+ }
1391
+ };
1392
+
1393
+ // src/config.ts
1394
+ var import_fs2 = require("fs");
1395
+ var import_path2 = require("path");
1396
+ var import_dotenv = require("dotenv");
1397
+
1398
+ // src/types.ts
1399
+ var import_zod = require("zod");
1400
+ var WalletSetupSchema = import_zod.z.enum(["generate", "provide"]);
1401
+ var LLMProviderSchema = import_zod.z.enum(["openai", "anthropic", "google", "deepseek", "mistral", "groq", "together", "ollama", "custom"]);
1402
+ var LLMConfigSchema = import_zod.z.object({
1403
+ provider: LLMProviderSchema,
1404
+ model: import_zod.z.string().optional(),
1405
+ apiKey: import_zod.z.string().optional(),
1406
+ endpoint: import_zod.z.string().url().optional(),
1407
+ temperature: import_zod.z.number().min(0).max(2).default(0.7),
1408
+ maxTokens: import_zod.z.number().positive().default(4096)
1409
+ });
1410
+ var RiskUniverseSchema = import_zod.z.enum(["core", "established", "derivatives", "emerging", "frontier"]);
1411
+ var TradingConfigSchema = import_zod.z.object({
1412
+ timeHorizon: import_zod.z.enum(["intraday", "swing", "position"]).default("swing"),
1413
+ maxPositionSizeBps: import_zod.z.number().min(100).max(1e4).default(1e3),
1414
+ // 1-100%
1415
+ maxDailyLossBps: import_zod.z.number().min(0).max(1e4).default(500),
1416
+ // 0-100%
1417
+ maxConcurrentPositions: import_zod.z.number().min(1).max(100).default(5),
1418
+ tradingIntervalMs: import_zod.z.number().min(1e3).default(6e4)
1419
+ // minimum 1 second
1420
+ });
1421
+ var VaultPolicySchema = import_zod.z.enum([
1422
+ "disabled",
1423
+ // Never create a vault - trade with agent's own capital only
1424
+ "manual",
1425
+ // Only create vault when explicitly directed by owner
1426
+ "auto_when_qualified"
1427
+ // Automatically create vault when requirements are met
1428
+ ]);
1429
+ var VaultConfigSchema = import_zod.z.object({
1430
+ // Policy for vault creation (asked during deployment)
1431
+ policy: VaultPolicySchema.default("manual"),
1432
+ // Default vault name (auto-generated from agent name if not set)
1433
+ defaultName: import_zod.z.string().optional(),
1434
+ // Default vault symbol (auto-generated if not set)
1435
+ defaultSymbol: import_zod.z.string().optional(),
1436
+ // Fee recipient for vault fees (default: agent wallet)
1437
+ feeRecipient: import_zod.z.string().optional(),
1438
+ // When vault exists, trade through vault instead of direct trading
1439
+ // This pools depositors' capital with the agent's trades
1440
+ preferVaultTrading: import_zod.z.boolean().default(true)
1441
+ });
1442
+ var WalletConfigSchema = import_zod.z.object({
1443
+ setup: WalletSetupSchema.default("provide")
1444
+ }).optional();
1445
+ var AgentConfigSchema = import_zod.z.object({
1446
+ // Identity (from on-chain registration)
1447
+ agentId: import_zod.z.union([import_zod.z.number().positive(), import_zod.z.string()]),
1448
+ name: import_zod.z.string().min(3).max(32),
1449
+ // Network
1450
+ network: import_zod.z.enum(["mainnet", "testnet"]).default("testnet"),
1451
+ // Wallet setup preference
1452
+ wallet: WalletConfigSchema,
1453
+ // LLM
1454
+ llm: LLMConfigSchema,
1455
+ // Trading parameters
1456
+ riskUniverse: RiskUniverseSchema.default("established"),
1457
+ trading: TradingConfigSchema.default({}),
1458
+ // Vault configuration (copy trading)
1459
+ vault: VaultConfigSchema.default({}),
1460
+ // Allowed tokens (addresses)
1461
+ allowedTokens: import_zod.z.array(import_zod.z.string()).optional()
1462
+ });
1463
+
1464
+ // src/config.ts
1465
+ function loadConfig(configPath) {
1466
+ (0, import_dotenv.config)();
1467
+ const configFile = configPath || process.env.EXAGENT_CONFIG || "agent-config.json";
1468
+ const fullPath = configFile.startsWith("/") ? configFile : (0, import_path2.join)(process.cwd(), configFile);
1469
+ if (!(0, import_fs2.existsSync)(fullPath)) {
1470
+ throw new Error(`Config file not found: ${fullPath}`);
1471
+ }
1472
+ const rawConfig = JSON.parse((0, import_fs2.readFileSync)(fullPath, "utf-8"));
1473
+ const config = AgentConfigSchema.parse(rawConfig);
1474
+ const privateKey = process.env.EXAGENT_PRIVATE_KEY;
1475
+ if (!privateKey) {
1476
+ throw new Error("EXAGENT_PRIVATE_KEY not set in environment");
1477
+ }
1478
+ if (!privateKey.startsWith("0x") || privateKey.length !== 66) {
1479
+ throw new Error("EXAGENT_PRIVATE_KEY must be a valid 32-byte hex string starting with 0x");
1480
+ }
1481
+ const llmConfig = { ...config.llm };
1482
+ if (process.env.OPENAI_API_KEY && config.llm.provider === "openai") {
1483
+ llmConfig.apiKey = process.env.OPENAI_API_KEY;
1484
+ }
1485
+ if (process.env.ANTHROPIC_API_KEY && config.llm.provider === "anthropic") {
1486
+ llmConfig.apiKey = process.env.ANTHROPIC_API_KEY;
1487
+ }
1488
+ if (process.env.EXAGENT_LLM_URL) {
1489
+ llmConfig.endpoint = process.env.EXAGENT_LLM_URL;
1490
+ }
1491
+ if (process.env.EXAGENT_LLM_MODEL) {
1492
+ llmConfig.model = process.env.EXAGENT_LLM_MODEL;
1493
+ }
1494
+ const network = process.env.EXAGENT_NETWORK || config.network;
1495
+ return {
1496
+ ...config,
1497
+ llm: llmConfig,
1498
+ network,
1499
+ privateKey
1500
+ };
1501
+ }
1502
+ function validateConfig(config) {
1503
+ if (!config.privateKey) {
1504
+ throw new Error("Private key is required");
1505
+ }
1506
+ if (config.llm.provider !== "ollama" && !config.llm.apiKey) {
1507
+ throw new Error(`API key required for ${config.llm.provider} provider`);
1508
+ }
1509
+ if (config.llm.provider === "ollama" && !config.llm.endpoint) {
1510
+ config.llm.endpoint = "http://localhost:11434";
1511
+ }
1512
+ if (config.llm.provider === "custom" && !config.llm.endpoint) {
1513
+ throw new Error("Endpoint required for custom LLM provider");
1514
+ }
1515
+ if (!config.agentId || Number(config.agentId) <= 0) {
1516
+ throw new Error("Valid agent ID required");
1517
+ }
1518
+ }
1519
+
1520
+ // src/cli.ts
1521
+ var import_accounts2 = require("viem/accounts");
1522
+ (0, import_dotenv2.config)();
1523
+ var program = new import_commander.Command();
1524
+ program.name("exagent").description("Exagent autonomous trading agent").version("0.1.0");
1525
+ function prompt(question, hidden = false) {
1526
+ const rl = readline.createInterface({
1527
+ input: process.stdin,
1528
+ output: process.stdout
1529
+ });
1530
+ return new Promise((resolve) => {
1531
+ if (hidden) {
1532
+ process.stdout.write(question);
1533
+ let input = "";
1534
+ process.stdin.setRawMode(true);
1535
+ process.stdin.resume();
1536
+ process.stdin.setEncoding("utf8");
1537
+ const onData = (char) => {
1538
+ if (char === "\n" || char === "\r") {
1539
+ process.stdin.setRawMode(false);
1540
+ process.stdin.pause();
1541
+ process.stdin.removeListener("data", onData);
1542
+ console.log("");
1543
+ rl.close();
1544
+ resolve(input);
1545
+ } else if (char === "") {
1546
+ process.exit(0);
1547
+ } else if (char === "\x7F") {
1548
+ if (input.length > 0) {
1549
+ input = input.slice(0, -1);
1550
+ }
1551
+ } else {
1552
+ input += char;
1553
+ process.stdout.write("*");
1554
+ }
1555
+ };
1556
+ process.stdin.on("data", onData);
1557
+ } else {
1558
+ rl.question(question, (answer) => {
1559
+ rl.close();
1560
+ resolve(answer);
1561
+ });
1562
+ }
1563
+ });
1564
+ }
1565
+ async function checkFirstRunSetup(configPath) {
1566
+ const envPath = path.join(path.dirname(configPath), ".env");
1567
+ if (fs.existsSync(envPath)) {
1568
+ const envContent2 = fs.readFileSync(envPath, "utf-8");
1569
+ const hasPrivateKey = envContent2.includes("EXAGENT_PRIVATE_KEY=") && !envContent2.includes("EXAGENT_PRIVATE_KEY=\n") && !envContent2.includes("EXAGENT_PRIVATE_KEY=$");
1570
+ if (hasPrivateKey) {
1571
+ return;
1572
+ }
1573
+ }
1574
+ const config = loadConfig(configPath);
1575
+ const walletSetup = config.wallet?.setup || "provide";
1576
+ console.log("");
1577
+ console.log("=".repeat(60));
1578
+ console.log(" EXAGENT FIRST-RUN SETUP");
1579
+ console.log("=".repeat(60));
1580
+ console.log("");
1581
+ console.log(" This appears to be your first time running this agent.");
1582
+ console.log(" Let's set up your wallet and API keys.");
1583
+ console.log("");
1584
+ console.log(" Your secrets will be stored locally in .env");
1585
+ console.log(" They are NEVER sent to Exagent servers.");
1586
+ console.log("");
1587
+ console.log("=".repeat(60));
1588
+ console.log("");
1589
+ let privateKey;
1590
+ let walletAddress;
1591
+ if (walletSetup === "generate") {
1592
+ console.log("[WALLET] Generating a new wallet for your agent...");
1593
+ console.log("");
1594
+ const generatedKey = (0, import_accounts2.generatePrivateKey)();
1595
+ const account = (0, import_accounts2.privateKeyToAccount)(generatedKey);
1596
+ privateKey = generatedKey;
1597
+ walletAddress = account.address;
1598
+ console.log(" New wallet created!");
1599
+ console.log("");
1600
+ console.log(" +" + "-".repeat(58) + "+");
1601
+ console.log(" | WALLET ADDRESS (fund this to start trading): |");
1602
+ console.log(" | " + walletAddress.padEnd(56) + " |");
1603
+ console.log(" +" + "-".repeat(58) + "+");
1604
+ console.log("");
1605
+ console.log(" IMPORTANT: Your private key has been saved to .env");
1606
+ console.log(" Back up this file securely. If lost, your funds are gone.");
1607
+ console.log("");
1608
+ const confirmed = await prompt(" Press Enter to continue, or Ctrl+C to cancel...");
1609
+ } else {
1610
+ console.log("[WALLET] Please enter your trading wallet private key.");
1611
+ console.log("");
1612
+ console.log(" Your input will be hidden for security.");
1613
+ console.log(" The key should start with 0x and be 66 characters total.");
1614
+ console.log("");
1615
+ privateKey = await prompt(" Private key: ", true);
1616
+ if (!privateKey.startsWith("0x")) {
1617
+ privateKey = "0x" + privateKey;
1618
+ }
1619
+ if (privateKey.length !== 66) {
1620
+ console.error("");
1621
+ console.error(" ERROR: Invalid private key length. Expected 66 characters (with 0x prefix).");
1622
+ process.exit(1);
1623
+ }
1624
+ try {
1625
+ const account = (0, import_accounts2.privateKeyToAccount)(privateKey);
1626
+ walletAddress = account.address;
1627
+ console.log("");
1628
+ console.log(` Wallet address: ${walletAddress}`);
1629
+ } catch (error) {
1630
+ console.error("");
1631
+ console.error(" ERROR: Invalid private key format.");
1632
+ process.exit(1);
1633
+ }
1634
+ }
1635
+ console.log("");
1636
+ const llmProvider = config.llm?.provider || "openai";
1637
+ let llmApiKey = "";
1638
+ let llmEnvVar = "";
1639
+ if (llmProvider === "ollama") {
1640
+ console.log("[LLM] Using Ollama (local). No API key needed.");
1641
+ console.log(" Make sure Ollama is running: ollama serve");
1642
+ console.log("");
1643
+ } else if (llmProvider === "custom") {
1644
+ console.log("[LLM] Using custom OpenAI-compatible endpoint.");
1645
+ console.log("");
1646
+ const llmUrl = await prompt(" Enter your API endpoint URL: ");
1647
+ llmApiKey = await prompt(" Enter your API key (or press Enter if none): ", true);
1648
+ llmEnvVar = `EXAGENT_LLM_URL=${llmUrl}
1649
+ `;
1650
+ if (llmApiKey) {
1651
+ llmEnvVar += `EXAGENT_LLM_API_KEY=${llmApiKey}
1652
+ `;
1653
+ }
1654
+ } else {
1655
+ const providerNames = {
1656
+ openai: { name: "OpenAI", envVar: "OPENAI_API_KEY", url: "https://platform.openai.com/api-keys" },
1657
+ anthropic: { name: "Anthropic", envVar: "ANTHROPIC_API_KEY", url: "https://console.anthropic.com/settings/keys" },
1658
+ google: { name: "Google AI", envVar: "GOOGLE_AI_API_KEY", url: "https://aistudio.google.com/apikey" },
1659
+ deepseek: { name: "DeepSeek", envVar: "DEEPSEEK_API_KEY", url: "https://platform.deepseek.com/api_keys" },
1660
+ mistral: { name: "Mistral AI", envVar: "MISTRAL_API_KEY", url: "https://console.mistral.ai/api-keys/" },
1661
+ groq: { name: "Groq", envVar: "GROQ_API_KEY", url: "https://console.groq.com/keys" },
1662
+ together: { name: "Together AI", envVar: "TOGETHER_API_KEY", url: "https://api.together.xyz/settings/api-keys" }
1663
+ };
1664
+ const provider = providerNames[llmProvider] || providerNames.openai;
1665
+ console.log(`[LLM] Enter your ${provider.name} API key.`);
1666
+ console.log(` Get one at: ${provider.url}`);
1667
+ console.log("");
1668
+ llmApiKey = await prompt(" API key: ", true);
1669
+ if (!llmApiKey) {
1670
+ console.error("");
1671
+ console.error(" ERROR: API key is required.");
1672
+ process.exit(1);
1673
+ }
1674
+ llmEnvVar = `${provider.envVar}=${llmApiKey}
1675
+ `;
1676
+ }
1677
+ const envContent = `# Exagent Agent Configuration
1678
+ # Agent: ${config.name}
1679
+ # Agent ID: ${config.agentId}
1680
+ # Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
1681
+ # WARNING: Never commit this file to version control!
1682
+
1683
+ # Wallet
1684
+ EXAGENT_PRIVATE_KEY=${privateKey}
1685
+
1686
+ # Network
1687
+ EXAGENT_NETWORK=${config.network || "testnet"}
1688
+
1689
+ # LLM (${llmProvider})
1690
+ ${llmEnvVar}EXAGENT_LLM_MODEL=${config.llm?.model || ""}
1691
+ `;
1692
+ fs.writeFileSync(envPath, envContent, { mode: 384 });
1693
+ console.log("");
1694
+ console.log("=".repeat(60));
1695
+ console.log(" SETUP COMPLETE");
1696
+ console.log("=".repeat(60));
1697
+ console.log("");
1698
+ console.log(" Your .env file has been created.");
1699
+ console.log("");
1700
+ console.log(` Wallet: ${walletAddress}`);
1701
+ console.log(` LLM: ${llmProvider}`);
1702
+ console.log("");
1703
+ if (walletSetup === "generate") {
1704
+ console.log(" NEXT: Fund your wallet before starting to trade.");
1705
+ console.log(` Send testnet ETH to: ${walletAddress}`);
1706
+ }
1707
+ console.log("");
1708
+ console.log(" The agent will now start...");
1709
+ console.log("");
1710
+ (0, import_dotenv2.config)({ path: envPath, override: true });
1711
+ }
1712
+ program.command("run").description("Start the trading agent").option("-c, --config <path>", "Path to agent-config.json", "agent-config.json").action(async (options) => {
1713
+ try {
1714
+ await checkFirstRunSetup(options.config);
1715
+ console.log("Loading configuration...");
1716
+ const config = loadConfig(options.config);
1717
+ validateConfig(config);
1718
+ console.log("");
1719
+ console.log("=".repeat(50));
1720
+ console.log(" EXAGENT TRADING AGENT");
1721
+ console.log("=".repeat(50));
1722
+ console.log("");
1723
+ console.log(` Agent: ${config.name} (ID: ${config.agentId})`);
1724
+ console.log(` Network: ${config.network}`);
1725
+ console.log(` LLM: ${config.llm.provider} (${config.llm.model || "default"})`);
1726
+ console.log(` Universe: ${config.riskUniverse}`);
1727
+ console.log("");
1728
+ console.log("=".repeat(50));
1729
+ console.log("");
1730
+ const agent = new AgentRuntime(config);
1731
+ await agent.initialize();
1732
+ process.on("SIGINT", () => {
1733
+ console.log("\nReceived SIGINT, shutting down...");
1734
+ agent.stop();
1735
+ process.exit(0);
1736
+ });
1737
+ process.on("SIGTERM", () => {
1738
+ console.log("\nReceived SIGTERM, shutting down...");
1739
+ agent.stop();
1740
+ process.exit(0);
1741
+ });
1742
+ await agent.run();
1743
+ } catch (error) {
1744
+ console.error("Error:", error instanceof Error ? error.message : error);
1745
+ process.exit(1);
1746
+ }
1747
+ });
1748
+ program.command("templates").description("List available strategy templates").action(() => {
1749
+ console.log("");
1750
+ console.log("Available Strategy Templates:");
1751
+ console.log("=".repeat(50));
1752
+ console.log("");
1753
+ const templates = getAllStrategyTemplates();
1754
+ for (const template of templates) {
1755
+ console.log(`${template.id.toUpperCase()}`);
1756
+ console.log(` Name: ${template.name}`);
1757
+ console.log(` Risk: ${template.riskLevel}`);
1758
+ console.log(` ${template.description}`);
1759
+ console.log("");
1760
+ console.log(" Warnings:");
1761
+ for (const warning of template.riskWarnings) {
1762
+ console.log(` - ${warning}`);
1763
+ }
1764
+ console.log("");
1765
+ console.log("-".repeat(50));
1766
+ console.log("");
1767
+ }
1768
+ });
1769
+ program.command("status").description("Check agent configuration status").option("-c, --config <path>", "Path to agent-config.json", "agent-config.json").action(async (options) => {
1770
+ try {
1771
+ const config = loadConfig(options.config);
1772
+ validateConfig(config);
1773
+ console.log("");
1774
+ console.log("Agent Configuration:");
1775
+ console.log("=".repeat(50));
1776
+ console.log(` Agent ID: ${config.agentId}`);
1777
+ console.log(` Name: ${config.name}`);
1778
+ console.log(` Network: ${config.network}`);
1779
+ console.log(` LLM: ${config.llm.provider} (${config.llm.model || "default"})`);
1780
+ console.log(` Universe: ${config.riskUniverse}`);
1781
+ console.log("");
1782
+ console.log("Trading Config:");
1783
+ console.log(` Horizon: ${config.trading.timeHorizon}`);
1784
+ console.log(` Max Position: ${config.trading.maxPositionSizeBps / 100}%`);
1785
+ console.log(` Max Daily Loss: ${config.trading.maxDailyLossBps / 100}%`);
1786
+ console.log(` Interval: ${config.trading.tradingIntervalMs}ms`);
1787
+ console.log("");
1788
+ console.log("Configuration is valid.");
1789
+ } catch (error) {
1790
+ console.error("Configuration error:", error instanceof Error ? error.message : error);
1791
+ process.exit(1);
1792
+ }
1793
+ });
1794
+ program.command("api-keys").description("Show how to get API keys for each LLM provider").action(() => {
1795
+ console.log("");
1796
+ console.log("LLM Provider API Keys:");
1797
+ console.log("=".repeat(50));
1798
+ console.log("");
1799
+ console.log("OPENAI");
1800
+ console.log(" Get your API key at: https://platform.openai.com/api-keys");
1801
+ console.log("");
1802
+ console.log("ANTHROPIC");
1803
+ console.log(" Get your API key at: https://console.anthropic.com/settings/keys");
1804
+ console.log("");
1805
+ console.log("DEEPSEEK");
1806
+ console.log(" Get your API key at: https://platform.deepseek.com/api_keys");
1807
+ console.log("");
1808
+ console.log("GOOGLE AI");
1809
+ console.log(" Get your API key at: https://aistudio.google.com/apikey");
1810
+ console.log("");
1811
+ console.log("MISTRAL AI");
1812
+ console.log(" Get your API key at: https://console.mistral.ai/api-keys/");
1813
+ console.log("");
1814
+ console.log("GROQ");
1815
+ console.log(" Get your API key at: https://console.groq.com/keys");
1816
+ console.log("");
1817
+ console.log("TOGETHER AI");
1818
+ console.log(" Get your API key at: https://api.together.xyz/settings/api-keys");
1819
+ console.log("");
1820
+ console.log("OLLAMA (Local - No API Key Required)");
1821
+ console.log(" Install: https://ollama.com/download");
1822
+ console.log(" Run: ollama serve");
1823
+ console.log(" Pull model: ollama pull mistral");
1824
+ console.log("");
1825
+ console.log("Note: Your API keys are stored locally in .env and never sent to Exagent servers.");
1826
+ console.log("");
1827
+ });
1828
+ program.parse();