@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.
@@ -0,0 +1,1121 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/llm/base.ts
9
+ var BaseLLMAdapter = class {
10
+ config;
11
+ constructor(config) {
12
+ this.config = config;
13
+ }
14
+ getMetadata() {
15
+ return {
16
+ provider: this.config.provider,
17
+ model: this.config.model || "unknown",
18
+ isLocal: this.config.provider === "ollama"
19
+ };
20
+ }
21
+ /**
22
+ * Format model name for display
23
+ */
24
+ getDisplayModel() {
25
+ if (this.config.provider === "ollama") {
26
+ return `Local (${this.config.model || "ollama"})`;
27
+ }
28
+ return this.config.model || this.config.provider;
29
+ }
30
+ };
31
+
32
+ // src/llm/openai.ts
33
+ import OpenAI from "openai";
34
+ var OpenAIAdapter = class extends BaseLLMAdapter {
35
+ client;
36
+ constructor(config) {
37
+ super(config);
38
+ if (!config.apiKey && !config.endpoint) {
39
+ throw new Error("OpenAI API key or custom endpoint required");
40
+ }
41
+ this.client = new OpenAI({
42
+ apiKey: config.apiKey || "not-needed-for-custom",
43
+ baseURL: config.endpoint
44
+ });
45
+ }
46
+ async chat(messages) {
47
+ try {
48
+ const response = await this.client.chat.completions.create({
49
+ model: this.config.model || "gpt-4-turbo-preview",
50
+ messages: messages.map((m) => ({
51
+ role: m.role,
52
+ content: m.content
53
+ })),
54
+ temperature: this.config.temperature,
55
+ max_tokens: this.config.maxTokens
56
+ });
57
+ const choice = response.choices[0];
58
+ if (!choice || !choice.message) {
59
+ throw new Error("No response from OpenAI");
60
+ }
61
+ return {
62
+ content: choice.message.content || "",
63
+ usage: response.usage ? {
64
+ promptTokens: response.usage.prompt_tokens,
65
+ completionTokens: response.usage.completion_tokens,
66
+ totalTokens: response.usage.total_tokens
67
+ } : void 0
68
+ };
69
+ } catch (error) {
70
+ if (error instanceof OpenAI.APIError) {
71
+ throw new Error(`OpenAI API error: ${error.message}`);
72
+ }
73
+ throw error;
74
+ }
75
+ }
76
+ };
77
+
78
+ // src/llm/anthropic.ts
79
+ var AnthropicAdapter = class extends BaseLLMAdapter {
80
+ apiKey;
81
+ baseUrl;
82
+ constructor(config) {
83
+ super(config);
84
+ if (!config.apiKey) {
85
+ throw new Error("Anthropic API key required");
86
+ }
87
+ this.apiKey = config.apiKey;
88
+ this.baseUrl = config.endpoint || "https://api.anthropic.com";
89
+ }
90
+ async chat(messages) {
91
+ const systemMessage = messages.find((m) => m.role === "system");
92
+ const chatMessages = messages.filter((m) => m.role !== "system");
93
+ const body = {
94
+ model: this.config.model || "claude-3-opus-20240229",
95
+ max_tokens: this.config.maxTokens || 4096,
96
+ temperature: this.config.temperature,
97
+ system: systemMessage?.content,
98
+ messages: chatMessages.map((m) => ({
99
+ role: m.role,
100
+ content: m.content
101
+ }))
102
+ };
103
+ const response = await fetch(`${this.baseUrl}/v1/messages`, {
104
+ method: "POST",
105
+ headers: {
106
+ "Content-Type": "application/json",
107
+ "x-api-key": this.apiKey,
108
+ "anthropic-version": "2023-06-01"
109
+ },
110
+ body: JSON.stringify(body)
111
+ });
112
+ if (!response.ok) {
113
+ const error = await response.text();
114
+ throw new Error(`Anthropic API error: ${response.status} - ${error}`);
115
+ }
116
+ const data = await response.json();
117
+ const content = data.content?.map(
118
+ (block) => block.type === "text" ? block.text : ""
119
+ ).join("") || "";
120
+ return {
121
+ content,
122
+ usage: data.usage ? {
123
+ promptTokens: data.usage.input_tokens,
124
+ completionTokens: data.usage.output_tokens,
125
+ totalTokens: data.usage.input_tokens + data.usage.output_tokens
126
+ } : void 0
127
+ };
128
+ }
129
+ };
130
+
131
+ // src/llm/ollama.ts
132
+ var OllamaAdapter = class extends BaseLLMAdapter {
133
+ baseUrl;
134
+ constructor(config) {
135
+ super(config);
136
+ this.baseUrl = config.endpoint || "http://localhost:11434";
137
+ }
138
+ /**
139
+ * Check if Ollama is running and the model is available
140
+ */
141
+ async healthCheck() {
142
+ try {
143
+ const response = await fetch(`${this.baseUrl}/api/tags`);
144
+ if (!response.ok) {
145
+ throw new Error("Ollama server not responding");
146
+ }
147
+ const data = await response.json();
148
+ const models = data.models?.map((m) => m.name) || [];
149
+ if (this.config.model && !models.some((m) => m.startsWith(this.config.model))) {
150
+ console.warn(
151
+ `Model "${this.config.model}" not found locally. Available: ${models.join(", ")}`
152
+ );
153
+ console.warn(`Run: ollama pull ${this.config.model}`);
154
+ }
155
+ } catch (error) {
156
+ throw new Error(
157
+ `Cannot connect to Ollama at ${this.baseUrl}. Make sure Ollama is running (ollama serve) or install it from https://ollama.com`
158
+ );
159
+ }
160
+ }
161
+ async chat(messages) {
162
+ const body = {
163
+ model: this.config.model || "mistral",
164
+ messages: messages.map((m) => ({
165
+ role: m.role,
166
+ content: m.content
167
+ })),
168
+ stream: false,
169
+ options: {
170
+ temperature: this.config.temperature,
171
+ num_predict: this.config.maxTokens
172
+ }
173
+ };
174
+ const response = await fetch(`${this.baseUrl}/api/chat`, {
175
+ method: "POST",
176
+ headers: {
177
+ "Content-Type": "application/json"
178
+ },
179
+ body: JSON.stringify(body)
180
+ });
181
+ if (!response.ok) {
182
+ const error = await response.text();
183
+ throw new Error(`Ollama API error: ${response.status} - ${error}`);
184
+ }
185
+ const data = await response.json();
186
+ return {
187
+ content: data.message?.content || "",
188
+ usage: data.eval_count ? {
189
+ promptTokens: data.prompt_eval_count || 0,
190
+ completionTokens: data.eval_count,
191
+ totalTokens: (data.prompt_eval_count || 0) + data.eval_count
192
+ } : void 0
193
+ };
194
+ }
195
+ getMetadata() {
196
+ return {
197
+ provider: "ollama",
198
+ model: this.config.model || "mistral",
199
+ isLocal: true
200
+ };
201
+ }
202
+ };
203
+
204
+ // src/llm/adapter.ts
205
+ async function createLLMAdapter(config) {
206
+ switch (config.provider) {
207
+ case "openai":
208
+ return new OpenAIAdapter(config);
209
+ case "anthropic":
210
+ return new AnthropicAdapter(config);
211
+ case "ollama":
212
+ const adapter = new OllamaAdapter(config);
213
+ await adapter.healthCheck();
214
+ return adapter;
215
+ case "custom":
216
+ return new OpenAIAdapter({
217
+ ...config,
218
+ endpoint: config.endpoint
219
+ });
220
+ default:
221
+ throw new Error(`Unsupported LLM provider: ${config.provider}`);
222
+ }
223
+ }
224
+
225
+ // src/strategy/loader.ts
226
+ import { existsSync } from "fs";
227
+ import { join } from "path";
228
+ import { spawn } from "child_process";
229
+ async function loadStrategy(strategyPath) {
230
+ const basePath = strategyPath || process.env.EXAGENT_STRATEGY || "strategy";
231
+ const tsPath = basePath.endsWith(".ts") || basePath.endsWith(".js") ? basePath : `${basePath}.ts`;
232
+ const jsPath = basePath.endsWith(".ts") || basePath.endsWith(".js") ? basePath.replace(".ts", ".js") : `${basePath}.js`;
233
+ const fullTsPath = tsPath.startsWith("/") ? tsPath : join(process.cwd(), tsPath);
234
+ const fullJsPath = jsPath.startsWith("/") ? jsPath : join(process.cwd(), jsPath);
235
+ if (existsSync(fullTsPath) && fullTsPath.endsWith(".ts")) {
236
+ try {
237
+ const module = await loadTypeScriptModule(fullTsPath);
238
+ if (typeof module.generateSignals !== "function") {
239
+ throw new Error("Strategy must export a generateSignals function");
240
+ }
241
+ console.log(`Loaded custom strategy from ${tsPath}`);
242
+ return module.generateSignals;
243
+ } catch (error) {
244
+ console.error(`Failed to load strategy from ${tsPath}:`, error);
245
+ throw error;
246
+ }
247
+ }
248
+ if (existsSync(fullJsPath)) {
249
+ try {
250
+ const module = await import(fullJsPath);
251
+ if (typeof module.generateSignals !== "function") {
252
+ throw new Error("Strategy must export a generateSignals function");
253
+ }
254
+ console.log(`Loaded custom strategy from ${jsPath}`);
255
+ return module.generateSignals;
256
+ } catch (error) {
257
+ console.error(`Failed to load strategy from ${jsPath}:`, error);
258
+ throw error;
259
+ }
260
+ }
261
+ console.log("No custom strategy found, using default (hold) strategy");
262
+ return defaultStrategy;
263
+ }
264
+ async function loadTypeScriptModule(path) {
265
+ try {
266
+ const tsxPath = __require.resolve("tsx");
267
+ const { pathToFileURL } = await import("url");
268
+ const result = await new Promise((resolve, reject) => {
269
+ const child = spawn(
270
+ process.execPath,
271
+ [
272
+ "--import",
273
+ "tsx/esm",
274
+ "-e",
275
+ `import('${pathToFileURL(path).href}').then(m => console.log(JSON.stringify({ exports: Object.keys(m) }))).catch(e => console.error('ERROR:', e.message))`
276
+ ],
277
+ {
278
+ cwd: process.cwd(),
279
+ env: process.env,
280
+ stdio: ["pipe", "pipe", "pipe"]
281
+ }
282
+ );
283
+ let stdout = "";
284
+ let stderr = "";
285
+ child.stdout.on("data", (data) => stdout += data.toString());
286
+ child.stderr.on("data", (data) => stderr += data.toString());
287
+ child.on("close", (code) => {
288
+ if (code !== 0 || stderr.includes("ERROR:")) {
289
+ reject(new Error(`Failed to load TypeScript: ${stderr || "Unknown error"}`));
290
+ } else {
291
+ resolve(stdout);
292
+ }
293
+ });
294
+ });
295
+ const tsx = await import("tsx/esm/api");
296
+ const unregister = tsx.register();
297
+ try {
298
+ const module = await import(path);
299
+ return module;
300
+ } finally {
301
+ unregister();
302
+ }
303
+ } catch (error) {
304
+ if (error.code === "MODULE_NOT_FOUND" || error.message.includes("Cannot find module")) {
305
+ throw new Error(
306
+ `Cannot load TypeScript strategy. Please either:
307
+ 1. Rename your strategy.ts to strategy.js (remove type annotations)
308
+ 2. Or compile it: npx tsc strategy.ts --outDir . --esModuleInterop
309
+ 3. Or install tsx: npm install tsx`
310
+ );
311
+ }
312
+ throw error;
313
+ }
314
+ }
315
+ var defaultStrategy = async (_marketData, _llm, _config) => {
316
+ return [];
317
+ };
318
+ function validateStrategy(fn) {
319
+ return typeof fn === "function";
320
+ }
321
+
322
+ // src/strategy/templates.ts
323
+ var STRATEGY_TEMPLATES = [
324
+ {
325
+ id: "momentum",
326
+ name: "Momentum Trader",
327
+ description: "Follows price trends and momentum indicators. Buys assets with strong upward momentum.",
328
+ riskLevel: "medium",
329
+ riskWarnings: [
330
+ "Momentum strategies can suffer significant losses during trend reversals",
331
+ "High volatility markets may generate false signals",
332
+ "Past performance does not guarantee future results",
333
+ "This strategy may underperform in sideways markets"
334
+ ],
335
+ systemPrompt: `You are an AI trading analyst specializing in momentum trading strategies.
336
+
337
+ Your role is to analyze market data and identify momentum-based trading opportunities.
338
+
339
+ IMPORTANT CONSTRAINTS:
340
+ - Only recommend trades when there is clear momentum evidence
341
+ - Always consider risk/reward ratios
342
+ - Never recommend more than the configured position size limits
343
+ - Be conservative with confidence scores
344
+
345
+ When analyzing data, look for:
346
+ 1. Price trends (higher highs, higher lows for uptrends)
347
+ 2. Volume confirmation (increasing volume on moves)
348
+ 3. Relative strength vs market benchmarks
349
+
350
+ Respond with JSON in this format:
351
+ {
352
+ "analysis": "Brief market analysis",
353
+ "signals": [
354
+ {
355
+ "action": "buy" | "sell" | "hold",
356
+ "tokenIn": "0x...",
357
+ "tokenOut": "0x...",
358
+ "percentage": 0-100,
359
+ "confidence": 0-1,
360
+ "reasoning": "Why this trade"
361
+ }
362
+ ]
363
+ }`,
364
+ exampleCode: `import { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig } from '@exagent/agent';
365
+
366
+ export const generateSignals: StrategyFunction = async (
367
+ marketData: MarketData,
368
+ llm: LLMAdapter,
369
+ config: AgentConfig
370
+ ): Promise<TradeSignal[]> => {
371
+ const response = await llm.chat([
372
+ { role: 'system', content: MOMENTUM_SYSTEM_PROMPT },
373
+ { role: 'user', content: JSON.stringify({
374
+ prices: marketData.prices,
375
+ balances: formatBalances(marketData.balances),
376
+ portfolioValue: marketData.portfolioValue,
377
+ })}
378
+ ]);
379
+
380
+ // Parse LLM response and convert to TradeSignals
381
+ const parsed = JSON.parse(response.content);
382
+ return parsed.signals.map(convertToTradeSignal);
383
+ };`
384
+ },
385
+ {
386
+ id: "value",
387
+ name: "Value Investor",
388
+ description: "Looks for undervalued assets based on fundamentals. Takes long-term positions.",
389
+ riskLevel: "low",
390
+ riskWarnings: [
391
+ "Value traps can result in prolonged losses",
392
+ "Requires patience - may underperform for extended periods",
393
+ "Fundamental analysis may not apply well to all crypto assets",
394
+ "Market sentiment can override fundamentals for long periods"
395
+ ],
396
+ systemPrompt: `You are an AI trading analyst specializing in value investing.
397
+
398
+ Your role is to identify undervalued assets with strong fundamentals.
399
+
400
+ IMPORTANT CONSTRAINTS:
401
+ - Focus on long-term value, not short-term price movements
402
+ - Only recommend assets with clear value propositions
403
+ - Consider protocol revenue, TVL, active users, developer activity
404
+ - Be very selective - quality over quantity
405
+
406
+ When analyzing, consider:
407
+ 1. Protocol fundamentals (revenue, TVL, user growth)
408
+ 2. Token economics (supply schedule, utility)
409
+ 3. Competitive positioning
410
+ 4. Valuation relative to peers
411
+
412
+ Respond with JSON in this format:
413
+ {
414
+ "analysis": "Brief fundamental analysis",
415
+ "signals": [
416
+ {
417
+ "action": "buy" | "sell" | "hold",
418
+ "tokenIn": "0x...",
419
+ "tokenOut": "0x...",
420
+ "percentage": 0-100,
421
+ "confidence": 0-1,
422
+ "reasoning": "Fundamental thesis"
423
+ }
424
+ ]
425
+ }`,
426
+ exampleCode: `import { StrategyFunction } from '@exagent/agent';
427
+
428
+ export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
429
+ // Value strategy runs less frequently
430
+ const response = await llm.chat([
431
+ { role: 'system', content: VALUE_SYSTEM_PROMPT },
432
+ { role: 'user', content: JSON.stringify(marketData) }
433
+ ]);
434
+
435
+ return parseSignals(response.content);
436
+ };`
437
+ },
438
+ {
439
+ id: "arbitrage",
440
+ name: "Arbitrage Hunter",
441
+ description: "Looks for price discrepancies across DEXs. Requires fast execution.",
442
+ riskLevel: "high",
443
+ riskWarnings: [
444
+ "Arbitrage opportunities are highly competitive - professional bots dominate",
445
+ "Slippage and gas costs can eliminate profits",
446
+ "MEV bots may front-run your transactions",
447
+ "Requires very fast execution and may not be profitable with standard infrastructure",
448
+ "This strategy is generally NOT recommended for beginners"
449
+ ],
450
+ systemPrompt: `You are an AI trading analyst specializing in arbitrage detection.
451
+
452
+ Your role is to identify price discrepancies that may offer arbitrage opportunities.
453
+
454
+ IMPORTANT CONSTRAINTS:
455
+ - Account for gas costs in all calculations
456
+ - Account for slippage (assume 0.3% minimum)
457
+ - Only flag opportunities with >1% net profit potential
458
+ - Consider MEV risk - assume some profit extraction
459
+
460
+ This is an advanced strategy with high competition.
461
+
462
+ Respond with JSON in this format:
463
+ {
464
+ "opportunities": [
465
+ {
466
+ "description": "What the arbitrage is",
467
+ "expectedProfit": "Net profit after costs",
468
+ "confidence": 0-1,
469
+ "warning": "Risks specific to this opportunity"
470
+ }
471
+ ]
472
+ }`,
473
+ exampleCode: `// Note: Pure arbitrage requires specialized infrastructure
474
+ // This template is for educational purposes
475
+
476
+ import { StrategyFunction } from '@exagent/agent';
477
+
478
+ export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
479
+ // Arbitrage requires real-time price feeds from multiple sources
480
+ // Standard LLM-based analysis is too slow for most arbitrage
481
+ console.warn('Arbitrage strategy requires specialized infrastructure');
482
+ return [];
483
+ };`
484
+ },
485
+ {
486
+ id: "custom",
487
+ name: "Custom Strategy",
488
+ description: "Build your own strategy from scratch. Full control over logic and prompts.",
489
+ riskLevel: "extreme",
490
+ riskWarnings: [
491
+ "Custom strategies have no guardrails - you are fully responsible",
492
+ "LLMs can hallucinate or make errors - always validate outputs",
493
+ "Test thoroughly on testnet before using real funds",
494
+ "Consider edge cases: what happens if the LLM returns invalid JSON?",
495
+ "Your prompts and strategy logic are your competitive advantage - protect them",
496
+ "Agents may not behave exactly as expected based on your prompts"
497
+ ],
498
+ systemPrompt: `// Define your own system prompt here
499
+
500
+ You are a trading AI. Analyze the market data and provide trading signals.
501
+
502
+ // Add your specific instructions, constraints, and output format.
503
+
504
+ Respond with JSON:
505
+ {
506
+ "signals": []
507
+ }`,
508
+ exampleCode: `import { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig } from '@exagent/agent';
509
+
510
+ /**
511
+ * Custom Strategy Template
512
+ *
513
+ * Customize this file with your own trading logic and prompts.
514
+ * Your prompts are YOUR intellectual property - we don't store them.
515
+ */
516
+ export const generateSignals: StrategyFunction = async (
517
+ marketData: MarketData,
518
+ llm: LLMAdapter,
519
+ config: AgentConfig
520
+ ): Promise<TradeSignal[]> => {
521
+ // Your custom system prompt (this is your secret sauce)
522
+ const systemPrompt = \`
523
+ Your custom instructions here...
524
+ \`;
525
+
526
+ // Call the LLM with your prompt
527
+ const response = await llm.chat([
528
+ { role: 'system', content: systemPrompt },
529
+ { role: 'user', content: JSON.stringify(marketData) }
530
+ ]);
531
+
532
+ // Parse and return signals
533
+ // IMPORTANT: Validate LLM output before using
534
+ try {
535
+ const parsed = JSON.parse(response.content);
536
+ return parsed.signals || [];
537
+ } catch (e) {
538
+ console.error('Failed to parse LLM response:', e);
539
+ return []; // Safe fallback: no trades
540
+ }
541
+ };`
542
+ }
543
+ ];
544
+ function getStrategyTemplate(id) {
545
+ return STRATEGY_TEMPLATES.find((t) => t.id === id);
546
+ }
547
+ function getAllStrategyTemplates() {
548
+ return STRATEGY_TEMPLATES;
549
+ }
550
+
551
+ // src/trading/executor.ts
552
+ var TradeExecutor = class {
553
+ client;
554
+ config;
555
+ constructor(client, config) {
556
+ this.client = client;
557
+ this.config = config;
558
+ }
559
+ /**
560
+ * Execute a single trade signal
561
+ */
562
+ async execute(signal) {
563
+ if (signal.action === "hold") {
564
+ return { success: true };
565
+ }
566
+ try {
567
+ console.log(`Executing ${signal.action}: ${signal.tokenIn} -> ${signal.tokenOut}`);
568
+ console.log(`Amount: ${signal.amountIn.toString()}, Confidence: ${signal.confidence}`);
569
+ if (!this.validateSignal(signal)) {
570
+ return { success: false, error: "Signal exceeds position limits" };
571
+ }
572
+ const result = await this.client.trade({
573
+ tokenIn: signal.tokenIn,
574
+ tokenOut: signal.tokenOut,
575
+ amountIn: signal.amountIn,
576
+ maxSlippageBps: 100
577
+ // 1% default slippage
578
+ });
579
+ console.log(`Trade executed: ${result.hash}`);
580
+ return { success: true, txHash: result.hash };
581
+ } catch (error) {
582
+ const message = error instanceof Error ? error.message : "Unknown error";
583
+ console.error(`Trade failed: ${message}`);
584
+ return { success: false, error: message };
585
+ }
586
+ }
587
+ /**
588
+ * Execute multiple trade signals
589
+ * Returns results for each signal
590
+ */
591
+ async executeAll(signals) {
592
+ const results = [];
593
+ for (const signal of signals) {
594
+ const result = await this.execute(signal);
595
+ results.push({ signal, ...result });
596
+ if (signals.indexOf(signal) < signals.length - 1) {
597
+ await this.delay(1e3);
598
+ }
599
+ }
600
+ return results;
601
+ }
602
+ /**
603
+ * Validate a signal against config limits
604
+ */
605
+ validateSignal(signal) {
606
+ if (signal.confidence < 0.5) {
607
+ console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
608
+ return false;
609
+ }
610
+ return true;
611
+ }
612
+ delay(ms) {
613
+ return new Promise((resolve) => setTimeout(resolve, ms));
614
+ }
615
+ };
616
+
617
+ // src/trading/risk.ts
618
+ var RiskManager = class {
619
+ config;
620
+ dailyPnL = 0;
621
+ lastResetDate = "";
622
+ constructor(config) {
623
+ this.config = config;
624
+ }
625
+ /**
626
+ * Filter signals through risk checks
627
+ * Returns only signals that pass all guardrails
628
+ */
629
+ filterSignals(signals, marketData) {
630
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
631
+ if (today !== this.lastResetDate) {
632
+ this.dailyPnL = 0;
633
+ this.lastResetDate = today;
634
+ }
635
+ if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
636
+ console.warn("Daily loss limit reached - no new trades");
637
+ return [];
638
+ }
639
+ return signals.filter((signal) => this.validateSignal(signal, marketData));
640
+ }
641
+ /**
642
+ * Validate individual signal against risk limits
643
+ */
644
+ validateSignal(signal, marketData) {
645
+ if (signal.action === "hold") {
646
+ return true;
647
+ }
648
+ const signalValue = this.estimateSignalValue(signal, marketData);
649
+ const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
650
+ if (signalValue > maxPositionValue) {
651
+ console.warn(
652
+ `Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
653
+ );
654
+ return false;
655
+ }
656
+ if (signal.confidence < 0.5) {
657
+ console.warn(`Signal confidence too low: ${signal.confidence}`);
658
+ return false;
659
+ }
660
+ return true;
661
+ }
662
+ /**
663
+ * Check if daily loss limit has been hit
664
+ */
665
+ isDailyLossLimitHit(portfolioValue) {
666
+ const maxLoss = portfolioValue * this.config.maxDailyLossBps / 1e4;
667
+ return this.dailyPnL < -maxLoss;
668
+ }
669
+ /**
670
+ * Estimate USD value of a trade signal
671
+ */
672
+ estimateSignalValue(signal, marketData) {
673
+ const price = marketData.prices[signal.tokenIn] || 0;
674
+ const amount = Number(signal.amountIn) / 1e18;
675
+ return amount * price;
676
+ }
677
+ /**
678
+ * Update daily PnL after a trade
679
+ */
680
+ updatePnL(pnl) {
681
+ this.dailyPnL += pnl;
682
+ }
683
+ /**
684
+ * Get current risk status
685
+ */
686
+ getStatus() {
687
+ return {
688
+ dailyPnL: this.dailyPnL,
689
+ dailyLossLimit: this.config.maxDailyLossBps / 100,
690
+ // As percentage
691
+ isLimitHit: this.dailyPnL < -(this.config.maxDailyLossBps / 100)
692
+ };
693
+ }
694
+ };
695
+
696
+ // src/trading/market.ts
697
+ var MarketDataService = class {
698
+ rpcUrl;
699
+ constructor(rpcUrl) {
700
+ this.rpcUrl = rpcUrl;
701
+ }
702
+ /**
703
+ * Fetch current market data for the agent
704
+ */
705
+ async fetchMarketData(walletAddress, tokenAddresses) {
706
+ const prices = await this.fetchPrices(tokenAddresses);
707
+ const balances = await this.fetchBalances(walletAddress, tokenAddresses);
708
+ const portfolioValue = this.calculatePortfolioValue(balances, prices);
709
+ return {
710
+ timestamp: Date.now(),
711
+ prices,
712
+ balances,
713
+ portfolioValue
714
+ };
715
+ }
716
+ /**
717
+ * Fetch token prices from price oracle
718
+ */
719
+ async fetchPrices(tokenAddresses) {
720
+ const prices = {};
721
+ const knownPrices = {
722
+ // WETH
723
+ "0x4200000000000000000000000000000000000006": 3500,
724
+ // USDC
725
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913": 1,
726
+ // USDbC (bridged USDC)
727
+ "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA": 1
728
+ };
729
+ for (const address of tokenAddresses) {
730
+ prices[address.toLowerCase()] = knownPrices[address.toLowerCase()] || 0;
731
+ }
732
+ return prices;
733
+ }
734
+ /**
735
+ * Fetch token balances from chain
736
+ */
737
+ async fetchBalances(walletAddress, tokenAddresses) {
738
+ const balances = {};
739
+ for (const address of tokenAddresses) {
740
+ balances[address.toLowerCase()] = 0n;
741
+ }
742
+ return balances;
743
+ }
744
+ /**
745
+ * Calculate total portfolio value in USD
746
+ */
747
+ calculatePortfolioValue(balances, prices) {
748
+ let total = 0;
749
+ for (const [address, balance] of Object.entries(balances)) {
750
+ const price = prices[address.toLowerCase()] || 0;
751
+ const amount = Number(balance) / 1e18;
752
+ total += amount * price;
753
+ }
754
+ return total;
755
+ }
756
+ };
757
+
758
+ // src/runtime.ts
759
+ import { ExagentClient, ExagentRegistry } from "@exagent/sdk";
760
+ var AgentRuntime = class {
761
+ config;
762
+ client;
763
+ llm;
764
+ strategy;
765
+ executor;
766
+ riskManager;
767
+ marketData;
768
+ isRunning = false;
769
+ configHash;
770
+ constructor(config) {
771
+ this.config = config;
772
+ }
773
+ /**
774
+ * Initialize the agent runtime
775
+ */
776
+ async initialize() {
777
+ console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
778
+ this.client = new ExagentClient({
779
+ privateKey: this.config.privateKey,
780
+ network: this.config.network
781
+ });
782
+ console.log(`Wallet: ${this.client.address}`);
783
+ const agent = await this.client.registry.getAgent(BigInt(this.config.agentId));
784
+ if (!agent) {
785
+ throw new Error(`Agent ID ${this.config.agentId} not found on-chain. Please register first.`);
786
+ }
787
+ console.log(`Agent verified: ${agent.name}`);
788
+ await this.ensureWalletLinked();
789
+ console.log(`Initializing LLM: ${this.config.llm.provider}`);
790
+ this.llm = await createLLMAdapter(this.config.llm);
791
+ const llmMeta = this.llm.getMetadata();
792
+ console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
793
+ await this.syncConfigHash();
794
+ this.strategy = await loadStrategy();
795
+ this.executor = new TradeExecutor(this.client, this.config);
796
+ this.riskManager = new RiskManager(this.config.trading);
797
+ this.marketData = new MarketDataService(this.getRpcUrl());
798
+ console.log("Agent initialized successfully");
799
+ }
800
+ /**
801
+ * Ensure the current wallet is linked to the agent
802
+ */
803
+ async ensureWalletLinked() {
804
+ const agentId = BigInt(this.config.agentId);
805
+ const address = this.client.address;
806
+ const isLinked = await this.client.registry.isLinkedWallet(agentId, address);
807
+ if (!isLinked) {
808
+ console.log("Wallet not linked, linking now...");
809
+ const agent = await this.client.registry.getAgent(agentId);
810
+ if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
811
+ throw new Error(
812
+ `Cannot link wallet: ${address} is not the owner of agent ${this.config.agentId}. Owner is ${agent?.owner}`
813
+ );
814
+ }
815
+ await this.client.registry.linkOwnWallet(agentId);
816
+ console.log("Wallet linked successfully");
817
+ } else {
818
+ console.log("Wallet already linked");
819
+ }
820
+ }
821
+ /**
822
+ * Sync the LLM config hash to chain for epoch tracking
823
+ * This ensures trades are attributed to the correct config epoch
824
+ */
825
+ async syncConfigHash() {
826
+ const agentId = BigInt(this.config.agentId);
827
+ const llmMeta = this.llm.getMetadata();
828
+ this.configHash = ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
829
+ console.log(`Config hash: ${this.configHash}`);
830
+ const onChainHash = await this.client.registry.getConfigHash(agentId);
831
+ if (onChainHash !== this.configHash) {
832
+ console.log("Config changed, updating on-chain...");
833
+ await this.client.registry.updateConfig(agentId, this.configHash);
834
+ const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
835
+ console.log(`Config updated, new epoch started: ${newEpoch}`);
836
+ } else {
837
+ const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
838
+ console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
839
+ }
840
+ }
841
+ /**
842
+ * Get the current config hash (for trade execution)
843
+ */
844
+ getConfigHash() {
845
+ return this.configHash;
846
+ }
847
+ /**
848
+ * Start the trading loop
849
+ */
850
+ async run() {
851
+ if (this.isRunning) {
852
+ throw new Error("Agent is already running");
853
+ }
854
+ this.isRunning = true;
855
+ console.log("Starting trading loop...");
856
+ console.log(`Interval: ${this.config.trading.tradingIntervalMs}ms`);
857
+ while (this.isRunning) {
858
+ try {
859
+ await this.runCycle();
860
+ } catch (error) {
861
+ console.error("Error in trading cycle:", error);
862
+ }
863
+ await this.sleep(this.config.trading.tradingIntervalMs);
864
+ }
865
+ }
866
+ /**
867
+ * Run a single trading cycle
868
+ */
869
+ async runCycle() {
870
+ console.log(`
871
+ --- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
872
+ const tokens = this.config.allowedTokens || this.getDefaultTokens();
873
+ const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
874
+ console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
875
+ const signals = await this.strategy(marketData, this.llm, this.config);
876
+ console.log(`Strategy generated ${signals.length} signals`);
877
+ const filteredSignals = this.riskManager.filterSignals(signals, marketData);
878
+ console.log(`${filteredSignals.length} signals passed risk checks`);
879
+ if (filteredSignals.length > 0) {
880
+ const results = await this.executor.executeAll(filteredSignals);
881
+ for (const result of results) {
882
+ if (result.success) {
883
+ console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
884
+ } else {
885
+ console.warn(`Trade failed: ${result.error}`);
886
+ }
887
+ }
888
+ }
889
+ }
890
+ /**
891
+ * Stop the trading loop
892
+ */
893
+ stop() {
894
+ console.log("Stopping agent...");
895
+ this.isRunning = false;
896
+ }
897
+ /**
898
+ * Get RPC URL based on network
899
+ */
900
+ getRpcUrl() {
901
+ if (this.config.network === "mainnet") {
902
+ return "https://mainnet.base.org";
903
+ }
904
+ return "https://sepolia.base.org";
905
+ }
906
+ /**
907
+ * Default tokens to track
908
+ */
909
+ getDefaultTokens() {
910
+ if (this.config.network === "mainnet") {
911
+ return [
912
+ "0x4200000000000000000000000000000000000006",
913
+ // WETH
914
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
915
+ // USDC
916
+ ];
917
+ }
918
+ return [
919
+ "0x4200000000000000000000000000000000000006"
920
+ // WETH
921
+ ];
922
+ }
923
+ sleep(ms) {
924
+ return new Promise((resolve) => setTimeout(resolve, ms));
925
+ }
926
+ /**
927
+ * Get current status
928
+ */
929
+ getStatus() {
930
+ return {
931
+ isRunning: this.isRunning,
932
+ agentId: Number(this.config.agentId),
933
+ wallet: this.client?.address || "not initialized",
934
+ llm: {
935
+ provider: this.config.llm.provider,
936
+ model: this.config.llm.model || "default"
937
+ },
938
+ configHash: this.configHash || "not initialized",
939
+ risk: this.riskManager?.getStatus() || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false }
940
+ };
941
+ }
942
+ };
943
+
944
+ // src/types.ts
945
+ import { z } from "zod";
946
+ var WalletSetupSchema = z.enum(["generate", "provide"]);
947
+ var LLMProviderSchema = z.enum(["openai", "anthropic", "google", "deepseek", "mistral", "groq", "together", "ollama", "custom"]);
948
+ var LLMConfigSchema = z.object({
949
+ provider: LLMProviderSchema,
950
+ model: z.string().optional(),
951
+ apiKey: z.string().optional(),
952
+ endpoint: z.string().url().optional(),
953
+ temperature: z.number().min(0).max(2).default(0.7),
954
+ maxTokens: z.number().positive().default(4096)
955
+ });
956
+ var RiskUniverseSchema = z.enum(["core", "established", "derivatives", "emerging", "frontier"]);
957
+ var TradingConfigSchema = z.object({
958
+ timeHorizon: z.enum(["intraday", "swing", "position"]).default("swing"),
959
+ maxPositionSizeBps: z.number().min(100).max(1e4).default(1e3),
960
+ // 1-100%
961
+ maxDailyLossBps: z.number().min(0).max(1e4).default(500),
962
+ // 0-100%
963
+ maxConcurrentPositions: z.number().min(1).max(100).default(5),
964
+ tradingIntervalMs: z.number().min(1e3).default(6e4)
965
+ // minimum 1 second
966
+ });
967
+ var VaultPolicySchema = z.enum([
968
+ "disabled",
969
+ // Never create a vault - trade with agent's own capital only
970
+ "manual",
971
+ // Only create vault when explicitly directed by owner
972
+ "auto_when_qualified"
973
+ // Automatically create vault when requirements are met
974
+ ]);
975
+ var VaultConfigSchema = z.object({
976
+ // Policy for vault creation (asked during deployment)
977
+ policy: VaultPolicySchema.default("manual"),
978
+ // Default vault name (auto-generated from agent name if not set)
979
+ defaultName: z.string().optional(),
980
+ // Default vault symbol (auto-generated if not set)
981
+ defaultSymbol: z.string().optional(),
982
+ // Fee recipient for vault fees (default: agent wallet)
983
+ feeRecipient: z.string().optional(),
984
+ // When vault exists, trade through vault instead of direct trading
985
+ // This pools depositors' capital with the agent's trades
986
+ preferVaultTrading: z.boolean().default(true)
987
+ });
988
+ var WalletConfigSchema = z.object({
989
+ setup: WalletSetupSchema.default("provide")
990
+ }).optional();
991
+ var AgentConfigSchema = z.object({
992
+ // Identity (from on-chain registration)
993
+ agentId: z.union([z.number().positive(), z.string()]),
994
+ name: z.string().min(3).max(32),
995
+ // Network
996
+ network: z.enum(["mainnet", "testnet"]).default("testnet"),
997
+ // Wallet setup preference
998
+ wallet: WalletConfigSchema,
999
+ // LLM
1000
+ llm: LLMConfigSchema,
1001
+ // Trading parameters
1002
+ riskUniverse: RiskUniverseSchema.default("established"),
1003
+ trading: TradingConfigSchema.default({}),
1004
+ // Vault configuration (copy trading)
1005
+ vault: VaultConfigSchema.default({}),
1006
+ // Allowed tokens (addresses)
1007
+ allowedTokens: z.array(z.string()).optional()
1008
+ });
1009
+
1010
+ // src/config.ts
1011
+ import { readFileSync, existsSync as existsSync2 } from "fs";
1012
+ import { join as join2 } from "path";
1013
+ import { config as loadEnv } from "dotenv";
1014
+ function loadConfig(configPath) {
1015
+ loadEnv();
1016
+ const configFile = configPath || process.env.EXAGENT_CONFIG || "agent-config.json";
1017
+ const fullPath = configFile.startsWith("/") ? configFile : join2(process.cwd(), configFile);
1018
+ if (!existsSync2(fullPath)) {
1019
+ throw new Error(`Config file not found: ${fullPath}`);
1020
+ }
1021
+ const rawConfig = JSON.parse(readFileSync(fullPath, "utf-8"));
1022
+ const config = AgentConfigSchema.parse(rawConfig);
1023
+ const privateKey = process.env.EXAGENT_PRIVATE_KEY;
1024
+ if (!privateKey) {
1025
+ throw new Error("EXAGENT_PRIVATE_KEY not set in environment");
1026
+ }
1027
+ if (!privateKey.startsWith("0x") || privateKey.length !== 66) {
1028
+ throw new Error("EXAGENT_PRIVATE_KEY must be a valid 32-byte hex string starting with 0x");
1029
+ }
1030
+ const llmConfig = { ...config.llm };
1031
+ if (process.env.OPENAI_API_KEY && config.llm.provider === "openai") {
1032
+ llmConfig.apiKey = process.env.OPENAI_API_KEY;
1033
+ }
1034
+ if (process.env.ANTHROPIC_API_KEY && config.llm.provider === "anthropic") {
1035
+ llmConfig.apiKey = process.env.ANTHROPIC_API_KEY;
1036
+ }
1037
+ if (process.env.EXAGENT_LLM_URL) {
1038
+ llmConfig.endpoint = process.env.EXAGENT_LLM_URL;
1039
+ }
1040
+ if (process.env.EXAGENT_LLM_MODEL) {
1041
+ llmConfig.model = process.env.EXAGENT_LLM_MODEL;
1042
+ }
1043
+ const network = process.env.EXAGENT_NETWORK || config.network;
1044
+ return {
1045
+ ...config,
1046
+ llm: llmConfig,
1047
+ network,
1048
+ privateKey
1049
+ };
1050
+ }
1051
+ function validateConfig(config) {
1052
+ if (!config.privateKey) {
1053
+ throw new Error("Private key is required");
1054
+ }
1055
+ if (config.llm.provider !== "ollama" && !config.llm.apiKey) {
1056
+ throw new Error(`API key required for ${config.llm.provider} provider`);
1057
+ }
1058
+ if (config.llm.provider === "ollama" && !config.llm.endpoint) {
1059
+ config.llm.endpoint = "http://localhost:11434";
1060
+ }
1061
+ if (config.llm.provider === "custom" && !config.llm.endpoint) {
1062
+ throw new Error("Endpoint required for custom LLM provider");
1063
+ }
1064
+ if (!config.agentId || Number(config.agentId) <= 0) {
1065
+ throw new Error("Valid agent ID required");
1066
+ }
1067
+ }
1068
+ function createSampleConfig(agentId, name) {
1069
+ return {
1070
+ agentId,
1071
+ name,
1072
+ network: "testnet",
1073
+ llm: {
1074
+ provider: "openai",
1075
+ model: "gpt-4-turbo-preview",
1076
+ temperature: 0.7,
1077
+ maxTokens: 4096
1078
+ },
1079
+ riskUniverse: "established",
1080
+ trading: {
1081
+ timeHorizon: "swing",
1082
+ maxPositionSizeBps: 1e3,
1083
+ maxDailyLossBps: 500,
1084
+ maxConcurrentPositions: 5,
1085
+ tradingIntervalMs: 6e4
1086
+ },
1087
+ vault: {
1088
+ // Default to manual - user must explicitly enable auto-creation
1089
+ policy: "manual",
1090
+ // Will use agent name for vault name if not set
1091
+ preferVaultTrading: true
1092
+ }
1093
+ };
1094
+ }
1095
+
1096
+ export {
1097
+ BaseLLMAdapter,
1098
+ OpenAIAdapter,
1099
+ AnthropicAdapter,
1100
+ OllamaAdapter,
1101
+ createLLMAdapter,
1102
+ loadStrategy,
1103
+ validateStrategy,
1104
+ STRATEGY_TEMPLATES,
1105
+ getStrategyTemplate,
1106
+ getAllStrategyTemplates,
1107
+ TradeExecutor,
1108
+ RiskManager,
1109
+ MarketDataService,
1110
+ AgentRuntime,
1111
+ LLMProviderSchema,
1112
+ LLMConfigSchema,
1113
+ RiskUniverseSchema,
1114
+ TradingConfigSchema,
1115
+ VaultPolicySchema,
1116
+ VaultConfigSchema,
1117
+ AgentConfigSchema,
1118
+ loadConfig,
1119
+ validateConfig,
1120
+ createSampleConfig
1121
+ };