@exagent/agent 0.1.17 → 0.1.19

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,4778 @@
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.1",
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-opus-4-5-20251101",
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/google.ts
132
+ var GoogleAdapter = class extends BaseLLMAdapter {
133
+ apiKey;
134
+ baseUrl;
135
+ constructor(config) {
136
+ super(config);
137
+ if (!config.apiKey) {
138
+ throw new Error("Google AI API key required");
139
+ }
140
+ this.apiKey = config.apiKey;
141
+ this.baseUrl = config.endpoint || "https://generativelanguage.googleapis.com/v1beta";
142
+ }
143
+ async chat(messages) {
144
+ const model = this.config.model || "gemini-2.5-flash";
145
+ const systemMessage = messages.find((m) => m.role === "system");
146
+ const chatMessages = messages.filter((m) => m.role !== "system");
147
+ const contents = chatMessages.map((m) => ({
148
+ role: m.role === "assistant" ? "model" : "user",
149
+ parts: [{ text: m.content }]
150
+ }));
151
+ const body = {
152
+ contents,
153
+ generationConfig: {
154
+ temperature: this.config.temperature,
155
+ maxOutputTokens: this.config.maxTokens || 4096
156
+ }
157
+ };
158
+ if (systemMessage) {
159
+ body.systemInstruction = {
160
+ parts: [{ text: systemMessage.content }]
161
+ };
162
+ }
163
+ const url = `${this.baseUrl}/models/${model}:generateContent?key=${this.apiKey}`;
164
+ const response = await fetch(url, {
165
+ method: "POST",
166
+ headers: {
167
+ "Content-Type": "application/json"
168
+ },
169
+ body: JSON.stringify(body)
170
+ });
171
+ if (!response.ok) {
172
+ const error = await response.text();
173
+ throw new Error(`Google AI API error: ${response.status} - ${error}`);
174
+ }
175
+ const data = await response.json();
176
+ const candidate = data.candidates?.[0];
177
+ if (!candidate?.content?.parts) {
178
+ throw new Error("No response from Google AI");
179
+ }
180
+ const content = candidate.content.parts.map((part) => part.text || "").join("");
181
+ const usageMetadata = data.usageMetadata;
182
+ return {
183
+ content,
184
+ usage: usageMetadata ? {
185
+ promptTokens: usageMetadata.promptTokenCount || 0,
186
+ completionTokens: usageMetadata.candidatesTokenCount || 0,
187
+ totalTokens: usageMetadata.totalTokenCount || 0
188
+ } : void 0
189
+ };
190
+ }
191
+ };
192
+
193
+ // src/llm/deepseek.ts
194
+ import OpenAI2 from "openai";
195
+ var DeepSeekAdapter = class extends BaseLLMAdapter {
196
+ client;
197
+ constructor(config) {
198
+ super(config);
199
+ if (!config.apiKey) {
200
+ throw new Error("DeepSeek API key required");
201
+ }
202
+ this.client = new OpenAI2({
203
+ apiKey: config.apiKey,
204
+ baseURL: config.endpoint || "https://api.deepseek.com/v1"
205
+ });
206
+ }
207
+ async chat(messages) {
208
+ try {
209
+ const response = await this.client.chat.completions.create({
210
+ model: this.config.model || "deepseek-chat",
211
+ messages: messages.map((m) => ({
212
+ role: m.role,
213
+ content: m.content
214
+ })),
215
+ temperature: this.config.temperature,
216
+ max_tokens: this.config.maxTokens
217
+ });
218
+ const choice = response.choices[0];
219
+ if (!choice || !choice.message) {
220
+ throw new Error("No response from DeepSeek");
221
+ }
222
+ return {
223
+ content: choice.message.content || "",
224
+ usage: response.usage ? {
225
+ promptTokens: response.usage.prompt_tokens,
226
+ completionTokens: response.usage.completion_tokens,
227
+ totalTokens: response.usage.total_tokens
228
+ } : void 0
229
+ };
230
+ } catch (error) {
231
+ if (error instanceof OpenAI2.APIError) {
232
+ throw new Error(`DeepSeek API error: ${error.message}`);
233
+ }
234
+ throw error;
235
+ }
236
+ }
237
+ };
238
+
239
+ // src/llm/mistral.ts
240
+ var MistralAdapter = class extends BaseLLMAdapter {
241
+ apiKey;
242
+ baseUrl;
243
+ constructor(config) {
244
+ super(config);
245
+ if (!config.apiKey) {
246
+ throw new Error("Mistral API key required");
247
+ }
248
+ this.apiKey = config.apiKey;
249
+ this.baseUrl = config.endpoint || "https://api.mistral.ai/v1";
250
+ }
251
+ async chat(messages) {
252
+ const body = {
253
+ model: this.config.model || "mistral-large-latest",
254
+ messages: messages.map((m) => ({
255
+ role: m.role,
256
+ content: m.content
257
+ })),
258
+ temperature: this.config.temperature,
259
+ max_tokens: this.config.maxTokens
260
+ };
261
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
262
+ method: "POST",
263
+ headers: {
264
+ "Content-Type": "application/json",
265
+ Authorization: `Bearer ${this.apiKey}`
266
+ },
267
+ body: JSON.stringify(body)
268
+ });
269
+ if (!response.ok) {
270
+ const error = await response.text();
271
+ throw new Error(`Mistral API error: ${response.status} - ${error}`);
272
+ }
273
+ const data = await response.json();
274
+ const choice = data.choices?.[0];
275
+ if (!choice || !choice.message) {
276
+ throw new Error("No response from Mistral");
277
+ }
278
+ return {
279
+ content: choice.message.content || "",
280
+ usage: data.usage ? {
281
+ promptTokens: data.usage.prompt_tokens,
282
+ completionTokens: data.usage.completion_tokens,
283
+ totalTokens: data.usage.total_tokens
284
+ } : void 0
285
+ };
286
+ }
287
+ };
288
+
289
+ // src/llm/groq.ts
290
+ import OpenAI3 from "openai";
291
+ var GroqAdapter = class extends BaseLLMAdapter {
292
+ client;
293
+ constructor(config) {
294
+ super(config);
295
+ if (!config.apiKey) {
296
+ throw new Error("Groq API key required");
297
+ }
298
+ this.client = new OpenAI3({
299
+ apiKey: config.apiKey,
300
+ baseURL: config.endpoint || "https://api.groq.com/openai/v1"
301
+ });
302
+ }
303
+ async chat(messages) {
304
+ try {
305
+ const response = await this.client.chat.completions.create({
306
+ model: this.config.model || "llama-3.1-70b-versatile",
307
+ messages: messages.map((m) => ({
308
+ role: m.role,
309
+ content: m.content
310
+ })),
311
+ temperature: this.config.temperature,
312
+ max_tokens: this.config.maxTokens
313
+ });
314
+ const choice = response.choices[0];
315
+ if (!choice || !choice.message) {
316
+ throw new Error("No response from Groq");
317
+ }
318
+ return {
319
+ content: choice.message.content || "",
320
+ usage: response.usage ? {
321
+ promptTokens: response.usage.prompt_tokens,
322
+ completionTokens: response.usage.completion_tokens,
323
+ totalTokens: response.usage.total_tokens
324
+ } : void 0
325
+ };
326
+ } catch (error) {
327
+ if (error instanceof OpenAI3.APIError) {
328
+ throw new Error(`Groq API error: ${error.message}`);
329
+ }
330
+ throw error;
331
+ }
332
+ }
333
+ };
334
+
335
+ // src/llm/together.ts
336
+ import OpenAI4 from "openai";
337
+ var TogetherAdapter = class extends BaseLLMAdapter {
338
+ client;
339
+ constructor(config) {
340
+ super(config);
341
+ if (!config.apiKey) {
342
+ throw new Error("Together AI API key required");
343
+ }
344
+ this.client = new OpenAI4({
345
+ apiKey: config.apiKey,
346
+ baseURL: config.endpoint || "https://api.together.xyz/v1"
347
+ });
348
+ }
349
+ async chat(messages) {
350
+ try {
351
+ const response = await this.client.chat.completions.create({
352
+ model: this.config.model || "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo",
353
+ messages: messages.map((m) => ({
354
+ role: m.role,
355
+ content: m.content
356
+ })),
357
+ temperature: this.config.temperature,
358
+ max_tokens: this.config.maxTokens
359
+ });
360
+ const choice = response.choices[0];
361
+ if (!choice || !choice.message) {
362
+ throw new Error("No response from Together AI");
363
+ }
364
+ return {
365
+ content: choice.message.content || "",
366
+ usage: response.usage ? {
367
+ promptTokens: response.usage.prompt_tokens,
368
+ completionTokens: response.usage.completion_tokens,
369
+ totalTokens: response.usage.total_tokens
370
+ } : void 0
371
+ };
372
+ } catch (error) {
373
+ if (error instanceof OpenAI4.APIError) {
374
+ throw new Error(`Together AI API error: ${error.message}`);
375
+ }
376
+ throw error;
377
+ }
378
+ }
379
+ };
380
+
381
+ // src/llm/ollama.ts
382
+ var OllamaAdapter = class extends BaseLLMAdapter {
383
+ baseUrl;
384
+ constructor(config) {
385
+ super(config);
386
+ this.baseUrl = config.endpoint || "http://localhost:11434";
387
+ }
388
+ /**
389
+ * Check if Ollama is running and the model is available
390
+ */
391
+ async healthCheck() {
392
+ try {
393
+ const response = await fetch(`${this.baseUrl}/api/tags`);
394
+ if (!response.ok) {
395
+ throw new Error("Ollama server not responding");
396
+ }
397
+ const data = await response.json();
398
+ const models = data.models?.map((m) => m.name) || [];
399
+ if (this.config.model && !models.some((m) => m.startsWith(this.config.model))) {
400
+ console.warn(
401
+ `Model "${this.config.model}" not found locally. Available: ${models.join(", ")}`
402
+ );
403
+ console.warn(`Run: ollama pull ${this.config.model}`);
404
+ }
405
+ } catch (error) {
406
+ throw new Error(
407
+ `Cannot connect to Ollama at ${this.baseUrl}. Make sure Ollama is running (ollama serve) or install it from https://ollama.com`
408
+ );
409
+ }
410
+ }
411
+ async chat(messages) {
412
+ const body = {
413
+ model: this.config.model || "llama3.2",
414
+ messages: messages.map((m) => ({
415
+ role: m.role,
416
+ content: m.content
417
+ })),
418
+ stream: false,
419
+ options: {
420
+ temperature: this.config.temperature,
421
+ num_predict: this.config.maxTokens
422
+ }
423
+ };
424
+ const response = await fetch(`${this.baseUrl}/api/chat`, {
425
+ method: "POST",
426
+ headers: {
427
+ "Content-Type": "application/json"
428
+ },
429
+ body: JSON.stringify(body)
430
+ });
431
+ if (!response.ok) {
432
+ const error = await response.text();
433
+ throw new Error(`Ollama API error: ${response.status} - ${error}`);
434
+ }
435
+ const data = await response.json();
436
+ return {
437
+ content: data.message?.content || "",
438
+ usage: data.eval_count ? {
439
+ promptTokens: data.prompt_eval_count || 0,
440
+ completionTokens: data.eval_count,
441
+ totalTokens: (data.prompt_eval_count || 0) + data.eval_count
442
+ } : void 0
443
+ };
444
+ }
445
+ getMetadata() {
446
+ return {
447
+ provider: "ollama",
448
+ model: this.config.model || "llama3.2",
449
+ isLocal: true
450
+ };
451
+ }
452
+ };
453
+
454
+ // src/llm/adapter.ts
455
+ async function createLLMAdapter(config) {
456
+ switch (config.provider) {
457
+ case "openai":
458
+ return new OpenAIAdapter(config);
459
+ case "anthropic":
460
+ return new AnthropicAdapter(config);
461
+ case "google":
462
+ return new GoogleAdapter(config);
463
+ case "deepseek":
464
+ return new DeepSeekAdapter(config);
465
+ case "mistral":
466
+ return new MistralAdapter(config);
467
+ case "groq":
468
+ return new GroqAdapter(config);
469
+ case "together":
470
+ return new TogetherAdapter(config);
471
+ case "ollama":
472
+ const adapter = new OllamaAdapter(config);
473
+ await adapter.healthCheck();
474
+ return adapter;
475
+ case "custom":
476
+ return new OpenAIAdapter({
477
+ ...config,
478
+ endpoint: config.endpoint
479
+ });
480
+ default:
481
+ throw new Error(`Unsupported LLM provider: ${config.provider}`);
482
+ }
483
+ }
484
+
485
+ // src/strategy/loader.ts
486
+ import { existsSync } from "fs";
487
+ import { join } from "path";
488
+ import { spawn } from "child_process";
489
+ async function loadStrategy(strategyPath) {
490
+ const basePath = strategyPath || process.env.EXAGENT_STRATEGY || "strategy";
491
+ const tsPath = basePath.endsWith(".ts") || basePath.endsWith(".js") ? basePath : `${basePath}.ts`;
492
+ const jsPath = basePath.endsWith(".ts") || basePath.endsWith(".js") ? basePath.replace(".ts", ".js") : `${basePath}.js`;
493
+ const fullTsPath = tsPath.startsWith("/") ? tsPath : join(process.cwd(), tsPath);
494
+ const fullJsPath = jsPath.startsWith("/") ? jsPath : join(process.cwd(), jsPath);
495
+ if (existsSync(fullTsPath) && fullTsPath.endsWith(".ts")) {
496
+ try {
497
+ const module = await loadTypeScriptModule(fullTsPath);
498
+ if (typeof module.generateSignals !== "function") {
499
+ throw new Error("Strategy must export a generateSignals function");
500
+ }
501
+ console.log(`Loaded custom strategy from ${tsPath}`);
502
+ return module.generateSignals;
503
+ } catch (error) {
504
+ console.error(`Failed to load strategy from ${tsPath}:`, error);
505
+ throw error;
506
+ }
507
+ }
508
+ if (existsSync(fullJsPath)) {
509
+ try {
510
+ const module = await import(fullJsPath);
511
+ if (typeof module.generateSignals !== "function") {
512
+ throw new Error("Strategy must export a generateSignals function");
513
+ }
514
+ console.log(`Loaded custom strategy from ${jsPath}`);
515
+ return module.generateSignals;
516
+ } catch (error) {
517
+ console.error(`Failed to load strategy from ${jsPath}:`, error);
518
+ throw error;
519
+ }
520
+ }
521
+ console.log("No custom strategy found, using default (hold) strategy");
522
+ return defaultStrategy;
523
+ }
524
+ async function loadTypeScriptModule(path2) {
525
+ try {
526
+ const tsxPath = __require.resolve("tsx");
527
+ const { pathToFileURL } = await import("url");
528
+ const result = await new Promise((resolve, reject) => {
529
+ const child = spawn(
530
+ process.execPath,
531
+ [
532
+ "--import",
533
+ "tsx/esm",
534
+ "-e",
535
+ `import('${pathToFileURL(path2).href}').then(m => console.log(JSON.stringify({ exports: Object.keys(m) }))).catch(e => console.error('ERROR:', e.message))`
536
+ ],
537
+ {
538
+ cwd: process.cwd(),
539
+ env: process.env,
540
+ stdio: ["pipe", "pipe", "pipe"]
541
+ }
542
+ );
543
+ let stdout = "";
544
+ let stderr = "";
545
+ child.stdout.on("data", (data) => stdout += data.toString());
546
+ child.stderr.on("data", (data) => stderr += data.toString());
547
+ child.on("close", (code) => {
548
+ if (code !== 0 || stderr.includes("ERROR:")) {
549
+ reject(new Error(`Failed to load TypeScript: ${stderr || "Unknown error"}`));
550
+ } else {
551
+ resolve(stdout);
552
+ }
553
+ });
554
+ });
555
+ const tsx = await import("tsx/esm/api");
556
+ const unregister = tsx.register();
557
+ try {
558
+ const module = await import(path2);
559
+ return module;
560
+ } finally {
561
+ unregister();
562
+ }
563
+ } catch (error) {
564
+ if (error.code === "MODULE_NOT_FOUND" || error.message.includes("Cannot find module")) {
565
+ throw new Error(
566
+ `Cannot load TypeScript strategy. Please either:
567
+ 1. Rename your strategy.ts to strategy.js (remove type annotations)
568
+ 2. Or compile it: npx tsc strategy.ts --outDir . --esModuleInterop
569
+ 3. Or install tsx: npm install tsx`
570
+ );
571
+ }
572
+ throw error;
573
+ }
574
+ }
575
+ var defaultStrategy = async (_marketData, _llm, _config) => {
576
+ return [];
577
+ };
578
+ function validateStrategy(fn) {
579
+ return typeof fn === "function";
580
+ }
581
+
582
+ // src/strategy/templates.ts
583
+ var STRATEGY_TEMPLATES = [
584
+ {
585
+ id: "momentum",
586
+ name: "Momentum Trader",
587
+ description: "Follows price trends and momentum indicators. Buys assets with strong upward momentum.",
588
+ riskLevel: "medium",
589
+ riskWarnings: [
590
+ "Momentum strategies can suffer significant losses during trend reversals",
591
+ "High volatility markets may generate false signals",
592
+ "Past performance does not guarantee future results",
593
+ "This strategy may underperform in sideways markets"
594
+ ],
595
+ systemPrompt: `You are an AI trading analyst specializing in momentum trading strategies.
596
+
597
+ Your role is to analyze market data and identify momentum-based trading opportunities.
598
+
599
+ IMPORTANT CONSTRAINTS:
600
+ - Only recommend trades when there is clear momentum evidence
601
+ - Always consider risk/reward ratios
602
+ - Never recommend more than the configured position size limits
603
+ - Be conservative with confidence scores
604
+
605
+ When analyzing data, look for:
606
+ 1. Price trends (higher highs, higher lows for uptrends)
607
+ 2. Volume confirmation (increasing volume on moves)
608
+ 3. Relative strength vs market benchmarks
609
+
610
+ Respond with JSON in this format:
611
+ {
612
+ "analysis": "Brief market analysis",
613
+ "signals": [
614
+ {
615
+ "action": "buy" | "sell" | "hold",
616
+ "tokenIn": "0x...",
617
+ "tokenOut": "0x...",
618
+ "percentage": 0-100,
619
+ "confidence": 0-1,
620
+ "reasoning": "Why this trade"
621
+ }
622
+ ]
623
+ }`,
624
+ exampleCode: `import { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig } from '@exagent/agent';
625
+
626
+ export const generateSignals: StrategyFunction = async (
627
+ marketData: MarketData,
628
+ llm: LLMAdapter,
629
+ config: AgentConfig
630
+ ): Promise<TradeSignal[]> => {
631
+ const response = await llm.chat([
632
+ { role: 'system', content: MOMENTUM_SYSTEM_PROMPT },
633
+ { role: 'user', content: JSON.stringify({
634
+ prices: marketData.prices,
635
+ balances: formatBalances(marketData.balances),
636
+ portfolioValue: marketData.portfolioValue,
637
+ })}
638
+ ]);
639
+
640
+ // Parse LLM response and convert to TradeSignals
641
+ const parsed = JSON.parse(response.content);
642
+ return parsed.signals.map(convertToTradeSignal);
643
+ };`
644
+ },
645
+ {
646
+ id: "value",
647
+ name: "Value Investor",
648
+ description: "Looks for undervalued assets based on fundamentals. Takes long-term positions.",
649
+ riskLevel: "low",
650
+ riskWarnings: [
651
+ "Value traps can result in prolonged losses",
652
+ "Requires patience - may underperform for extended periods",
653
+ "Fundamental analysis may not apply well to all crypto assets",
654
+ "Market sentiment can override fundamentals for long periods"
655
+ ],
656
+ systemPrompt: `You are an AI trading analyst specializing in value investing.
657
+
658
+ Your role is to identify undervalued assets with strong fundamentals.
659
+
660
+ IMPORTANT CONSTRAINTS:
661
+ - Focus on long-term value, not short-term price movements
662
+ - Only recommend assets with clear value propositions
663
+ - Consider protocol revenue, TVL, active users, developer activity
664
+ - Be very selective - quality over quantity
665
+
666
+ When analyzing, consider:
667
+ 1. Protocol fundamentals (revenue, TVL, user growth)
668
+ 2. Token economics (supply schedule, utility)
669
+ 3. Competitive positioning
670
+ 4. Valuation relative to peers
671
+
672
+ Respond with JSON in this format:
673
+ {
674
+ "analysis": "Brief fundamental analysis",
675
+ "signals": [
676
+ {
677
+ "action": "buy" | "sell" | "hold",
678
+ "tokenIn": "0x...",
679
+ "tokenOut": "0x...",
680
+ "percentage": 0-100,
681
+ "confidence": 0-1,
682
+ "reasoning": "Fundamental thesis"
683
+ }
684
+ ]
685
+ }`,
686
+ exampleCode: `import { StrategyFunction } from '@exagent/agent';
687
+
688
+ export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
689
+ // Value strategy runs less frequently
690
+ const response = await llm.chat([
691
+ { role: 'system', content: VALUE_SYSTEM_PROMPT },
692
+ { role: 'user', content: JSON.stringify(marketData) }
693
+ ]);
694
+
695
+ return parseSignals(response.content);
696
+ };`
697
+ },
698
+ {
699
+ id: "arbitrage",
700
+ name: "Arbitrage Hunter",
701
+ description: "Looks for price discrepancies across DEXs. Requires fast execution.",
702
+ riskLevel: "high",
703
+ riskWarnings: [
704
+ "Arbitrage opportunities are highly competitive - professional bots dominate",
705
+ "Slippage and gas costs can eliminate profits",
706
+ "MEV bots may front-run your transactions",
707
+ "Requires very fast execution and may not be profitable with standard infrastructure",
708
+ "This strategy is generally NOT recommended for beginners"
709
+ ],
710
+ systemPrompt: `You are an AI trading analyst specializing in arbitrage detection.
711
+
712
+ Your role is to identify price discrepancies that may offer arbitrage opportunities.
713
+
714
+ IMPORTANT CONSTRAINTS:
715
+ - Account for gas costs in all calculations
716
+ - Account for slippage (assume 0.3% minimum)
717
+ - Only flag opportunities with >1% net profit potential
718
+ - Consider MEV risk - assume some profit extraction
719
+
720
+ This is an advanced strategy with high competition.
721
+
722
+ Respond with JSON in this format:
723
+ {
724
+ "opportunities": [
725
+ {
726
+ "description": "What the arbitrage is",
727
+ "expectedProfit": "Net profit after costs",
728
+ "confidence": 0-1,
729
+ "warning": "Risks specific to this opportunity"
730
+ }
731
+ ]
732
+ }`,
733
+ exampleCode: `// Note: Pure arbitrage requires specialized infrastructure
734
+ // This template is for educational purposes
735
+
736
+ import { StrategyFunction } from '@exagent/agent';
737
+
738
+ export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
739
+ // Arbitrage requires real-time price feeds from multiple sources
740
+ // Standard LLM-based analysis is too slow for most arbitrage
741
+ console.warn('Arbitrage strategy requires specialized infrastructure');
742
+ return [];
743
+ };`
744
+ },
745
+ {
746
+ id: "custom",
747
+ name: "Custom Strategy",
748
+ description: "Build your own strategy from scratch. Full control over logic and prompts.",
749
+ riskLevel: "extreme",
750
+ riskWarnings: [
751
+ "Custom strategies have no guardrails - you are fully responsible",
752
+ "LLMs can hallucinate or make errors - always validate outputs",
753
+ "Start with small amounts before scaling up",
754
+ "Consider edge cases: what happens if the LLM returns invalid JSON?",
755
+ "Your prompts and strategy logic are your competitive advantage - protect them",
756
+ "Agents may not behave exactly as expected based on your prompts"
757
+ ],
758
+ systemPrompt: `// Define your own system prompt here
759
+
760
+ You are a trading AI. Analyze the market data and provide trading signals.
761
+
762
+ // Add your specific instructions, constraints, and output format.
763
+
764
+ Respond with JSON:
765
+ {
766
+ "signals": []
767
+ }`,
768
+ exampleCode: `import { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig } from '@exagent/agent';
769
+
770
+ /**
771
+ * Custom Strategy Template
772
+ *
773
+ * Customize this file with your own trading logic and prompts.
774
+ * Your prompts are YOUR intellectual property - we don't store them.
775
+ */
776
+ export const generateSignals: StrategyFunction = async (
777
+ marketData: MarketData,
778
+ llm: LLMAdapter,
779
+ config: AgentConfig
780
+ ): Promise<TradeSignal[]> => {
781
+ // Your custom system prompt (this is your secret sauce)
782
+ const systemPrompt = \`
783
+ Your custom instructions here...
784
+ \`;
785
+
786
+ // Call the LLM with your prompt
787
+ const response = await llm.chat([
788
+ { role: 'system', content: systemPrompt },
789
+ { role: 'user', content: JSON.stringify(marketData) }
790
+ ]);
791
+
792
+ // Parse and return signals
793
+ // IMPORTANT: Validate LLM output before using
794
+ try {
795
+ const parsed = JSON.parse(response.content);
796
+ return parsed.signals || [];
797
+ } catch (e) {
798
+ console.error('Failed to parse LLM response:', e);
799
+ return []; // Safe fallback: no trades
800
+ }
801
+ };`
802
+ }
803
+ ];
804
+ function getStrategyTemplate(id) {
805
+ return STRATEGY_TEMPLATES.find((t) => t.id === id);
806
+ }
807
+ function getAllStrategyTemplates() {
808
+ return STRATEGY_TEMPLATES;
809
+ }
810
+
811
+ // src/types.ts
812
+ import { z } from "zod";
813
+ var WalletSetupSchema = z.enum(["generate", "provide"]);
814
+ var LLMProviderSchema = z.enum(["openai", "anthropic", "google", "deepseek", "mistral", "groq", "together", "ollama", "custom"]);
815
+ var LLMConfigSchema = z.object({
816
+ provider: LLMProviderSchema,
817
+ model: z.string().optional(),
818
+ apiKey: z.string().optional(),
819
+ endpoint: z.string().url().optional(),
820
+ temperature: z.number().min(0).max(2).default(0.7),
821
+ maxTokens: z.number().positive().default(4096)
822
+ });
823
+ var RiskUniverseSchema = z.enum(["core", "established", "derivatives", "emerging", "frontier"]);
824
+ var TradingConfigSchema = z.object({
825
+ timeHorizon: z.enum(["intraday", "swing", "position"]).default("swing"),
826
+ maxPositionSizeBps: z.number().min(100).max(1e4).default(1e3),
827
+ // 1-100%
828
+ maxDailyLossBps: z.number().min(0).max(1e4).default(500),
829
+ // 0-100%
830
+ maxConcurrentPositions: z.number().min(1).max(100).default(5),
831
+ tradingIntervalMs: z.number().min(1e3).default(6e4),
832
+ // minimum 1 second
833
+ maxSlippageBps: z.number().min(10).max(1e3).default(100),
834
+ // 0.1-10%, default 1%
835
+ minTradeValueUSD: z.number().min(0).default(1)
836
+ // minimum trade value in USD
837
+ });
838
+ var VaultPolicySchema = z.enum([
839
+ "disabled",
840
+ // Never create a vault - trade with agent's own capital only
841
+ "manual"
842
+ // Only create vault when explicitly directed by owner
843
+ ]);
844
+ var VaultConfigSchema = z.object({
845
+ // Policy for vault creation (asked during deployment)
846
+ policy: VaultPolicySchema.default("manual"),
847
+ // Default vault name (auto-generated from agent name if not set)
848
+ defaultName: z.string().optional(),
849
+ // Default vault symbol (auto-generated if not set)
850
+ defaultSymbol: z.string().optional(),
851
+ // Fee recipient for vault fees (default: agent wallet)
852
+ feeRecipient: z.string().optional(),
853
+ // When vault exists, trade through vault instead of direct trading
854
+ // This pools depositors' capital with the agent's trades
855
+ preferVaultTrading: z.boolean().default(true)
856
+ });
857
+ var WalletConfigSchema = z.object({
858
+ setup: WalletSetupSchema.default("provide")
859
+ }).optional();
860
+ var RelayConfigSchema = z.object({
861
+ enabled: z.boolean().default(false),
862
+ apiUrl: z.string().url(),
863
+ heartbeatIntervalMs: z.number().min(5e3).default(3e4)
864
+ }).optional();
865
+ var PerpConfigSchema = z.object({
866
+ /** Enable perp trading */
867
+ enabled: z.boolean().default(false),
868
+ /** Hyperliquid REST API URL */
869
+ apiUrl: z.string().url().default("https://api.hyperliquid.xyz"),
870
+ /** Hyperliquid WebSocket URL */
871
+ wsUrl: z.string().default("wss://api.hyperliquid.xyz/ws"),
872
+ /** Builder address for fee collection (must have >= 100 USDC on HL) */
873
+ builderAddress: z.string(),
874
+ /** Builder fee in tenths of basis points (100 = 10 bps = 0.10%) */
875
+ builderFeeTenthsBps: z.number().min(0).max(500).default(100),
876
+ /** Private key for the perp relayer (calls recordPerpTrade on Base). Falls back to agent wallet. */
877
+ perpRelayerKey: z.string().optional(),
878
+ /** Maximum leverage per position (default: 10) */
879
+ maxLeverage: z.number().min(1).max(50).default(10),
880
+ /** Maximum notional position size in USD (default: 50000) */
881
+ maxNotionalUSD: z.number().min(100).default(5e4),
882
+ /** Allowed perp instruments (e.g. ["ETH", "BTC", "SOL"]). If empty, all instruments allowed. */
883
+ allowedInstruments: z.array(z.string()).optional()
884
+ }).optional();
885
+ var AgentConfigSchema = z.object({
886
+ // Identity (from on-chain registration)
887
+ agentId: z.union([z.number().positive(), z.string()]),
888
+ name: z.string().min(3).max(32),
889
+ // Network
890
+ network: z.literal("mainnet").default("mainnet"),
891
+ // Wallet setup preference
892
+ wallet: WalletConfigSchema,
893
+ // LLM
894
+ llm: LLMConfigSchema,
895
+ // Trading parameters
896
+ riskUniverse: RiskUniverseSchema.default("established"),
897
+ trading: TradingConfigSchema.default({}),
898
+ // Vault configuration (copy trading)
899
+ vault: VaultConfigSchema.default({}),
900
+ // Relay configuration (command center)
901
+ relay: RelayConfigSchema,
902
+ // Perp trading configuration (Hyperliquid)
903
+ perp: PerpConfigSchema,
904
+ // Allowed tokens (addresses)
905
+ allowedTokens: z.array(z.string()).optional()
906
+ });
907
+
908
+ // src/config.ts
909
+ import { readFileSync, existsSync as existsSync2 } from "fs";
910
+ import { join as join2 } from "path";
911
+ import { config as loadEnv } from "dotenv";
912
+ function loadConfig(configPath) {
913
+ loadEnv();
914
+ const configFile = configPath || process.env.EXAGENT_CONFIG || "agent-config.json";
915
+ const fullPath = configFile.startsWith("/") ? configFile : join2(process.cwd(), configFile);
916
+ if (!existsSync2(fullPath)) {
917
+ throw new Error(`Config file not found: ${fullPath}`);
918
+ }
919
+ const rawConfig = JSON.parse(readFileSync(fullPath, "utf-8"));
920
+ const config = AgentConfigSchema.parse(rawConfig);
921
+ const privateKey = process.env.EXAGENT_PRIVATE_KEY;
922
+ if (privateKey && (!privateKey.startsWith("0x") || privateKey.length !== 66)) {
923
+ throw new Error("EXAGENT_PRIVATE_KEY must be a valid 32-byte hex string starting with 0x");
924
+ }
925
+ const llmConfig = { ...config.llm };
926
+ if (process.env.OPENAI_API_KEY && config.llm.provider === "openai") {
927
+ llmConfig.apiKey = process.env.OPENAI_API_KEY;
928
+ }
929
+ if (process.env.ANTHROPIC_API_KEY && config.llm.provider === "anthropic") {
930
+ llmConfig.apiKey = process.env.ANTHROPIC_API_KEY;
931
+ }
932
+ if (process.env.GOOGLE_AI_API_KEY && config.llm.provider === "google") {
933
+ llmConfig.apiKey = process.env.GOOGLE_AI_API_KEY;
934
+ }
935
+ if (process.env.DEEPSEEK_API_KEY && config.llm.provider === "deepseek") {
936
+ llmConfig.apiKey = process.env.DEEPSEEK_API_KEY;
937
+ }
938
+ if (process.env.MISTRAL_API_KEY && config.llm.provider === "mistral") {
939
+ llmConfig.apiKey = process.env.MISTRAL_API_KEY;
940
+ }
941
+ if (process.env.GROQ_API_KEY && config.llm.provider === "groq") {
942
+ llmConfig.apiKey = process.env.GROQ_API_KEY;
943
+ }
944
+ if (process.env.TOGETHER_API_KEY && config.llm.provider === "together") {
945
+ llmConfig.apiKey = process.env.TOGETHER_API_KEY;
946
+ }
947
+ if (process.env.EXAGENT_LLM_URL) {
948
+ llmConfig.endpoint = process.env.EXAGENT_LLM_URL;
949
+ }
950
+ if (process.env.EXAGENT_LLM_MODEL) {
951
+ llmConfig.model = process.env.EXAGENT_LLM_MODEL;
952
+ }
953
+ const network = process.env.EXAGENT_NETWORK || config.network;
954
+ return {
955
+ ...config,
956
+ llm: llmConfig,
957
+ network,
958
+ privateKey: privateKey || ""
959
+ };
960
+ }
961
+ function validateConfig(config) {
962
+ if (!config.privateKey) {
963
+ throw new Error("Private key is required");
964
+ }
965
+ if (config.llm.provider !== "ollama" && !config.llm.apiKey) {
966
+ throw new Error(`API key required for ${config.llm.provider} provider`);
967
+ }
968
+ if (config.llm.provider === "ollama" && !config.llm.endpoint) {
969
+ config.llm.endpoint = "http://localhost:11434";
970
+ }
971
+ if (config.llm.provider === "custom" && !config.llm.endpoint) {
972
+ throw new Error("Endpoint required for custom LLM provider");
973
+ }
974
+ if (!config.agentId || Number(config.agentId) <= 0) {
975
+ throw new Error("Valid agent ID required");
976
+ }
977
+ }
978
+ var DEFAULT_RPC_URL = "https://base-rpc.publicnode.com";
979
+ function getRpcUrl() {
980
+ return process.env.BASE_RPC_URL || process.env.EXAGENT_RPC_URL || DEFAULT_RPC_URL;
981
+ }
982
+ function createSampleConfig(agentId, name) {
983
+ return {
984
+ agentId,
985
+ name,
986
+ network: "mainnet",
987
+ llm: {
988
+ provider: "openai",
989
+ model: "gpt-4.1",
990
+ temperature: 0.7,
991
+ maxTokens: 4096
992
+ },
993
+ riskUniverse: "established",
994
+ trading: {
995
+ timeHorizon: "swing",
996
+ maxPositionSizeBps: 1e3,
997
+ maxDailyLossBps: 500,
998
+ maxConcurrentPositions: 5,
999
+ tradingIntervalMs: 6e4,
1000
+ maxSlippageBps: 100,
1001
+ minTradeValueUSD: 1
1002
+ },
1003
+ vault: {
1004
+ // Default to manual - user must explicitly enable auto-creation
1005
+ policy: "manual",
1006
+ // Will use agent name for vault name if not set
1007
+ preferVaultTrading: true
1008
+ }
1009
+ };
1010
+ }
1011
+
1012
+ // src/trading/executor.ts
1013
+ var TradeExecutor = class {
1014
+ client;
1015
+ config;
1016
+ allowedTokens;
1017
+ configHashFn;
1018
+ constructor(client, config, configHashFn) {
1019
+ this.client = client;
1020
+ this.config = config;
1021
+ this.configHashFn = configHashFn;
1022
+ this.allowedTokens = new Set(
1023
+ (config.allowedTokens || []).map((t) => t.toLowerCase())
1024
+ );
1025
+ }
1026
+ /**
1027
+ * Execute a single trade signal
1028
+ */
1029
+ async execute(signal) {
1030
+ if (signal.action === "hold") {
1031
+ return { success: true };
1032
+ }
1033
+ try {
1034
+ console.log(`Executing ${signal.action}: ${signal.tokenIn} -> ${signal.tokenOut}`);
1035
+ console.log(`Amount: ${signal.amountIn.toString()}, Confidence: ${signal.confidence}`);
1036
+ if (!this.validateSignal(signal)) {
1037
+ return { success: false, error: "Signal exceeds position limits" };
1038
+ }
1039
+ const configHash = this.configHashFn?.();
1040
+ const result = await this.client.trade({
1041
+ tokenIn: signal.tokenIn,
1042
+ tokenOut: signal.tokenOut,
1043
+ amountIn: signal.amountIn,
1044
+ maxSlippageBps: this.config.trading?.maxSlippageBps ?? 100,
1045
+ ...configHash && { configHash }
1046
+ });
1047
+ console.log(`Trade executed: ${result.hash}`);
1048
+ return { success: true, txHash: result.hash };
1049
+ } catch (error) {
1050
+ const message = error instanceof Error ? error.message : "Unknown error";
1051
+ console.error(`Trade failed: ${message}`);
1052
+ return { success: false, error: message };
1053
+ }
1054
+ }
1055
+ /**
1056
+ * Execute multiple trade signals
1057
+ * Returns results for each signal
1058
+ */
1059
+ async executeAll(signals) {
1060
+ const results = [];
1061
+ for (const signal of signals) {
1062
+ const result = await this.execute(signal);
1063
+ results.push({ signal, ...result });
1064
+ if (signals.indexOf(signal) < signals.length - 1) {
1065
+ await this.delay(1e3);
1066
+ }
1067
+ }
1068
+ return results;
1069
+ }
1070
+ /**
1071
+ * Validate a signal against config limits and token restrictions
1072
+ */
1073
+ validateSignal(signal) {
1074
+ if (signal.confidence < 0.5) {
1075
+ console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
1076
+ return false;
1077
+ }
1078
+ if (this.allowedTokens.size > 0) {
1079
+ const tokenInAllowed = this.allowedTokens.has(signal.tokenIn.toLowerCase());
1080
+ const tokenOutAllowed = this.allowedTokens.has(signal.tokenOut.toLowerCase());
1081
+ if (!tokenInAllowed) {
1082
+ console.warn(`Token ${signal.tokenIn} not in allowed list for this agent's risk universe \u2014 skipping`);
1083
+ return false;
1084
+ }
1085
+ if (!tokenOutAllowed) {
1086
+ console.warn(`Token ${signal.tokenOut} not in allowed list for this agent's risk universe \u2014 skipping`);
1087
+ return false;
1088
+ }
1089
+ }
1090
+ return true;
1091
+ }
1092
+ delay(ms) {
1093
+ return new Promise((resolve) => setTimeout(resolve, ms));
1094
+ }
1095
+ };
1096
+
1097
+ // src/trading/market.ts
1098
+ import { createPublicClient, http, erc20Abi } from "viem";
1099
+ var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
1100
+ var TOKEN_DECIMALS = {
1101
+ // Base Mainnet — Core tokens
1102
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
1103
+ // USDC
1104
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
1105
+ // USDbC
1106
+ "0x4200000000000000000000000000000000000006": 18,
1107
+ // WETH
1108
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
1109
+ // DAI
1110
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
1111
+ // cbETH
1112
+ [NATIVE_ETH.toLowerCase()]: 18,
1113
+ // Native ETH
1114
+ // Base Mainnet — Established tokens
1115
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
1116
+ // AERO (Aerodrome)
1117
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
1118
+ // BRETT
1119
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
1120
+ // DEGEN
1121
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
1122
+ // VIRTUAL
1123
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
1124
+ // TOSHI
1125
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
1126
+ // cbBTC
1127
+ "0x2416092f143378750bb29b79ed961ab195cceea5": 18,
1128
+ // ezETH (Renzo)
1129
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
1130
+ // wstETH (Lido)
1131
+ };
1132
+ function getTokenDecimals(address) {
1133
+ const decimals = TOKEN_DECIMALS[address.toLowerCase()];
1134
+ if (decimals === void 0) {
1135
+ console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
1136
+ return 18;
1137
+ }
1138
+ return decimals;
1139
+ }
1140
+ var TOKEN_TO_COINGECKO = {
1141
+ // Core
1142
+ "0x4200000000000000000000000000000000000006": "ethereum",
1143
+ // WETH
1144
+ [NATIVE_ETH.toLowerCase()]: "ethereum",
1145
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
1146
+ // USDC
1147
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
1148
+ // USDbC
1149
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
1150
+ // cbETH
1151
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
1152
+ // DAI
1153
+ // Established
1154
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
1155
+ // AERO
1156
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
1157
+ // BRETT
1158
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
1159
+ // DEGEN
1160
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
1161
+ // VIRTUAL
1162
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
1163
+ // TOSHI
1164
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
1165
+ // cbBTC
1166
+ "0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
1167
+ // ezETH
1168
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
1169
+ // wstETH
1170
+ };
1171
+ var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
1172
+ var PRICE_STALENESS_MS = 6e4;
1173
+ var MarketDataService = class {
1174
+ rpcUrl;
1175
+ client;
1176
+ /** Cached prices from last fetch */
1177
+ cachedPrices = {};
1178
+ /** Timestamp of last successful price fetch */
1179
+ lastPriceFetchAt = 0;
1180
+ constructor(rpcUrl) {
1181
+ this.rpcUrl = rpcUrl;
1182
+ this.client = createPublicClient({
1183
+ transport: http(rpcUrl)
1184
+ });
1185
+ }
1186
+ /** Cached volume data */
1187
+ cachedVolume24h = {};
1188
+ /** Cached price change data */
1189
+ cachedPriceChange24h = {};
1190
+ /**
1191
+ * Fetch current market data for the agent
1192
+ */
1193
+ async fetchMarketData(walletAddress, tokenAddresses) {
1194
+ const prices = await this.fetchPrices(tokenAddresses);
1195
+ const balances = await this.fetchBalances(walletAddress, tokenAddresses);
1196
+ const portfolioValue = this.calculatePortfolioValue(balances, prices);
1197
+ let gasPrice;
1198
+ try {
1199
+ gasPrice = await this.client.getGasPrice();
1200
+ } catch {
1201
+ }
1202
+ return {
1203
+ timestamp: Date.now(),
1204
+ prices,
1205
+ balances,
1206
+ portfolioValue,
1207
+ volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
1208
+ priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
1209
+ gasPrice,
1210
+ network: {
1211
+ chainId: this.client.chain?.id ?? 8453
1212
+ }
1213
+ };
1214
+ }
1215
+ /**
1216
+ * Check if cached prices are still fresh
1217
+ */
1218
+ get pricesAreFresh() {
1219
+ return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
1220
+ }
1221
+ /**
1222
+ * Fetch token prices from CoinGecko free API
1223
+ * Returns cached prices if still fresh (<60s old)
1224
+ */
1225
+ async fetchPrices(tokenAddresses) {
1226
+ if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
1227
+ const prices2 = { ...this.cachedPrices };
1228
+ for (const addr of tokenAddresses) {
1229
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1230
+ if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
1231
+ prices2[addr.toLowerCase()] = 1;
1232
+ }
1233
+ }
1234
+ return prices2;
1235
+ }
1236
+ const prices = {};
1237
+ const idsToFetch = /* @__PURE__ */ new Set();
1238
+ for (const addr of tokenAddresses) {
1239
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1240
+ if (cgId && !STABLECOIN_IDS.has(cgId)) {
1241
+ idsToFetch.add(cgId);
1242
+ }
1243
+ }
1244
+ idsToFetch.add("ethereum");
1245
+ if (idsToFetch.size > 0) {
1246
+ try {
1247
+ const ids = Array.from(idsToFetch).join(",");
1248
+ const response = await fetch(
1249
+ `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
1250
+ { signal: AbortSignal.timeout(5e3) }
1251
+ );
1252
+ if (response.ok) {
1253
+ const data = await response.json();
1254
+ for (const [cgId, priceData] of Object.entries(data)) {
1255
+ for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
1256
+ if (id === cgId) {
1257
+ const key = addr.toLowerCase();
1258
+ prices[key] = priceData.usd;
1259
+ if (priceData.usd_24h_vol !== void 0) {
1260
+ this.cachedVolume24h[key] = priceData.usd_24h_vol;
1261
+ }
1262
+ if (priceData.usd_24h_change !== void 0) {
1263
+ this.cachedPriceChange24h[key] = priceData.usd_24h_change;
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+ this.lastPriceFetchAt = Date.now();
1269
+ } else {
1270
+ console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
1271
+ }
1272
+ } catch (error) {
1273
+ console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
1274
+ }
1275
+ }
1276
+ for (const addr of tokenAddresses) {
1277
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1278
+ if (cgId && STABLECOIN_IDS.has(cgId)) {
1279
+ prices[addr.toLowerCase()] = 1;
1280
+ }
1281
+ }
1282
+ const missingAddrs = tokenAddresses.filter(
1283
+ (addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
1284
+ );
1285
+ if (missingAddrs.length > 0) {
1286
+ try {
1287
+ const coins = missingAddrs.map((a) => `base:${a}`).join(",");
1288
+ const llamaResponse = await fetch(
1289
+ `https://coins.llama.fi/prices/current/${coins}`,
1290
+ { signal: AbortSignal.timeout(5e3) }
1291
+ );
1292
+ if (llamaResponse.ok) {
1293
+ const llamaData = await llamaResponse.json();
1294
+ for (const [key, data] of Object.entries(llamaData.coins)) {
1295
+ const addr = key.replace("base:", "").toLowerCase();
1296
+ if (data.price && data.confidence > 0.5) {
1297
+ prices[addr] = data.price;
1298
+ }
1299
+ }
1300
+ if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
1301
+ }
1302
+ } catch {
1303
+ }
1304
+ }
1305
+ if (Object.keys(prices).length > 0) {
1306
+ this.cachedPrices = prices;
1307
+ }
1308
+ if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
1309
+ console.warn("Using cached prices (last successful fetch was stale)");
1310
+ return { ...this.cachedPrices };
1311
+ }
1312
+ for (const addr of tokenAddresses) {
1313
+ if (!prices[addr.toLowerCase()]) {
1314
+ console.warn(`No price available for ${addr}, using 0`);
1315
+ prices[addr.toLowerCase()] = 0;
1316
+ }
1317
+ }
1318
+ return prices;
1319
+ }
1320
+ /**
1321
+ * Fetch real on-chain balances: native ETH + ERC-20 tokens
1322
+ */
1323
+ async fetchBalances(walletAddress, tokenAddresses) {
1324
+ const balances = {};
1325
+ const wallet = walletAddress;
1326
+ try {
1327
+ const nativeBalance = await this.client.getBalance({ address: wallet });
1328
+ balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
1329
+ const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
1330
+ try {
1331
+ const balance = await this.client.readContract({
1332
+ address: tokenAddress,
1333
+ abi: erc20Abi,
1334
+ functionName: "balanceOf",
1335
+ args: [wallet]
1336
+ });
1337
+ return { address: tokenAddress.toLowerCase(), balance };
1338
+ } catch (error) {
1339
+ return { address: tokenAddress.toLowerCase(), balance: 0n };
1340
+ }
1341
+ });
1342
+ const results = await Promise.all(erc20Promises);
1343
+ for (const { address, balance } of results) {
1344
+ balances[address] = balance;
1345
+ }
1346
+ } catch (error) {
1347
+ console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
1348
+ balances[NATIVE_ETH.toLowerCase()] = 0n;
1349
+ for (const address of tokenAddresses) {
1350
+ balances[address.toLowerCase()] = 0n;
1351
+ }
1352
+ }
1353
+ return balances;
1354
+ }
1355
+ /**
1356
+ * Calculate total portfolio value in USD
1357
+ */
1358
+ calculatePortfolioValue(balances, prices) {
1359
+ let total = 0;
1360
+ for (const [address, balance] of Object.entries(balances)) {
1361
+ const price = prices[address.toLowerCase()] || 0;
1362
+ const decimals = getTokenDecimals(address);
1363
+ const amount = Number(balance) / Math.pow(10, decimals);
1364
+ total += amount * price;
1365
+ }
1366
+ return total;
1367
+ }
1368
+ };
1369
+
1370
+ // src/trading/risk.ts
1371
+ var RiskManager = class {
1372
+ config;
1373
+ dailyPnL = 0;
1374
+ dailyFees = 0;
1375
+ lastResetDate = "";
1376
+ /** Minimum trade value in USD — trades below this are rejected as dust */
1377
+ minTradeValueUSD;
1378
+ constructor(config) {
1379
+ this.config = config;
1380
+ this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
1381
+ }
1382
+ /**
1383
+ * Filter signals through risk checks
1384
+ * Returns only signals that pass all guardrails
1385
+ */
1386
+ filterSignals(signals, marketData) {
1387
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1388
+ if (today !== this.lastResetDate) {
1389
+ this.dailyPnL = 0;
1390
+ this.dailyFees = 0;
1391
+ this.lastResetDate = today;
1392
+ }
1393
+ if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
1394
+ console.warn("Daily loss limit reached - no new trades");
1395
+ return [];
1396
+ }
1397
+ return signals.filter((signal) => this.validateSignal(signal, marketData));
1398
+ }
1399
+ /**
1400
+ * Validate individual signal against risk limits
1401
+ */
1402
+ validateSignal(signal, marketData) {
1403
+ if (signal.action === "hold") {
1404
+ return true;
1405
+ }
1406
+ const signalValue = this.estimateSignalValue(signal, marketData);
1407
+ const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
1408
+ if (signalValue > maxPositionValue) {
1409
+ console.warn(
1410
+ `Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
1411
+ );
1412
+ return false;
1413
+ }
1414
+ if (signal.confidence < 0.5) {
1415
+ console.warn(`Signal confidence too low: ${signal.confidence}`);
1416
+ return false;
1417
+ }
1418
+ if (signal.action === "buy" && this.config.maxConcurrentPositions) {
1419
+ const activePositions = this.countActivePositions(marketData);
1420
+ if (activePositions >= this.config.maxConcurrentPositions) {
1421
+ console.warn(
1422
+ `Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
1423
+ );
1424
+ return false;
1425
+ }
1426
+ }
1427
+ if (signalValue < this.minTradeValueUSD) {
1428
+ console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
1429
+ return false;
1430
+ }
1431
+ return true;
1432
+ }
1433
+ /**
1434
+ * Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
1435
+ */
1436
+ countActivePositions(marketData) {
1437
+ const NATIVE_ETH_KEY = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
1438
+ let count = 0;
1439
+ for (const [address, balance] of Object.entries(marketData.balances)) {
1440
+ if (address.toLowerCase() === NATIVE_ETH_KEY) continue;
1441
+ if (balance > 0n) count++;
1442
+ }
1443
+ return count;
1444
+ }
1445
+ /**
1446
+ * Check if daily loss limit has been hit
1447
+ */
1448
+ isDailyLossLimitHit(portfolioValue) {
1449
+ const maxLoss = portfolioValue * this.config.maxDailyLossBps / 1e4;
1450
+ return this.dailyPnL < -maxLoss;
1451
+ }
1452
+ /**
1453
+ * Estimate USD value of a trade signal
1454
+ */
1455
+ estimateSignalValue(signal, marketData) {
1456
+ const price = marketData.prices[signal.tokenIn.toLowerCase()] || 0;
1457
+ const tokenDecimals = getTokenDecimals(signal.tokenIn);
1458
+ const amount = Number(signal.amountIn) / Math.pow(10, tokenDecimals);
1459
+ return amount * price;
1460
+ }
1461
+ /**
1462
+ * Update daily PnL after a trade (market gains/losses only)
1463
+ */
1464
+ updatePnL(pnl) {
1465
+ this.dailyPnL += pnl;
1466
+ }
1467
+ /**
1468
+ * Update daily fees (trading fees, gas costs, etc.)
1469
+ * Fees are tracked separately and do NOT count toward the daily loss limit.
1470
+ * This prevents protocol fees from triggering circuit breakers.
1471
+ */
1472
+ updateFees(fees) {
1473
+ this.dailyFees += fees;
1474
+ }
1475
+ /**
1476
+ * Get current risk status
1477
+ * @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
1478
+ */
1479
+ getStatus(portfolioValue) {
1480
+ const pv = portfolioValue || 0;
1481
+ const maxLossUSD = pv * this.config.maxDailyLossBps / 1e4;
1482
+ return {
1483
+ dailyPnL: this.dailyPnL,
1484
+ dailyFees: this.dailyFees,
1485
+ dailyNetPnL: this.dailyPnL - this.dailyFees,
1486
+ dailyLossLimit: maxLossUSD,
1487
+ // Only market PnL triggers the limit — fees are excluded
1488
+ isLimitHit: pv > 0 ? this.dailyPnL < -maxLossUSD : false
1489
+ };
1490
+ }
1491
+ // ============================================================
1492
+ // PERP RISK FILTERING
1493
+ // ============================================================
1494
+ /**
1495
+ * Filter perp trade signals through risk checks.
1496
+ * Reduce-only signals (closes) always pass.
1497
+ *
1498
+ * @param signals - Raw perp signals from strategy
1499
+ * @param positions - Current open positions on Hyperliquid
1500
+ * @param account - Current account equity and margin
1501
+ * @param maxLeverage - Maximum allowed leverage
1502
+ * @param maxNotionalUSD - Maximum notional per position
1503
+ * @returns Signals that pass risk checks
1504
+ */
1505
+ filterPerpSignals(signals, positions, account, maxLeverage, maxNotionalUSD) {
1506
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1507
+ if (today !== this.lastResetDate) {
1508
+ this.dailyPnL = 0;
1509
+ this.dailyFees = 0;
1510
+ this.lastResetDate = today;
1511
+ }
1512
+ if (this.isDailyLossLimitHit(account.totalEquity)) {
1513
+ console.warn("Daily loss limit reached \u2014 blocking new perp trades");
1514
+ return signals.filter((s) => s.reduceOnly);
1515
+ }
1516
+ return signals.filter((signal) => this.validatePerpSignal(signal, positions, account, maxLeverage, maxNotionalUSD));
1517
+ }
1518
+ /**
1519
+ * Validate an individual perp signal.
1520
+ */
1521
+ validatePerpSignal(signal, positions, account, maxLeverage, maxNotionalUSD) {
1522
+ if (signal.action === "hold") {
1523
+ return true;
1524
+ }
1525
+ if (signal.reduceOnly || signal.action === "close_long" || signal.action === "close_short") {
1526
+ return true;
1527
+ }
1528
+ if (signal.confidence < 0.5) {
1529
+ console.warn(`Perp signal confidence too low: ${signal.confidence} for ${signal.instrument}`);
1530
+ return false;
1531
+ }
1532
+ if (signal.leverage > maxLeverage) {
1533
+ console.warn(`Perp signal leverage ${signal.leverage}x exceeds max ${maxLeverage}x for ${signal.instrument}`);
1534
+ return false;
1535
+ }
1536
+ const signalNotional = signal.size * signal.price;
1537
+ if (signalNotional > maxNotionalUSD) {
1538
+ console.warn(`Perp signal notional $${signalNotional.toFixed(0)} exceeds max $${maxNotionalUSD} for ${signal.instrument}`);
1539
+ return false;
1540
+ }
1541
+ const currentNotional = account.totalNotional;
1542
+ const projectedNotional = currentNotional + signalNotional;
1543
+ const projectedLeverage = account.totalEquity > 0 ? projectedNotional / account.totalEquity : 0;
1544
+ if (projectedLeverage > maxLeverage) {
1545
+ console.warn(
1546
+ `Perp signal would push aggregate leverage to ${projectedLeverage.toFixed(1)}x (max: ${maxLeverage}x) \u2014 blocked`
1547
+ );
1548
+ return false;
1549
+ }
1550
+ const existingPos = positions.find((p) => p.instrument === signal.instrument);
1551
+ if (existingPos) {
1552
+ const liqProximity = this.calculateLiquidationProximity(existingPos);
1553
+ if (liqProximity > 0.7) {
1554
+ console.warn(
1555
+ `Position ${signal.instrument} liquidation proximity ${(liqProximity * 100).toFixed(0)}% \u2014 blocking new entry`
1556
+ );
1557
+ return false;
1558
+ }
1559
+ }
1560
+ const requiredMargin = signalNotional / signal.leverage;
1561
+ if (requiredMargin > account.availableMargin) {
1562
+ console.warn(
1563
+ `Insufficient margin for ${signal.instrument}: need $${requiredMargin.toFixed(0)}, have $${account.availableMargin.toFixed(0)}`
1564
+ );
1565
+ return false;
1566
+ }
1567
+ return true;
1568
+ }
1569
+ /**
1570
+ * Calculate liquidation proximity for a position (0.0 = safe, 1.0 = liquidated).
1571
+ */
1572
+ calculateLiquidationProximity(pos) {
1573
+ if (pos.liquidationPrice <= 0 || pos.markPrice <= 0) return 0;
1574
+ if (pos.size > 0) {
1575
+ if (pos.markPrice <= pos.liquidationPrice) return 1;
1576
+ const distance = pos.markPrice - pos.liquidationPrice;
1577
+ const total = pos.entryPrice - pos.liquidationPrice;
1578
+ return total > 0 ? 1 - distance / total : 0;
1579
+ } else {
1580
+ if (pos.markPrice >= pos.liquidationPrice) return 1;
1581
+ const distance = pos.liquidationPrice - pos.markPrice;
1582
+ const total = pos.liquidationPrice - pos.entryPrice;
1583
+ return total > 0 ? 1 - distance / total : 0;
1584
+ }
1585
+ }
1586
+ };
1587
+
1588
+ // src/vault/manager.ts
1589
+ import { createPublicClient as createPublicClient2, createWalletClient, http as http2 } from "viem";
1590
+ import { privateKeyToAccount } from "viem/accounts";
1591
+ import { base } from "viem/chains";
1592
+ var ADDRESSES = {
1593
+ mainnet: {
1594
+ vaultFactory: process.env.EXAGENT_VAULT_FACTORY_ADDRESS || "0x0000000000000000000000000000000000000000",
1595
+ registry: process.env.EXAGENT_REGISTRY_ADDRESS || "0x0000000000000000000000000000000000000000",
1596
+ usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
1597
+ }
1598
+ };
1599
+ var VAULT_FACTORY_ABI = [
1600
+ {
1601
+ type: "function",
1602
+ name: "vaults",
1603
+ inputs: [{ name: "agentId", type: "uint256" }, { name: "asset", type: "address" }],
1604
+ outputs: [{ type: "address" }],
1605
+ stateMutability: "view"
1606
+ },
1607
+ {
1608
+ type: "function",
1609
+ name: "canCreateVault",
1610
+ inputs: [{ name: "creator", type: "address" }],
1611
+ outputs: [{ name: "canCreate", type: "bool" }, { name: "reason", type: "string" }],
1612
+ stateMutability: "view"
1613
+ },
1614
+ {
1615
+ type: "function",
1616
+ name: "createVault",
1617
+ inputs: [
1618
+ { name: "agentId", type: "uint256" },
1619
+ { name: "asset", type: "address" },
1620
+ { name: "seedAmount", type: "uint256" },
1621
+ { name: "name", type: "string" },
1622
+ { name: "symbol", type: "string" },
1623
+ { name: "feeRecipient", type: "address" }
1624
+ ],
1625
+ outputs: [{ type: "address" }],
1626
+ stateMutability: "nonpayable"
1627
+ },
1628
+ {
1629
+ type: "function",
1630
+ name: "minimumVeEXARequired",
1631
+ inputs: [],
1632
+ outputs: [{ type: "uint256" }],
1633
+ stateMutability: "view"
1634
+ }
1635
+ ];
1636
+ var VAULT_ABI = [
1637
+ {
1638
+ type: "function",
1639
+ name: "totalAssets",
1640
+ inputs: [],
1641
+ outputs: [{ type: "uint256" }],
1642
+ stateMutability: "view"
1643
+ },
1644
+ {
1645
+ type: "function",
1646
+ name: "executeTrade",
1647
+ inputs: [
1648
+ { name: "tokenIn", type: "address" },
1649
+ { name: "tokenOut", type: "address" },
1650
+ { name: "amountIn", type: "uint256" },
1651
+ { name: "minAmountOut", type: "uint256" },
1652
+ { name: "aggregator", type: "address" },
1653
+ { name: "swapData", type: "bytes" },
1654
+ { name: "deadline", type: "uint256" }
1655
+ ],
1656
+ outputs: [{ type: "uint256" }],
1657
+ stateMutability: "nonpayable"
1658
+ }
1659
+ ];
1660
+ var VaultManager = class {
1661
+ config;
1662
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1663
+ publicClient;
1664
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1665
+ walletClient;
1666
+ addresses;
1667
+ account;
1668
+ chain;
1669
+ cachedVaultAddress = null;
1670
+ lastVaultCheck = 0;
1671
+ VAULT_CACHE_TTL = 6e4;
1672
+ // 1 minute
1673
+ enabled = true;
1674
+ constructor(config) {
1675
+ this.config = config;
1676
+ this.addresses = ADDRESSES[config.network];
1677
+ this.account = privateKeyToAccount(config.walletKey);
1678
+ this.chain = base;
1679
+ const rpcUrl = getRpcUrl();
1680
+ this.publicClient = createPublicClient2({
1681
+ chain: this.chain,
1682
+ transport: http2(rpcUrl)
1683
+ });
1684
+ this.walletClient = createWalletClient({
1685
+ account: this.account,
1686
+ chain: this.chain,
1687
+ transport: http2(rpcUrl)
1688
+ });
1689
+ if (this.addresses.vaultFactory === "0x0000000000000000000000000000000000000000") {
1690
+ console.warn("VaultFactory address is zero \u2014 vault operations will be disabled");
1691
+ this.enabled = false;
1692
+ }
1693
+ }
1694
+ /**
1695
+ * Get the agent's vault policy
1696
+ */
1697
+ get policy() {
1698
+ return this.config.vaultConfig.policy;
1699
+ }
1700
+ /**
1701
+ * Check if vault trading is preferred when a vault exists
1702
+ */
1703
+ get preferVaultTrading() {
1704
+ return this.config.vaultConfig.preferVaultTrading;
1705
+ }
1706
+ /**
1707
+ * Get comprehensive vault status
1708
+ */
1709
+ async getVaultStatus() {
1710
+ if (!this.enabled) {
1711
+ return {
1712
+ hasVault: false,
1713
+ vaultAddress: null,
1714
+ totalAssets: BigInt(0),
1715
+ canCreateVault: false,
1716
+ cannotCreateReason: "Vault operations disabled (contract address not set)",
1717
+ requirementsMet: false,
1718
+ requirements: { veXARequired: BigInt(0), isBypassed: false }
1719
+ };
1720
+ }
1721
+ const vaultAddress = await this.getVaultAddress();
1722
+ const hasVault = vaultAddress !== null;
1723
+ let totalAssets = BigInt(0);
1724
+ if (hasVault && vaultAddress) {
1725
+ try {
1726
+ totalAssets = await this.publicClient.readContract({
1727
+ address: vaultAddress,
1728
+ abi: VAULT_ABI,
1729
+ functionName: "totalAssets"
1730
+ });
1731
+ } catch {
1732
+ }
1733
+ }
1734
+ const [canCreateResult, requirements] = await Promise.all([
1735
+ this.publicClient.readContract({
1736
+ address: this.addresses.vaultFactory,
1737
+ abi: VAULT_FACTORY_ABI,
1738
+ functionName: "canCreateVault",
1739
+ args: [this.account.address]
1740
+ }),
1741
+ this.getRequirements()
1742
+ ]);
1743
+ return {
1744
+ hasVault,
1745
+ vaultAddress,
1746
+ totalAssets,
1747
+ canCreateVault: canCreateResult[0],
1748
+ cannotCreateReason: canCreateResult[0] ? null : canCreateResult[1],
1749
+ requirementsMet: canCreateResult[0] || requirements.isBypassed,
1750
+ requirements
1751
+ };
1752
+ }
1753
+ /**
1754
+ * Get vault creation requirements
1755
+ * Note: No burnFee on mainnet — vault creation requires USDC seed instead
1756
+ */
1757
+ async getRequirements() {
1758
+ const veXARequired = await this.publicClient.readContract({
1759
+ address: this.addresses.vaultFactory,
1760
+ abi: VAULT_FACTORY_ABI,
1761
+ functionName: "minimumVeEXARequired"
1762
+ });
1763
+ const isBypassed = veXARequired === BigInt(0);
1764
+ return { veXARequired, isBypassed };
1765
+ }
1766
+ /**
1767
+ * Get the agent's vault address (cached)
1768
+ */
1769
+ async getVaultAddress() {
1770
+ const now = Date.now();
1771
+ if (this.cachedVaultAddress && now - this.lastVaultCheck < this.VAULT_CACHE_TTL) {
1772
+ return this.cachedVaultAddress;
1773
+ }
1774
+ const vaultAddress = await this.publicClient.readContract({
1775
+ address: this.addresses.vaultFactory,
1776
+ abi: VAULT_FACTORY_ABI,
1777
+ functionName: "vaults",
1778
+ args: [this.config.agentId, this.addresses.usdc]
1779
+ });
1780
+ this.lastVaultCheck = now;
1781
+ if (vaultAddress === "0x0000000000000000000000000000000000000000") {
1782
+ this.cachedVaultAddress = null;
1783
+ return null;
1784
+ }
1785
+ this.cachedVaultAddress = vaultAddress;
1786
+ return vaultAddress;
1787
+ }
1788
+ /**
1789
+ * Create a vault for the agent
1790
+ * @param seedAmount - USDC seed amount in raw units (default: 100e6 = 100 USDC)
1791
+ * @returns Vault address if successful
1792
+ */
1793
+ async createVault(seedAmount) {
1794
+ if (!this.enabled) {
1795
+ return { success: false, error: "Vault operations disabled (contract address not set)" };
1796
+ }
1797
+ if (this.policy === "disabled") {
1798
+ return { success: false, error: "Vault creation disabled by policy" };
1799
+ }
1800
+ const existingVault = await this.getVaultAddress();
1801
+ if (existingVault) {
1802
+ return { success: false, error: "Vault already exists", vaultAddress: existingVault };
1803
+ }
1804
+ const status = await this.getVaultStatus();
1805
+ if (!status.canCreateVault) {
1806
+ return { success: false, error: status.cannotCreateReason || "Requirements not met" };
1807
+ }
1808
+ const seed = seedAmount || BigInt(1e8);
1809
+ const vaultName = this.config.vaultConfig.defaultName || `${this.config.agentName} Trading Vault`;
1810
+ const vaultSymbol = this.config.vaultConfig.defaultSymbol || `ex${this.config.agentName.replace(/[^a-zA-Z]/g, "").slice(0, 4).toUpperCase()}`;
1811
+ const feeRecipient = this.config.vaultConfig.feeRecipient || this.account.address;
1812
+ try {
1813
+ const hash = await this.walletClient.writeContract({
1814
+ address: this.addresses.vaultFactory,
1815
+ abi: VAULT_FACTORY_ABI,
1816
+ functionName: "createVault",
1817
+ args: [
1818
+ this.config.agentId,
1819
+ this.addresses.usdc,
1820
+ seed,
1821
+ vaultName,
1822
+ vaultSymbol,
1823
+ feeRecipient
1824
+ ],
1825
+ chain: this.chain,
1826
+ account: this.account
1827
+ });
1828
+ const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
1829
+ if (receipt.status !== "success") {
1830
+ return { success: false, error: "Transaction failed", txHash: hash };
1831
+ }
1832
+ const vaultAddress = await this.getVaultAddress();
1833
+ this.cachedVaultAddress = vaultAddress;
1834
+ return {
1835
+ success: true,
1836
+ vaultAddress,
1837
+ txHash: hash
1838
+ };
1839
+ } catch (error) {
1840
+ return {
1841
+ success: false,
1842
+ error: error instanceof Error ? error.message : "Unknown error"
1843
+ };
1844
+ }
1845
+ }
1846
+ /**
1847
+ * Execute a trade through the vault (if it exists and policy allows)
1848
+ * Returns null if should use direct trading instead
1849
+ */
1850
+ async executeVaultTrade(params) {
1851
+ if (!this.preferVaultTrading) {
1852
+ return null;
1853
+ }
1854
+ const vaultAddress = await this.getVaultAddress();
1855
+ if (!vaultAddress) {
1856
+ return null;
1857
+ }
1858
+ const deadline = params.deadline || BigInt(Math.floor(Date.now() / 1e3) + 3600);
1859
+ try {
1860
+ const hash = await this.walletClient.writeContract({
1861
+ address: vaultAddress,
1862
+ abi: VAULT_ABI,
1863
+ functionName: "executeTrade",
1864
+ args: [
1865
+ params.tokenIn,
1866
+ params.tokenOut,
1867
+ params.amountIn,
1868
+ params.minAmountOut,
1869
+ params.aggregator,
1870
+ params.swapData,
1871
+ deadline
1872
+ ],
1873
+ chain: this.chain,
1874
+ account: this.account
1875
+ });
1876
+ return { usedVault: true, txHash: hash };
1877
+ } catch (error) {
1878
+ return {
1879
+ usedVault: true,
1880
+ error: error instanceof Error ? error.message : "Vault trade failed"
1881
+ };
1882
+ }
1883
+ }
1884
+ };
1885
+
1886
+ // src/relay.ts
1887
+ import WebSocket from "ws";
1888
+ import { privateKeyToAccount as privateKeyToAccount2, signMessage } from "viem/accounts";
1889
+ import { SDK_VERSION } from "@exagent/sdk";
1890
+ var RelayClient = class {
1891
+ config;
1892
+ ws = null;
1893
+ authenticated = false;
1894
+ authRejected = false;
1895
+ reconnectAttempts = 0;
1896
+ maxReconnectAttempts = 50;
1897
+ reconnectTimer = null;
1898
+ heartbeatTimer = null;
1899
+ stopped = false;
1900
+ constructor(config) {
1901
+ this.config = config;
1902
+ }
1903
+ /**
1904
+ * Connect to the relay server
1905
+ */
1906
+ async connect() {
1907
+ if (this.stopped) return;
1908
+ const wsUrl = this.config.relay.apiUrl.replace(/^https?:\/\//, (m) => m.includes("https") ? "wss://" : "ws://").replace(/\/$/, "") + "/ws/agent";
1909
+ return new Promise((resolve, reject) => {
1910
+ try {
1911
+ this.ws = new WebSocket(wsUrl);
1912
+ } catch (error) {
1913
+ console.error("Relay: Failed to create WebSocket:", error);
1914
+ this.scheduleReconnect();
1915
+ reject(error);
1916
+ return;
1917
+ }
1918
+ const connectTimeout = setTimeout(() => {
1919
+ if (!this.authenticated) {
1920
+ console.error("Relay: Connection timeout");
1921
+ this.ws?.close();
1922
+ this.scheduleReconnect();
1923
+ reject(new Error("Connection timeout"));
1924
+ }
1925
+ }, 15e3);
1926
+ this.ws.on("open", async () => {
1927
+ this.authRejected = false;
1928
+ console.log("Relay: Connected, authenticating...");
1929
+ try {
1930
+ await this.authenticate();
1931
+ } catch (error) {
1932
+ console.error("Relay: Authentication failed:", error);
1933
+ this.ws?.close();
1934
+ clearTimeout(connectTimeout);
1935
+ reject(error);
1936
+ }
1937
+ });
1938
+ this.ws.on("message", (raw) => {
1939
+ try {
1940
+ const data = JSON.parse(raw.toString());
1941
+ this.handleMessage(data);
1942
+ if (data.type === "auth_success") {
1943
+ clearTimeout(connectTimeout);
1944
+ this.authenticated = true;
1945
+ this.reconnectAttempts = 0;
1946
+ this.startHeartbeat();
1947
+ console.log("Relay: Authenticated successfully");
1948
+ resolve();
1949
+ } else if (data.type === "auth_error") {
1950
+ clearTimeout(connectTimeout);
1951
+ this.authRejected = true;
1952
+ console.error(`Relay: Auth rejected: ${data.message}`);
1953
+ reject(new Error(data.message));
1954
+ }
1955
+ } catch {
1956
+ }
1957
+ });
1958
+ this.ws.on("close", (code, reason) => {
1959
+ clearTimeout(connectTimeout);
1960
+ this.authenticated = false;
1961
+ this.stopHeartbeat();
1962
+ if (!this.stopped) {
1963
+ if (!this.authRejected) {
1964
+ console.log(`Relay: Disconnected (${code}: ${reason.toString() || "unknown"})`);
1965
+ }
1966
+ this.scheduleReconnect();
1967
+ }
1968
+ });
1969
+ this.ws.on("error", (error) => {
1970
+ if (!this.stopped) {
1971
+ console.error("Relay: WebSocket error:", error.message);
1972
+ }
1973
+ });
1974
+ });
1975
+ }
1976
+ /**
1977
+ * Authenticate with the relay server using wallet signature
1978
+ */
1979
+ async authenticate() {
1980
+ const account = privateKeyToAccount2(this.config.privateKey);
1981
+ const timestamp = Math.floor(Date.now() / 1e3);
1982
+ const message = `ExagentRelay:${this.config.agentId}:${timestamp}`;
1983
+ const signature = await signMessage({
1984
+ message,
1985
+ privateKey: this.config.privateKey
1986
+ });
1987
+ this.send({
1988
+ type: "auth",
1989
+ agentId: this.config.agentId,
1990
+ wallet: account.address,
1991
+ timestamp,
1992
+ signature,
1993
+ sdkVersion: SDK_VERSION
1994
+ });
1995
+ }
1996
+ /**
1997
+ * Handle incoming messages from the relay server
1998
+ */
1999
+ handleMessage(data) {
2000
+ switch (data.type) {
2001
+ case "command":
2002
+ if (data.command && this.config.onCommand) {
2003
+ this.config.onCommand(data.command);
2004
+ }
2005
+ break;
2006
+ case "auth_success":
2007
+ case "auth_error":
2008
+ break;
2009
+ case "error":
2010
+ console.error(`Relay: Server error: ${data.message}`);
2011
+ break;
2012
+ }
2013
+ }
2014
+ /**
2015
+ * Send a status heartbeat
2016
+ */
2017
+ sendHeartbeat(status) {
2018
+ if (!this.authenticated) return;
2019
+ this.send({
2020
+ type: "heartbeat",
2021
+ agentId: this.config.agentId,
2022
+ status
2023
+ });
2024
+ }
2025
+ /**
2026
+ * Send a status update (outside of regular heartbeat)
2027
+ */
2028
+ sendStatusUpdate(status) {
2029
+ if (!this.authenticated) return;
2030
+ this.send({
2031
+ type: "status_update",
2032
+ agentId: this.config.agentId,
2033
+ status
2034
+ });
2035
+ }
2036
+ /**
2037
+ * Send a message to the command center
2038
+ */
2039
+ sendMessage(messageType, level, title, body, data) {
2040
+ if (!this.authenticated) return;
2041
+ this.send({
2042
+ type: "message",
2043
+ agentId: this.config.agentId,
2044
+ messageType,
2045
+ level,
2046
+ title,
2047
+ body,
2048
+ data
2049
+ });
2050
+ }
2051
+ /**
2052
+ * Send a command execution result
2053
+ */
2054
+ sendCommandResult(commandId, success, result) {
2055
+ if (!this.authenticated) return;
2056
+ this.send({
2057
+ type: "command_result",
2058
+ agentId: this.config.agentId,
2059
+ commandId,
2060
+ success,
2061
+ result
2062
+ });
2063
+ }
2064
+ /**
2065
+ * Start the heartbeat timer
2066
+ */
2067
+ startHeartbeat() {
2068
+ this.stopHeartbeat();
2069
+ const interval = this.config.relay.heartbeatIntervalMs || 3e4;
2070
+ this.heartbeatTimer = setInterval(() => {
2071
+ if (this.ws?.readyState === WebSocket.OPEN) {
2072
+ this.ws.ping();
2073
+ }
2074
+ }, interval);
2075
+ }
2076
+ /**
2077
+ * Stop the heartbeat timer
2078
+ */
2079
+ stopHeartbeat() {
2080
+ if (this.heartbeatTimer) {
2081
+ clearInterval(this.heartbeatTimer);
2082
+ this.heartbeatTimer = null;
2083
+ }
2084
+ }
2085
+ /**
2086
+ * Schedule a reconnection with exponential backoff
2087
+ */
2088
+ scheduleReconnect() {
2089
+ if (this.stopped) return;
2090
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
2091
+ console.error("Relay: Max reconnection attempts reached. Giving up.");
2092
+ return;
2093
+ }
2094
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
2095
+ this.reconnectAttempts++;
2096
+ console.log(
2097
+ `Relay: Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
2098
+ );
2099
+ this.reconnectTimer = setTimeout(() => {
2100
+ this.connect().catch(() => {
2101
+ });
2102
+ }, delay);
2103
+ }
2104
+ /**
2105
+ * Send a JSON message to the WebSocket
2106
+ */
2107
+ send(data) {
2108
+ if (this.ws?.readyState === WebSocket.OPEN) {
2109
+ this.ws.send(JSON.stringify(data));
2110
+ }
2111
+ }
2112
+ /**
2113
+ * Check if connected and authenticated
2114
+ */
2115
+ get isConnected() {
2116
+ return this.authenticated && this.ws?.readyState === WebSocket.OPEN;
2117
+ }
2118
+ /**
2119
+ * Disconnect and stop reconnecting
2120
+ */
2121
+ disconnect() {
2122
+ this.stopped = true;
2123
+ this.stopHeartbeat();
2124
+ if (this.reconnectTimer) {
2125
+ clearTimeout(this.reconnectTimer);
2126
+ this.reconnectTimer = null;
2127
+ }
2128
+ if (this.ws) {
2129
+ this.ws.close(1e3, "Agent shutting down");
2130
+ this.ws = null;
2131
+ }
2132
+ this.authenticated = false;
2133
+ console.log("Relay: Disconnected");
2134
+ }
2135
+ };
2136
+
2137
+ // src/perp/client.ts
2138
+ var HyperliquidClient = class {
2139
+ apiUrl;
2140
+ meta = null;
2141
+ assetIndexCache = /* @__PURE__ */ new Map();
2142
+ constructor(config) {
2143
+ this.apiUrl = config.apiUrl;
2144
+ }
2145
+ // ============================================================
2146
+ // INFO API (read-only)
2147
+ // ============================================================
2148
+ /** Fetch perpetuals metadata (asset specs, names, indices) */
2149
+ async getMeta() {
2150
+ if (this.meta) return this.meta;
2151
+ const resp = await this.infoRequest({ type: "meta" });
2152
+ this.meta = resp.universe;
2153
+ this.meta.forEach((asset, idx) => {
2154
+ this.assetIndexCache.set(asset.name, idx);
2155
+ });
2156
+ return this.meta;
2157
+ }
2158
+ /** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
2159
+ async getAssetIndex(coin) {
2160
+ if (this.assetIndexCache.has(coin)) return this.assetIndexCache.get(coin);
2161
+ await this.getMeta();
2162
+ const idx = this.assetIndexCache.get(coin);
2163
+ if (idx === void 0) throw new Error(`Unknown instrument: ${coin}`);
2164
+ return idx;
2165
+ }
2166
+ /** Get mid-market prices for all perpetuals */
2167
+ async getAllMids() {
2168
+ return this.infoRequest({ type: "allMids" });
2169
+ }
2170
+ /** Get clearinghouse state (positions, margin, equity) for a user */
2171
+ async getClearinghouseState(user) {
2172
+ return this.infoRequest({ type: "clearinghouseState", user });
2173
+ }
2174
+ /** Get user's recent fills */
2175
+ async getUserFills(user, startTime) {
2176
+ return this.infoRequest({
2177
+ type: "userFills",
2178
+ user,
2179
+ ...startTime !== void 0 && { startTime }
2180
+ });
2181
+ }
2182
+ /** Get user's fills in a time range */
2183
+ async getUserFillsByTime(user, startTime, endTime) {
2184
+ return this.infoRequest({
2185
+ type: "userFillsByTime",
2186
+ user,
2187
+ startTime,
2188
+ ...endTime !== void 0 && { endTime }
2189
+ });
2190
+ }
2191
+ /** Get user's open orders */
2192
+ async getOpenOrders(user) {
2193
+ return this.infoRequest({ type: "openOrders", user });
2194
+ }
2195
+ /** Get user's funding history */
2196
+ async getUserFunding(user, startTime) {
2197
+ return this.infoRequest({ type: "userFunding", user, startTime });
2198
+ }
2199
+ /** Check max approved builder fee for a user */
2200
+ async getMaxBuilderFee(user, builder) {
2201
+ const resp = await this.infoRequest({ type: "maxBuilderFee", user, builder });
2202
+ return resp;
2203
+ }
2204
+ /** Get L2 order book for a coin */
2205
+ async getL2Book(coin, depth) {
2206
+ return this.infoRequest({
2207
+ type: "l2Book",
2208
+ coin,
2209
+ ...depth !== void 0 && { nSigFigs: depth }
2210
+ });
2211
+ }
2212
+ // ============================================================
2213
+ // HIGH-LEVEL HELPERS
2214
+ // ============================================================
2215
+ /** Get parsed positions for a user address */
2216
+ async getPositions(user) {
2217
+ const state = await this.getClearinghouseState(user);
2218
+ return state.assetPositions.filter((p) => parseFloat(p.position.szi) !== 0).map((p) => this.parsePosition(p));
2219
+ }
2220
+ /** Get account summary (equity, margin, leverage) */
2221
+ async getAccountSummary(user) {
2222
+ const state = await this.getClearinghouseState(user);
2223
+ const crossMarginSummary = state.crossMarginSummary;
2224
+ const totalEquity = parseFloat(crossMarginSummary.accountValue);
2225
+ const totalNotional = parseFloat(crossMarginSummary.totalNtlPos);
2226
+ const totalMarginUsed = parseFloat(crossMarginSummary.totalMarginUsed);
2227
+ return {
2228
+ totalEquity,
2229
+ availableMargin: totalEquity - totalMarginUsed,
2230
+ totalMarginUsed,
2231
+ totalUnrealizedPnl: parseFloat(crossMarginSummary.totalRawUsd) - totalEquity,
2232
+ totalNotional,
2233
+ maintenanceMargin: totalMarginUsed * 0.5,
2234
+ // Approximate
2235
+ effectiveLeverage: totalEquity > 0 ? totalNotional / totalEquity : 0,
2236
+ cashBalance: parseFloat(state.crossMarginSummary.accountValue) - state.assetPositions.reduce(
2237
+ (sum, p) => sum + parseFloat(p.position.unrealizedPnl),
2238
+ 0
2239
+ )
2240
+ };
2241
+ }
2242
+ /** Get market data for a list of instruments */
2243
+ async getMarketData(instruments) {
2244
+ const mids = await this.getAllMids();
2245
+ const meta = await this.getMeta();
2246
+ return instruments.filter((inst) => mids[inst] !== void 0).map((inst) => {
2247
+ const midPrice = parseFloat(mids[inst]);
2248
+ const assetMeta = meta.find((m) => m.name === inst);
2249
+ return {
2250
+ instrument: inst,
2251
+ midPrice,
2252
+ bestBid: midPrice,
2253
+ // Approximate — use L2 book for exact
2254
+ bestAsk: midPrice,
2255
+ funding8h: 0,
2256
+ // Would need separate funding API call
2257
+ openInterest: 0,
2258
+ // Would need separate meta call
2259
+ volume24h: 0,
2260
+ priceChange24h: 0
2261
+ };
2262
+ });
2263
+ }
2264
+ /** Convert fills to our PerpFill type */
2265
+ parseFill(fill) {
2266
+ return {
2267
+ oid: fill.oid,
2268
+ coin: fill.coin,
2269
+ side: fill.side,
2270
+ px: fill.px,
2271
+ sz: fill.sz,
2272
+ fee: fill.fee,
2273
+ time: fill.time,
2274
+ hash: fill.hash,
2275
+ isMaker: fill.startPosition !== fill.px,
2276
+ // Approximate maker detection
2277
+ builderFee: fill.builderFee,
2278
+ liquidation: fill.liquidation
2279
+ };
2280
+ }
2281
+ // ============================================================
2282
+ // PRIVATE HELPERS
2283
+ // ============================================================
2284
+ parsePosition(ap) {
2285
+ const pos = ap.position;
2286
+ const size = parseFloat(pos.szi);
2287
+ const entryPrice = parseFloat(pos.entryPx || "0");
2288
+ const markPrice = parseFloat(pos.positionValue || "0") / Math.abs(size || 1);
2289
+ return {
2290
+ instrument: pos.coin,
2291
+ assetIndex: this.assetIndexCache.get(pos.coin) ?? -1,
2292
+ size,
2293
+ entryPrice,
2294
+ markPrice,
2295
+ unrealizedPnl: parseFloat(pos.unrealizedPnl),
2296
+ leverage: parseFloat(pos.leverage?.value || "1"),
2297
+ marginType: pos.leverage?.type === "isolated" ? "isolated" : "cross",
2298
+ liquidationPrice: parseFloat(pos.liquidationPx || "0"),
2299
+ notionalUSD: Math.abs(size) * markPrice,
2300
+ marginUsed: parseFloat(pos.marginUsed)
2301
+ };
2302
+ }
2303
+ async infoRequest(body) {
2304
+ const resp = await fetch(`${this.apiUrl}/info`, {
2305
+ method: "POST",
2306
+ headers: { "Content-Type": "application/json" },
2307
+ body: JSON.stringify(body)
2308
+ });
2309
+ if (!resp.ok) {
2310
+ throw new Error(`Hyperliquid Info API error: ${resp.status} ${await resp.text()}`);
2311
+ }
2312
+ return resp.json();
2313
+ }
2314
+ };
2315
+
2316
+ // src/perp/signer.ts
2317
+ import { keccak256, encodePacked } from "viem";
2318
+ var HYPERLIQUID_DOMAIN = {
2319
+ name: "HyperliquidSignTransaction",
2320
+ version: "1",
2321
+ chainId: 42161n,
2322
+ // Always Arbitrum chain ID
2323
+ verifyingContract: "0x0000000000000000000000000000000000000000"
2324
+ };
2325
+ var HYPERLIQUID_TYPES = {
2326
+ HyperliquidTransaction: [
2327
+ { name: "hyperliquidChain", type: "string" },
2328
+ { name: "action", type: "string" },
2329
+ { name: "nonce", type: "uint64" }
2330
+ ]
2331
+ };
2332
+ var lastNonce = 0n;
2333
+ function getNextNonce() {
2334
+ const now = BigInt(Date.now());
2335
+ if (now <= lastNonce) {
2336
+ lastNonce = lastNonce + 1n;
2337
+ } else {
2338
+ lastNonce = now;
2339
+ }
2340
+ return lastNonce;
2341
+ }
2342
+ var HyperliquidSigner = class {
2343
+ constructor(walletClient) {
2344
+ this.walletClient = walletClient;
2345
+ }
2346
+ /**
2347
+ * Sign an exchange action (order, cancel, etc.)
2348
+ *
2349
+ * @param action - The action object (will be JSON-serialized)
2350
+ * @param nonce - Nonce (defaults to current timestamp)
2351
+ * @returns Signature hex string
2352
+ */
2353
+ async signAction(action, nonce) {
2354
+ const actionNonce = nonce ?? getNextNonce();
2355
+ const actionStr = JSON.stringify(action);
2356
+ const account = this.walletClient.account;
2357
+ if (!account) throw new Error("Wallet client has no account");
2358
+ const signature = await this.walletClient.signTypedData({
2359
+ account,
2360
+ domain: HYPERLIQUID_DOMAIN,
2361
+ types: HYPERLIQUID_TYPES,
2362
+ primaryType: "HyperliquidTransaction",
2363
+ message: {
2364
+ hyperliquidChain: "Mainnet",
2365
+ action: actionStr,
2366
+ nonce: actionNonce
2367
+ }
2368
+ });
2369
+ return { signature, nonce: actionNonce };
2370
+ }
2371
+ /**
2372
+ * Sign a user-level approval action (approve builder fee, approve agent).
2373
+ * These use the same EIP-712 structure but with different action payloads.
2374
+ */
2375
+ async signApproval(action, nonce) {
2376
+ return this.signAction(action, nonce);
2377
+ }
2378
+ /**
2379
+ * Get the signer's address
2380
+ */
2381
+ getAddress() {
2382
+ const account = this.walletClient.account;
2383
+ if (!account) throw new Error("Wallet client has no account");
2384
+ return account.address;
2385
+ }
2386
+ };
2387
+ function fillHashToBytes32(fillHash) {
2388
+ if (fillHash.startsWith("0x") && fillHash.length === 66) {
2389
+ return fillHash;
2390
+ }
2391
+ return keccak256(encodePacked(["string"], [fillHash]));
2392
+ }
2393
+ function fillOidToBytes32(oid) {
2394
+ return keccak256(encodePacked(["uint256"], [BigInt(oid)]));
2395
+ }
2396
+
2397
+ // src/perp/orders.ts
2398
+ var OrderManager = class {
2399
+ client;
2400
+ signer;
2401
+ config;
2402
+ constructor(client, signer, config) {
2403
+ this.client = client;
2404
+ this.signer = signer;
2405
+ this.config = config;
2406
+ }
2407
+ /**
2408
+ * Place an order on Hyperliquid from a trade signal.
2409
+ * Attaches builder fee for revenue collection.
2410
+ */
2411
+ async placeOrder(signal) {
2412
+ try {
2413
+ const assetIndex = await this.client.getAssetIndex(signal.instrument);
2414
+ const isBuy = signal.action === "open_long" || signal.action === "close_short";
2415
+ const side = isBuy ? "B" : "A";
2416
+ const orderWire = {
2417
+ a: assetIndex,
2418
+ b: isBuy,
2419
+ p: signal.orderType === "market" ? this.getMarketPrice(signal) : signal.price.toString(),
2420
+ s: signal.size.toString(),
2421
+ r: signal.reduceOnly,
2422
+ t: signal.orderType === "market" ? { limit: { tif: "Ioc" } } : { limit: { tif: "Gtc" } }
2423
+ };
2424
+ const action = {
2425
+ type: "order",
2426
+ orders: [orderWire],
2427
+ grouping: "na",
2428
+ builder: {
2429
+ b: this.config.builderAddress,
2430
+ f: this.config.builderFeeTenthsBps
2431
+ }
2432
+ };
2433
+ const nonce = getNextNonce();
2434
+ const { signature } = await this.signer.signAction(action, nonce);
2435
+ const address = this.signer.getAddress();
2436
+ const resp = await this.exchangeRequest({
2437
+ action,
2438
+ nonce: Number(nonce),
2439
+ signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
2440
+ vaultAddress: null
2441
+ });
2442
+ return this.parseOrderResponse(resp);
2443
+ } catch (error) {
2444
+ const message = error instanceof Error ? error.message : String(error);
2445
+ console.error(`Order placement failed for ${signal.instrument}:`, message);
2446
+ return {
2447
+ success: false,
2448
+ status: "error",
2449
+ error: message
2450
+ };
2451
+ }
2452
+ }
2453
+ /**
2454
+ * Cancel an open order by ID.
2455
+ */
2456
+ async cancelOrder(instrument, orderId) {
2457
+ try {
2458
+ const assetIndex = await this.client.getAssetIndex(instrument);
2459
+ const action = {
2460
+ type: "cancel",
2461
+ cancels: [{ a: assetIndex, o: orderId }]
2462
+ };
2463
+ const nonce = getNextNonce();
2464
+ const { signature } = await this.signer.signAction(action, nonce);
2465
+ await this.exchangeRequest({
2466
+ action,
2467
+ nonce: Number(nonce),
2468
+ signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
2469
+ vaultAddress: null
2470
+ });
2471
+ console.log(`Cancelled order ${orderId} for ${instrument}`);
2472
+ return true;
2473
+ } catch (error) {
2474
+ const message = error instanceof Error ? error.message : String(error);
2475
+ console.error(`Cancel failed for order ${orderId}:`, message);
2476
+ return false;
2477
+ }
2478
+ }
2479
+ /**
2480
+ * Close an entire position for an instrument.
2481
+ * Uses a market order with reduceOnly flag.
2482
+ */
2483
+ async closePosition(instrument, positionSize) {
2484
+ const isLong = positionSize > 0;
2485
+ const signal = {
2486
+ action: isLong ? "close_long" : "close_short",
2487
+ instrument,
2488
+ size: Math.abs(positionSize),
2489
+ price: 0,
2490
+ leverage: 1,
2491
+ orderType: "market",
2492
+ reduceOnly: true,
2493
+ confidence: 1,
2494
+ reasoning: "Position close"
2495
+ };
2496
+ return this.placeOrder(signal);
2497
+ }
2498
+ /**
2499
+ * Update leverage for an instrument.
2500
+ */
2501
+ async updateLeverage(instrument, leverage, isCross = true) {
2502
+ try {
2503
+ const assetIndex = await this.client.getAssetIndex(instrument);
2504
+ const action = {
2505
+ type: "updateLeverage",
2506
+ asset: assetIndex,
2507
+ isCross,
2508
+ leverage
2509
+ };
2510
+ const nonce = getNextNonce();
2511
+ const { signature } = await this.signer.signAction(action, nonce);
2512
+ await this.exchangeRequest({
2513
+ action,
2514
+ nonce: Number(nonce),
2515
+ signature: { r: signature.slice(0, 66), s: `0x${signature.slice(66, 130)}`, v: parseInt(signature.slice(130, 132), 16) },
2516
+ vaultAddress: null
2517
+ });
2518
+ console.log(`Leverage updated for ${instrument}: ${leverage}x (${isCross ? "cross" : "isolated"})`);
2519
+ return true;
2520
+ } catch (error) {
2521
+ const message = error instanceof Error ? error.message : String(error);
2522
+ console.error(`Leverage update failed for ${instrument}:`, message);
2523
+ return false;
2524
+ }
2525
+ }
2526
+ // ============================================================
2527
+ // PRIVATE HELPERS
2528
+ // ============================================================
2529
+ /**
2530
+ * Get a market price string for IOC orders.
2531
+ * Uses a generous slippage buffer to ensure fills.
2532
+ */
2533
+ getMarketPrice(signal) {
2534
+ const isBuy = signal.action === "open_long" || signal.action === "close_short";
2535
+ if (signal.price > 0) {
2536
+ const slippage = isBuy ? 1.005 : 0.995;
2537
+ return (signal.price * slippage).toString();
2538
+ }
2539
+ return "0";
2540
+ }
2541
+ /**
2542
+ * Parse Hyperliquid exchange response into OrderResult.
2543
+ */
2544
+ parseOrderResponse(resp) {
2545
+ if (resp?.status === "ok" && resp?.response?.type === "order") {
2546
+ const statuses = resp.response.data?.statuses || [];
2547
+ if (statuses.length > 0) {
2548
+ const status = statuses[0];
2549
+ if (status.filled) {
2550
+ return {
2551
+ success: true,
2552
+ orderId: status.filled.oid,
2553
+ status: "filled",
2554
+ avgPrice: status.filled.avgPx,
2555
+ filledSize: status.filled.totalSz
2556
+ };
2557
+ }
2558
+ if (status.resting) {
2559
+ return {
2560
+ success: true,
2561
+ orderId: status.resting.oid,
2562
+ status: "resting"
2563
+ };
2564
+ }
2565
+ if (status.error) {
2566
+ return {
2567
+ success: false,
2568
+ status: "error",
2569
+ error: status.error
2570
+ };
2571
+ }
2572
+ }
2573
+ }
2574
+ return {
2575
+ success: false,
2576
+ status: "error",
2577
+ error: `Unexpected response: ${JSON.stringify(resp)}`
2578
+ };
2579
+ }
2580
+ /**
2581
+ * Send a signed request to the Hyperliquid Exchange API.
2582
+ */
2583
+ async exchangeRequest(body) {
2584
+ const resp = await fetch(`${this.config.apiUrl}/exchange`, {
2585
+ method: "POST",
2586
+ headers: { "Content-Type": "application/json" },
2587
+ body: JSON.stringify(body)
2588
+ });
2589
+ if (!resp.ok) {
2590
+ throw new Error(`Hyperliquid Exchange API error: ${resp.status} ${await resp.text()}`);
2591
+ }
2592
+ return resp.json();
2593
+ }
2594
+ };
2595
+
2596
+ // src/perp/positions.ts
2597
+ var PositionManager = class {
2598
+ client;
2599
+ userAddress;
2600
+ /** Cached positions (updated each cycle) */
2601
+ cachedPositions = [];
2602
+ cachedAccount = null;
2603
+ lastRefreshAt = 0;
2604
+ /** Cache TTL in ms (5 seconds — positions refresh each cycle anyway) */
2605
+ cacheTtlMs = 5e3;
2606
+ constructor(client, userAddress) {
2607
+ this.client = client;
2608
+ this.userAddress = userAddress;
2609
+ }
2610
+ // ============================================================
2611
+ // POSITION QUERIES
2612
+ // ============================================================
2613
+ /**
2614
+ * Get all open positions. Uses cache if fresh.
2615
+ */
2616
+ async getPositions(forceRefresh = false) {
2617
+ if (!forceRefresh && this.isCacheFresh()) {
2618
+ return this.cachedPositions;
2619
+ }
2620
+ await this.refresh();
2621
+ return this.cachedPositions;
2622
+ }
2623
+ /**
2624
+ * Get a specific position by instrument.
2625
+ * Returns null if no position is open.
2626
+ */
2627
+ async getPosition(instrument) {
2628
+ const positions = await this.getPositions();
2629
+ return positions.find((p) => p.instrument === instrument) ?? null;
2630
+ }
2631
+ /**
2632
+ * Get account summary (equity, margin, leverage).
2633
+ */
2634
+ async getAccountSummary(forceRefresh = false) {
2635
+ if (!forceRefresh && this.isCacheFresh() && this.cachedAccount) {
2636
+ return this.cachedAccount;
2637
+ }
2638
+ await this.refresh();
2639
+ return this.cachedAccount;
2640
+ }
2641
+ // ============================================================
2642
+ // LIQUIDATION MONITORING
2643
+ // ============================================================
2644
+ /**
2645
+ * Get liquidation proximity for all positions.
2646
+ * Returns a value between 0.0 (safe) and 1.0 (at liquidation price).
2647
+ * Values above 0.7 should trigger risk warnings.
2648
+ */
2649
+ async getLiquidationProximity() {
2650
+ const positions = await this.getPositions();
2651
+ const proximities = /* @__PURE__ */ new Map();
2652
+ for (const pos of positions) {
2653
+ if (pos.liquidationPrice <= 0 || pos.markPrice <= 0) {
2654
+ proximities.set(pos.instrument, 0);
2655
+ continue;
2656
+ }
2657
+ let proximity;
2658
+ if (pos.size > 0) {
2659
+ if (pos.markPrice <= pos.liquidationPrice) {
2660
+ proximity = 1;
2661
+ } else {
2662
+ const distanceToLiq = pos.markPrice - pos.liquidationPrice;
2663
+ const entryToLiq = pos.entryPrice - pos.liquidationPrice;
2664
+ proximity = entryToLiq > 0 ? 1 - distanceToLiq / entryToLiq : 0;
2665
+ }
2666
+ } else {
2667
+ if (pos.markPrice >= pos.liquidationPrice) {
2668
+ proximity = 1;
2669
+ } else {
2670
+ const distanceToLiq = pos.liquidationPrice - pos.markPrice;
2671
+ const entryToLiq = pos.liquidationPrice - pos.entryPrice;
2672
+ proximity = entryToLiq > 0 ? 1 - distanceToLiq / entryToLiq : 0;
2673
+ }
2674
+ }
2675
+ proximities.set(pos.instrument, Math.max(0, Math.min(1, proximity)));
2676
+ }
2677
+ return proximities;
2678
+ }
2679
+ /**
2680
+ * Check if any position is dangerously close to liquidation.
2681
+ * Returns instruments with proximity > threshold.
2682
+ */
2683
+ async getDangerousPositions(threshold = 0.7) {
2684
+ const positions = await this.getPositions();
2685
+ const proximities = await this.getLiquidationProximity();
2686
+ return positions.filter((p) => {
2687
+ const prox = proximities.get(p.instrument) ?? 0;
2688
+ return prox > threshold;
2689
+ });
2690
+ }
2691
+ // ============================================================
2692
+ // SUMMARY HELPERS
2693
+ // ============================================================
2694
+ /**
2695
+ * Get total unrealized PnL across all positions.
2696
+ */
2697
+ async getTotalUnrealizedPnl() {
2698
+ const positions = await this.getPositions();
2699
+ return positions.reduce((sum, p) => sum + p.unrealizedPnl, 0);
2700
+ }
2701
+ /**
2702
+ * Get total notional exposure.
2703
+ */
2704
+ async getTotalNotional() {
2705
+ const positions = await this.getPositions();
2706
+ return positions.reduce((sum, p) => sum + p.notionalUSD, 0);
2707
+ }
2708
+ /**
2709
+ * Get position count.
2710
+ */
2711
+ async getPositionCount() {
2712
+ const positions = await this.getPositions();
2713
+ return positions.length;
2714
+ }
2715
+ // ============================================================
2716
+ // CACHE MANAGEMENT
2717
+ // ============================================================
2718
+ /**
2719
+ * Force refresh positions and account from Hyperliquid.
2720
+ */
2721
+ async refresh() {
2722
+ try {
2723
+ const [positions, account] = await Promise.all([
2724
+ this.client.getPositions(this.userAddress),
2725
+ this.client.getAccountSummary(this.userAddress)
2726
+ ]);
2727
+ this.cachedPositions = positions;
2728
+ this.cachedAccount = account;
2729
+ this.lastRefreshAt = Date.now();
2730
+ } catch (error) {
2731
+ const message = error instanceof Error ? error.message : String(error);
2732
+ console.error("Failed to refresh positions:", message);
2733
+ }
2734
+ }
2735
+ /**
2736
+ * Check if cache is still fresh.
2737
+ */
2738
+ isCacheFresh() {
2739
+ return Date.now() - this.lastRefreshAt < this.cacheTtlMs;
2740
+ }
2741
+ };
2742
+
2743
+ // src/perp/websocket.ts
2744
+ import WebSocket2 from "ws";
2745
+ var HyperliquidWebSocket = class {
2746
+ wsUrl;
2747
+ userAddress;
2748
+ client;
2749
+ ws = null;
2750
+ reconnectAttempts = 0;
2751
+ maxReconnectAttempts = 20;
2752
+ baseReconnectMs = 1e3;
2753
+ maxReconnectMs = 6e4;
2754
+ reconnectTimer = null;
2755
+ pingTimer = null;
2756
+ isConnecting = false;
2757
+ shouldReconnect = true;
2758
+ /** Last processed fill time (ms) — used for REST backfill on reconnect */
2759
+ lastProcessedFillTime = 0;
2760
+ /** Callbacks */
2761
+ onFill = null;
2762
+ onFunding = null;
2763
+ onLiquidation = null;
2764
+ constructor(config, userAddress, client) {
2765
+ this.wsUrl = config.wsUrl;
2766
+ this.userAddress = userAddress;
2767
+ this.client = client;
2768
+ }
2769
+ // ============================================================
2770
+ // CONNECTION
2771
+ // ============================================================
2772
+ /**
2773
+ * Connect to Hyperliquid WebSocket and subscribe to user events.
2774
+ */
2775
+ async connect() {
2776
+ if (this.ws?.readyState === WebSocket2.OPEN || this.isConnecting) {
2777
+ return;
2778
+ }
2779
+ this.isConnecting = true;
2780
+ this.shouldReconnect = true;
2781
+ return new Promise((resolve, reject) => {
2782
+ try {
2783
+ this.ws = new WebSocket2(this.wsUrl);
2784
+ this.ws.on("open", () => {
2785
+ this.isConnecting = false;
2786
+ this.reconnectAttempts = 0;
2787
+ console.log("Hyperliquid WebSocket connected");
2788
+ this.subscribe({
2789
+ type: "subscribe",
2790
+ subscription: { type: "userFills", user: this.userAddress }
2791
+ });
2792
+ this.subscribe({
2793
+ type: "subscribe",
2794
+ subscription: { type: "userFundings", user: this.userAddress }
2795
+ });
2796
+ this.startPing();
2797
+ this.backfillMissedFills().catch((err) => {
2798
+ console.warn("Fill backfill failed:", err instanceof Error ? err.message : err);
2799
+ });
2800
+ resolve();
2801
+ });
2802
+ this.ws.on("message", (data) => {
2803
+ this.handleMessage(data);
2804
+ });
2805
+ this.ws.on("close", (code, reason) => {
2806
+ this.isConnecting = false;
2807
+ console.log(`Hyperliquid WebSocket closed: ${code} ${reason.toString()}`);
2808
+ this.stopPing();
2809
+ this.scheduleReconnect();
2810
+ });
2811
+ this.ws.on("error", (error) => {
2812
+ this.isConnecting = false;
2813
+ console.error("Hyperliquid WebSocket error:", error.message);
2814
+ if (this.reconnectAttempts === 0) {
2815
+ reject(error);
2816
+ }
2817
+ });
2818
+ } catch (error) {
2819
+ this.isConnecting = false;
2820
+ reject(error);
2821
+ }
2822
+ });
2823
+ }
2824
+ /**
2825
+ * Disconnect and stop reconnecting.
2826
+ */
2827
+ disconnect() {
2828
+ this.shouldReconnect = false;
2829
+ if (this.reconnectTimer) {
2830
+ clearTimeout(this.reconnectTimer);
2831
+ this.reconnectTimer = null;
2832
+ }
2833
+ this.stopPing();
2834
+ if (this.ws) {
2835
+ this.ws.removeAllListeners();
2836
+ if (this.ws.readyState === WebSocket2.OPEN) {
2837
+ this.ws.close(1e3, "Client disconnect");
2838
+ }
2839
+ this.ws = null;
2840
+ }
2841
+ console.log("Hyperliquid WebSocket disconnected");
2842
+ }
2843
+ /**
2844
+ * Check if WebSocket is connected.
2845
+ */
2846
+ get isConnected() {
2847
+ return this.ws?.readyState === WebSocket2.OPEN;
2848
+ }
2849
+ // ============================================================
2850
+ // EVENT HANDLERS
2851
+ // ============================================================
2852
+ /**
2853
+ * Register callback for fill events.
2854
+ */
2855
+ onFillReceived(callback) {
2856
+ this.onFill = callback;
2857
+ }
2858
+ /**
2859
+ * Register callback for funding payment events.
2860
+ */
2861
+ onFundingReceived(callback) {
2862
+ this.onFunding = callback;
2863
+ }
2864
+ /**
2865
+ * Register callback for liquidation events.
2866
+ */
2867
+ onLiquidationDetected(callback) {
2868
+ this.onLiquidation = callback;
2869
+ }
2870
+ /**
2871
+ * Get the last processed fill time (for external checkpoint management).
2872
+ */
2873
+ getLastProcessedFillTime() {
2874
+ return this.lastProcessedFillTime;
2875
+ }
2876
+ // ============================================================
2877
+ // MESSAGE HANDLING
2878
+ // ============================================================
2879
+ handleMessage(data) {
2880
+ try {
2881
+ const msg = JSON.parse(data.toString());
2882
+ if (msg.channel === "userFills") {
2883
+ this.handleFillMessage(msg.data);
2884
+ } else if (msg.channel === "userFundings") {
2885
+ this.handleFundingMessage(msg.data);
2886
+ }
2887
+ } catch (error) {
2888
+ }
2889
+ }
2890
+ handleFillMessage(fills) {
2891
+ if (!Array.isArray(fills) || !this.onFill) return;
2892
+ for (const rawFill of fills) {
2893
+ const fill = this.client.parseFill(rawFill);
2894
+ if (fill.time > this.lastProcessedFillTime) {
2895
+ this.lastProcessedFillTime = fill.time;
2896
+ }
2897
+ if (fill.liquidation && this.onLiquidation) {
2898
+ this.onLiquidation(fill.coin, parseFloat(fill.sz));
2899
+ }
2900
+ this.onFill(fill);
2901
+ }
2902
+ }
2903
+ handleFundingMessage(fundings) {
2904
+ if (!Array.isArray(fundings) || !this.onFunding) return;
2905
+ for (const funding of fundings) {
2906
+ this.onFunding({
2907
+ time: funding.time,
2908
+ coin: funding.coin,
2909
+ usdc: funding.usdc,
2910
+ szi: funding.szi,
2911
+ fundingRate: funding.fundingRate
2912
+ });
2913
+ }
2914
+ }
2915
+ // ============================================================
2916
+ // BACKFILL
2917
+ // ============================================================
2918
+ /**
2919
+ * Backfill fills that may have been missed during WebSocket downtime.
2920
+ * Uses the last processed fill time as the starting point.
2921
+ */
2922
+ async backfillMissedFills() {
2923
+ if (this.lastProcessedFillTime === 0 || !this.onFill) {
2924
+ return;
2925
+ }
2926
+ console.log(`Backfilling fills since ${new Date(this.lastProcessedFillTime).toISOString()}`);
2927
+ const fills = await this.client.getUserFillsByTime(
2928
+ this.userAddress,
2929
+ this.lastProcessedFillTime + 1
2930
+ // +1 to avoid duplicate
2931
+ );
2932
+ if (fills.length > 0) {
2933
+ console.log(`Backfilled ${fills.length} missed fills`);
2934
+ for (const rawFill of fills) {
2935
+ const fill = this.client.parseFill(rawFill);
2936
+ if (fill.time > this.lastProcessedFillTime) {
2937
+ this.lastProcessedFillTime = fill.time;
2938
+ }
2939
+ if (fill.liquidation && this.onLiquidation) {
2940
+ this.onLiquidation(fill.coin, parseFloat(fill.sz));
2941
+ }
2942
+ this.onFill(fill);
2943
+ }
2944
+ }
2945
+ }
2946
+ // ============================================================
2947
+ // RECONNECTION
2948
+ // ============================================================
2949
+ scheduleReconnect() {
2950
+ if (!this.shouldReconnect || this.reconnectAttempts >= this.maxReconnectAttempts) {
2951
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
2952
+ console.error(`Hyperliquid WebSocket: max reconnect attempts (${this.maxReconnectAttempts}) reached`);
2953
+ }
2954
+ return;
2955
+ }
2956
+ const delay = Math.min(
2957
+ this.baseReconnectMs * Math.pow(2, this.reconnectAttempts),
2958
+ this.maxReconnectMs
2959
+ );
2960
+ this.reconnectAttempts++;
2961
+ console.log(`Hyperliquid WebSocket: reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
2962
+ this.reconnectTimer = setTimeout(() => {
2963
+ this.connect().catch((err) => {
2964
+ console.error("Reconnect failed:", err instanceof Error ? err.message : err);
2965
+ });
2966
+ }, delay);
2967
+ }
2968
+ // ============================================================
2969
+ // KEEPALIVE
2970
+ // ============================================================
2971
+ startPing() {
2972
+ this.stopPing();
2973
+ this.pingTimer = setInterval(() => {
2974
+ if (this.ws?.readyState === WebSocket2.OPEN) {
2975
+ this.ws.send(JSON.stringify({ method: "ping" }));
2976
+ }
2977
+ }, 25e3);
2978
+ }
2979
+ stopPing() {
2980
+ if (this.pingTimer) {
2981
+ clearInterval(this.pingTimer);
2982
+ this.pingTimer = null;
2983
+ }
2984
+ }
2985
+ // ============================================================
2986
+ // HELPERS
2987
+ // ============================================================
2988
+ subscribe(msg) {
2989
+ if (this.ws?.readyState === WebSocket2.OPEN) {
2990
+ this.ws.send(JSON.stringify(msg));
2991
+ }
2992
+ }
2993
+ };
2994
+
2995
+ // src/perp/recorder.ts
2996
+ import {
2997
+ createPublicClient as createPublicClient3,
2998
+ createWalletClient as createWalletClient2,
2999
+ http as http3
3000
+ } from "viem";
3001
+ import { base as base2 } from "viem/chains";
3002
+ import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
3003
+ var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
3004
+ var ROUTER_ABI = [
3005
+ {
3006
+ type: "function",
3007
+ name: "recordPerpTrade",
3008
+ stateMutability: "nonpayable",
3009
+ inputs: [
3010
+ { name: "agentId", type: "uint256" },
3011
+ { name: "configHash", type: "bytes32" },
3012
+ { name: "instrument", type: "string" },
3013
+ { name: "isLong", type: "bool" },
3014
+ { name: "notionalUSD", type: "uint256" },
3015
+ { name: "feeUSD", type: "uint256" },
3016
+ { name: "fillId", type: "bytes32" }
3017
+ ],
3018
+ outputs: [{ name: "", type: "bool" }]
3019
+ }
3020
+ ];
3021
+ var MAX_RETRIES = 3;
3022
+ var RETRY_DELAY_MS = 5e3;
3023
+ var PerpTradeRecorder = class {
3024
+ // Use `any` for viem client types to avoid L2 chain type conflicts (Base has "deposit" tx type)
3025
+ publicClient;
3026
+ // eslint-disable-line @typescript-eslint/no-explicit-any
3027
+ walletClient;
3028
+ // eslint-disable-line @typescript-eslint/no-explicit-any
3029
+ account;
3030
+ agentId;
3031
+ configHash;
3032
+ /** Retry queue for failed recordings */
3033
+ retryQueue = [];
3034
+ /** Set of fill IDs already recorded (or in-progress) to prevent local dups */
3035
+ recordedFills = /* @__PURE__ */ new Set();
3036
+ /** Timer for processing retry queue */
3037
+ retryTimer = null;
3038
+ constructor(opts) {
3039
+ this.agentId = opts.agentId;
3040
+ this.configHash = opts.configHash;
3041
+ this.account = privateKeyToAccount3(opts.privateKey);
3042
+ const rpcUrl = opts.rpcUrl || "https://mainnet.base.org";
3043
+ const transport = http3(rpcUrl);
3044
+ this.publicClient = createPublicClient3({
3045
+ chain: base2,
3046
+ transport
3047
+ });
3048
+ this.walletClient = createWalletClient2({
3049
+ chain: base2,
3050
+ transport,
3051
+ account: this.account
3052
+ });
3053
+ this.retryTimer = setInterval(() => this.processRetryQueue(), RETRY_DELAY_MS);
3054
+ }
3055
+ // ============================================================
3056
+ // PUBLIC API
3057
+ // ============================================================
3058
+ /**
3059
+ * Record a fill on-chain.
3060
+ * Converts the Hyperliquid fill into recordPerpTrade params and submits.
3061
+ */
3062
+ async recordFill(fill) {
3063
+ const fillId = fillHashToBytes32(fill.hash);
3064
+ const fillIdStr = fillId.toLowerCase();
3065
+ if (this.recordedFills.has(fillIdStr)) {
3066
+ return { success: true };
3067
+ }
3068
+ this.recordedFills.add(fillIdStr);
3069
+ const params = {
3070
+ agentId: this.agentId,
3071
+ configHash: this.configHash,
3072
+ instrument: fill.coin,
3073
+ isLong: fill.side === "B",
3074
+ notionalUSD: this.calculateNotionalUSD(fill),
3075
+ feeUSD: this.calculateFeeUSD(fill),
3076
+ fillId
3077
+ };
3078
+ return this.submitRecord(params);
3079
+ }
3080
+ /**
3081
+ * Update the config hash (when epoch changes).
3082
+ */
3083
+ updateConfigHash(configHash) {
3084
+ this.configHash = configHash;
3085
+ }
3086
+ /**
3087
+ * Get the number of fills pending retry.
3088
+ */
3089
+ get pendingRetries() {
3090
+ return this.retryQueue.length;
3091
+ }
3092
+ /**
3093
+ * Get the number of fills recorded (local dedup set size).
3094
+ */
3095
+ get recordedCount() {
3096
+ return this.recordedFills.size;
3097
+ }
3098
+ /**
3099
+ * Stop the recorder (clear retry timer).
3100
+ */
3101
+ stop() {
3102
+ if (this.retryTimer) {
3103
+ clearInterval(this.retryTimer);
3104
+ this.retryTimer = null;
3105
+ }
3106
+ }
3107
+ // ============================================================
3108
+ // PRIVATE
3109
+ // ============================================================
3110
+ /**
3111
+ * Submit a recordPerpTrade transaction on Base.
3112
+ */
3113
+ async submitRecord(params) {
3114
+ try {
3115
+ const { request } = await this.publicClient.simulateContract({
3116
+ address: ROUTER_ADDRESS,
3117
+ abi: ROUTER_ABI,
3118
+ functionName: "recordPerpTrade",
3119
+ args: [
3120
+ params.agentId,
3121
+ params.configHash,
3122
+ params.instrument,
3123
+ params.isLong,
3124
+ params.notionalUSD,
3125
+ params.feeUSD,
3126
+ params.fillId
3127
+ ],
3128
+ account: this.account
3129
+ });
3130
+ const txHash = await this.walletClient.writeContract(request);
3131
+ console.log(`Perp trade recorded: ${params.instrument} ${params.isLong ? "LONG" : "SHORT"} $${Number(params.notionalUSD) / 1e6} \u2014 tx: ${txHash}`);
3132
+ return { success: true, txHash };
3133
+ } catch (error) {
3134
+ const message = error instanceof Error ? error.message : String(error);
3135
+ if (message.includes("DuplicateFill") || message.includes("already recorded")) {
3136
+ console.log(`Fill already recorded on-chain: ${params.fillId}`);
3137
+ return { success: true };
3138
+ }
3139
+ console.error(`Failed to record perp trade: ${message}`);
3140
+ this.retryQueue.push({
3141
+ params,
3142
+ retries: 0,
3143
+ lastAttempt: Date.now()
3144
+ });
3145
+ return { success: false, error: message };
3146
+ }
3147
+ }
3148
+ /**
3149
+ * Process the retry queue — attempt to re-submit failed recordings.
3150
+ */
3151
+ async processRetryQueue() {
3152
+ if (this.retryQueue.length === 0) return;
3153
+ const now = Date.now();
3154
+ const toRetry = this.retryQueue.filter(
3155
+ (item) => now - item.lastAttempt >= RETRY_DELAY_MS
3156
+ );
3157
+ for (const item of toRetry) {
3158
+ item.retries++;
3159
+ item.lastAttempt = now;
3160
+ if (item.retries > MAX_RETRIES) {
3161
+ console.error(
3162
+ `Perp trade recording permanently failed after ${MAX_RETRIES} retries: ${item.params.instrument} ${item.params.fillId}`
3163
+ );
3164
+ const idx = this.retryQueue.indexOf(item);
3165
+ if (idx >= 0) this.retryQueue.splice(idx, 1);
3166
+ continue;
3167
+ }
3168
+ console.log(
3169
+ `Retrying perp trade recording (attempt ${item.retries}/${MAX_RETRIES}): ${item.params.instrument}`
3170
+ );
3171
+ const result = await this.submitRecord(item.params);
3172
+ if (result.success) {
3173
+ const idx = this.retryQueue.indexOf(item);
3174
+ if (idx >= 0) this.retryQueue.splice(idx, 1);
3175
+ }
3176
+ }
3177
+ }
3178
+ // ============================================================
3179
+ // CONVERSION HELPERS
3180
+ // ============================================================
3181
+ /**
3182
+ * Calculate notional USD from a fill (6-decimal).
3183
+ * notionalUSD = px * sz * 1e6
3184
+ */
3185
+ calculateNotionalUSD(fill) {
3186
+ const px = parseFloat(fill.px);
3187
+ const sz = parseFloat(fill.sz);
3188
+ return BigInt(Math.round(px * sz * 1e6));
3189
+ }
3190
+ /**
3191
+ * Calculate fee USD from a fill (6-decimal).
3192
+ * feeUSD = fee * 1e6 (fee is already in USD on Hyperliquid)
3193
+ */
3194
+ calculateFeeUSD(fill) {
3195
+ const fee = parseFloat(fill.fee);
3196
+ const builderFee = fill.builderFee ? parseFloat(fill.builderFee) : 0;
3197
+ return BigInt(Math.round((fee + builderFee) * 1e6));
3198
+ }
3199
+ };
3200
+
3201
+ // src/perp/onboarding.ts
3202
+ var PerpOnboarding = class {
3203
+ client;
3204
+ signer;
3205
+ config;
3206
+ constructor(client, signer, config) {
3207
+ this.client = client;
3208
+ this.signer = signer;
3209
+ this.config = config;
3210
+ }
3211
+ // ============================================================
3212
+ // BUILDER FEE
3213
+ // ============================================================
3214
+ /**
3215
+ * Check if the user has approved the builder fee.
3216
+ * Builder fee must be approved before orders can include builder fees.
3217
+ */
3218
+ async isBuilderFeeApproved() {
3219
+ try {
3220
+ const maxFee = await this.client.getMaxBuilderFee(
3221
+ this.signer.getAddress(),
3222
+ this.config.builderAddress
3223
+ );
3224
+ return maxFee >= this.config.builderFeeTenthsBps;
3225
+ } catch {
3226
+ return false;
3227
+ }
3228
+ }
3229
+ /**
3230
+ * Approve the builder fee on Hyperliquid.
3231
+ * This is a one-time approval per builder address.
3232
+ */
3233
+ async approveBuilderFee() {
3234
+ try {
3235
+ const action = {
3236
+ type: "approveBuilderFee",
3237
+ hyperliquidChain: "Mainnet",
3238
+ maxFeeRate: `${this.config.builderFeeTenthsBps / 1e4}%`,
3239
+ builder: this.config.builderAddress,
3240
+ nonce: Number(getNextNonce())
3241
+ };
3242
+ const { signature } = await this.signer.signApproval(action);
3243
+ const resp = await fetch(`${this.config.apiUrl}/exchange`, {
3244
+ method: "POST",
3245
+ headers: { "Content-Type": "application/json" },
3246
+ body: JSON.stringify({
3247
+ action,
3248
+ signature: {
3249
+ r: signature.slice(0, 66),
3250
+ s: `0x${signature.slice(66, 130)}`,
3251
+ v: parseInt(signature.slice(130, 132), 16)
3252
+ },
3253
+ nonce: action.nonce,
3254
+ vaultAddress: null
3255
+ })
3256
+ });
3257
+ if (!resp.ok) {
3258
+ const text = await resp.text();
3259
+ console.error(`Builder fee approval failed: ${resp.status} ${text}`);
3260
+ return false;
3261
+ }
3262
+ console.log(`Builder fee approved: ${this.config.builderFeeTenthsBps / 10} bps for ${this.config.builderAddress}`);
3263
+ return true;
3264
+ } catch (error) {
3265
+ const message = error instanceof Error ? error.message : String(error);
3266
+ console.error(`Builder fee approval failed: ${message}`);
3267
+ return false;
3268
+ }
3269
+ }
3270
+ // ============================================================
3271
+ // BALANCE & REQUIREMENTS
3272
+ // ============================================================
3273
+ /**
3274
+ * Check if the user has sufficient USDC balance on Hyperliquid.
3275
+ * Returns the account equity in USD.
3276
+ */
3277
+ async checkBalance() {
3278
+ try {
3279
+ const account = await this.client.getAccountSummary(this.signer.getAddress());
3280
+ return {
3281
+ hasBalance: account.totalEquity > 0,
3282
+ equity: account.totalEquity
3283
+ };
3284
+ } catch {
3285
+ return { hasBalance: false, equity: 0 };
3286
+ }
3287
+ }
3288
+ /**
3289
+ * Verify that the agent's risk universe allows perp trading.
3290
+ * Perps require risk universe >= 2 (Derivatives or higher).
3291
+ */
3292
+ verifyRiskUniverse(riskUniverse) {
3293
+ if (riskUniverse >= 2) {
3294
+ return {
3295
+ allowed: true,
3296
+ message: `Risk universe ${riskUniverse} allows perp trading`
3297
+ };
3298
+ }
3299
+ return {
3300
+ allowed: false,
3301
+ message: `Risk universe ${riskUniverse} does not allow perp trading. Perps require Derivatives (2) or higher.`
3302
+ };
3303
+ }
3304
+ // ============================================================
3305
+ // FULL ONBOARDING CHECK
3306
+ // ============================================================
3307
+ /**
3308
+ * Run all onboarding checks and return status.
3309
+ * Does NOT auto-approve — caller must explicitly approve after review.
3310
+ */
3311
+ async checkOnboardingStatus(riskUniverse) {
3312
+ const riskCheck = this.verifyRiskUniverse(riskUniverse);
3313
+ const balanceCheck = await this.checkBalance();
3314
+ const builderFeeApproved = await this.isBuilderFeeApproved();
3315
+ const ready = riskCheck.allowed && balanceCheck.hasBalance && builderFeeApproved;
3316
+ return {
3317
+ ready,
3318
+ riskUniverseOk: riskCheck.allowed,
3319
+ riskUniverseMessage: riskCheck.message,
3320
+ hasBalance: balanceCheck.hasBalance,
3321
+ equity: balanceCheck.equity,
3322
+ builderFeeApproved,
3323
+ builderAddress: this.config.builderAddress,
3324
+ builderFeeBps: this.config.builderFeeTenthsBps / 10
3325
+ };
3326
+ }
3327
+ /**
3328
+ * Run full onboarding: check status and auto-approve builder fee if needed.
3329
+ * Returns the final status after all actions.
3330
+ */
3331
+ async onboard(riskUniverse) {
3332
+ let status = await this.checkOnboardingStatus(riskUniverse);
3333
+ if (!status.riskUniverseOk) {
3334
+ console.error(`Perp onboarding blocked: ${status.riskUniverseMessage}`);
3335
+ return status;
3336
+ }
3337
+ if (!status.hasBalance) {
3338
+ console.warn("No USDC balance on Hyperliquid \u2014 deposit required before trading");
3339
+ return status;
3340
+ }
3341
+ if (!status.builderFeeApproved) {
3342
+ console.log("Approving builder fee...");
3343
+ const approved = await this.approveBuilderFee();
3344
+ if (approved) {
3345
+ status = { ...status, builderFeeApproved: true, ready: true };
3346
+ }
3347
+ }
3348
+ if (status.ready) {
3349
+ console.log(`Perp onboarding complete \u2014 equity: $${status.equity.toFixed(2)}`);
3350
+ }
3351
+ return status;
3352
+ }
3353
+ };
3354
+
3355
+ // src/perp/funding.ts
3356
+ import {
3357
+ createPublicClient as createPublicClient4,
3358
+ createWalletClient as createWalletClient3,
3359
+ http as http4,
3360
+ parseAbi
3361
+ } from "viem";
3362
+ import { base as base3, arbitrum } from "viem/chains";
3363
+ import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
3364
+ var ERC20_ABI = parseAbi([
3365
+ "function approve(address spender, uint256 amount) external returns (bool)",
3366
+ "function balanceOf(address account) external view returns (uint256)",
3367
+ "function allowance(address owner, address spender) external view returns (uint256)"
3368
+ ]);
3369
+ var TOKEN_MESSENGER_V2_ABI = parseAbi([
3370
+ "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken) external returns (uint64 nonce)",
3371
+ "event DepositForBurn(uint64 indexed nonce, address indexed burnToken, uint256 amount, address indexed depositor, bytes32 mintRecipient, uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller)"
3372
+ ]);
3373
+ var MESSAGE_TRANSMITTER_V2_ABI = parseAbi([
3374
+ "function receiveMessage(bytes message, bytes attestation) external returns (bool success)",
3375
+ "event MessageSent(bytes message)"
3376
+ ]);
3377
+ var CORE_DEPOSIT_WALLET_ABI = parseAbi([
3378
+ "function deposit(uint256 amount, uint32 destinationDex) external"
3379
+ ]);
3380
+
3381
+ // src/runtime.ts
3382
+ import { ExagentClient, ExagentRegistry } from "@exagent/sdk";
3383
+ import { createPublicClient as createPublicClient5, createWalletClient as createWalletClient4, http as http5 } from "viem";
3384
+ import { base as base4 } from "viem/chains";
3385
+ import { privateKeyToAccount as privateKeyToAccount5 } from "viem/accounts";
3386
+
3387
+ // src/browser-open.ts
3388
+ import { exec } from "child_process";
3389
+ function openBrowser(url) {
3390
+ const platform = process.platform;
3391
+ try {
3392
+ if (platform === "darwin") {
3393
+ exec(`open "${url}"`);
3394
+ } else if (platform === "win32") {
3395
+ exec(`start "" "${url}"`);
3396
+ } else {
3397
+ exec(`xdg-open "${url}"`);
3398
+ }
3399
+ } catch {
3400
+ }
3401
+ }
3402
+
3403
+ // src/runtime.ts
3404
+ var FUNDS_LOW_THRESHOLD = 5e-3;
3405
+ var FUNDS_CRITICAL_THRESHOLD = 1e-3;
3406
+ var AgentRuntime = class {
3407
+ config;
3408
+ client;
3409
+ llm;
3410
+ strategy;
3411
+ executor;
3412
+ riskManager;
3413
+ marketData;
3414
+ vaultManager;
3415
+ relay = null;
3416
+ isRunning = false;
3417
+ mode = "idle";
3418
+ configHash;
3419
+ cycleCount = 0;
3420
+ lastCycleAt = 0;
3421
+ lastPortfolioValue = 0;
3422
+ lastEthBalance = "0";
3423
+ processAlive = true;
3424
+ riskUniverse = 0;
3425
+ allowedTokens = /* @__PURE__ */ new Set();
3426
+ // Perp trading components (null if perp not enabled)
3427
+ perpClient = null;
3428
+ perpSigner = null;
3429
+ perpOrders = null;
3430
+ perpPositions = null;
3431
+ perpWebSocket = null;
3432
+ perpRecorder = null;
3433
+ perpOnboarding = null;
3434
+ perpStrategy = null;
3435
+ // Two-layer perp control:
3436
+ // perpConnected = Hyperliquid infrastructure is initialized (WS, signer, recorder ready)
3437
+ // perpTradingActive = Dedicated perp trading cycle is mandated to run
3438
+ // When perpConnected && !perpTradingActive: agent's strategy can optionally return perp signals
3439
+ // When perpConnected && perpTradingActive: dedicated runPerpCycle() runs every interval
3440
+ perpConnected = false;
3441
+ perpTradingActive = false;
3442
+ constructor(config) {
3443
+ this.config = config;
3444
+ }
3445
+ /**
3446
+ * Initialize the agent runtime
3447
+ */
3448
+ async initialize() {
3449
+ console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
3450
+ this.client = new ExagentClient({
3451
+ privateKey: this.config.privateKey,
3452
+ network: this.config.network
3453
+ });
3454
+ console.log(`Wallet: ${this.client.address}`);
3455
+ const agent = await this.client.registry.getAgent(BigInt(this.config.agentId));
3456
+ if (!agent) {
3457
+ throw new Error(`Agent ID ${this.config.agentId} not found on-chain. Please register first.`);
3458
+ }
3459
+ console.log(`Agent verified: ${agent.name}`);
3460
+ await this.ensureWalletLinked();
3461
+ await this.loadTradingRestrictions();
3462
+ console.log(`Initializing LLM: ${this.config.llm.provider}`);
3463
+ this.llm = await createLLMAdapter(this.config.llm);
3464
+ const llmMeta = this.llm.getMetadata();
3465
+ console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
3466
+ await this.syncConfigHash();
3467
+ this.strategy = await loadStrategy();
3468
+ this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
3469
+ this.riskManager = new RiskManager(this.config.trading);
3470
+ this.marketData = new MarketDataService(this.getRpcUrl());
3471
+ await this.initializeVaultManager();
3472
+ await this.initializePerp();
3473
+ await this.initializeRelay();
3474
+ console.log("Agent initialized successfully");
3475
+ }
3476
+ /**
3477
+ * Initialize the relay client for command center connectivity
3478
+ */
3479
+ async initializeRelay() {
3480
+ const relayConfig = this.config.relay;
3481
+ const relayEnabled = process.env.EXAGENT_RELAY_ENABLED !== "false";
3482
+ if (!relayConfig?.enabled || !relayEnabled) {
3483
+ console.log("Relay: Disabled");
3484
+ return;
3485
+ }
3486
+ const apiUrl = process.env.EXAGENT_API_URL || relayConfig.apiUrl;
3487
+ if (!apiUrl) {
3488
+ console.log("Relay: No API URL configured, skipping");
3489
+ return;
3490
+ }
3491
+ this.relay = new RelayClient({
3492
+ agentId: String(this.config.agentId),
3493
+ privateKey: this.config.privateKey,
3494
+ relay: {
3495
+ ...relayConfig,
3496
+ apiUrl
3497
+ },
3498
+ onCommand: (cmd) => this.handleCommand(cmd)
3499
+ });
3500
+ try {
3501
+ await this.relay.connect();
3502
+ console.log("Relay: Connected to command center");
3503
+ this.sendRelayStatus();
3504
+ } catch (error) {
3505
+ console.warn(
3506
+ "Relay: Failed to connect (agent will work locally):",
3507
+ error instanceof Error ? error.message : error
3508
+ );
3509
+ }
3510
+ }
3511
+ /**
3512
+ * Initialize the vault manager based on config
3513
+ */
3514
+ async initializeVaultManager() {
3515
+ const vaultConfig = this.config.vault || { policy: "disabled", preferVaultTrading: false };
3516
+ this.vaultManager = new VaultManager({
3517
+ agentId: BigInt(this.config.agentId),
3518
+ agentName: this.config.name,
3519
+ network: this.config.network,
3520
+ walletKey: this.config.privateKey,
3521
+ vaultConfig
3522
+ });
3523
+ console.log(`Vault policy: ${vaultConfig.policy}`);
3524
+ const status = await this.vaultManager.getVaultStatus();
3525
+ if (status.hasVault) {
3526
+ console.log(`Vault exists: ${status.vaultAddress}`);
3527
+ console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
3528
+ } else {
3529
+ console.log("No vault exists for this agent");
3530
+ if (vaultConfig.policy === "manual") {
3531
+ console.log("Vault creation is manual \u2014 use the command center to create one");
3532
+ }
3533
+ }
3534
+ }
3535
+ /**
3536
+ * Initialize Hyperliquid perp trading components.
3537
+ * Only initializes if perp is enabled in config AND risk universe >= 2.
3538
+ */
3539
+ async initializePerp() {
3540
+ const perpConfig = this.config.perp;
3541
+ if (!perpConfig?.enabled) {
3542
+ console.log("Perp trading: Disabled");
3543
+ return;
3544
+ }
3545
+ if (this.riskUniverse < 2) {
3546
+ console.warn(`Perp trading: Blocked by risk universe ${this.riskUniverse} (need >= 2)`);
3547
+ return;
3548
+ }
3549
+ try {
3550
+ const config = {
3551
+ enabled: true,
3552
+ apiUrl: perpConfig.apiUrl || "https://api.hyperliquid.xyz",
3553
+ wsUrl: perpConfig.wsUrl || "wss://api.hyperliquid.xyz/ws",
3554
+ builderAddress: perpConfig.builderAddress,
3555
+ builderFeeTenthsBps: perpConfig.builderFeeTenthsBps ?? 100,
3556
+ maxLeverage: perpConfig.maxLeverage ?? 10,
3557
+ maxNotionalUSD: perpConfig.maxNotionalUSD ?? 5e4,
3558
+ allowedInstruments: perpConfig.allowedInstruments
3559
+ };
3560
+ this.perpClient = new HyperliquidClient(config);
3561
+ const perpKey = perpConfig.perpRelayerKey || this.config.privateKey;
3562
+ const account = privateKeyToAccount5(perpKey);
3563
+ const walletClient = createWalletClient4({
3564
+ chain: { id: 42161, name: "Arbitrum", nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["https://arb1.arbitrum.io/rpc"] } } },
3565
+ transport: http5("https://arb1.arbitrum.io/rpc"),
3566
+ account
3567
+ });
3568
+ this.perpSigner = new HyperliquidSigner(walletClient);
3569
+ this.perpOrders = new OrderManager(this.perpClient, this.perpSigner, config);
3570
+ this.perpPositions = new PositionManager(this.perpClient, this.perpSigner.getAddress());
3571
+ this.perpOnboarding = new PerpOnboarding(this.perpClient, this.perpSigner, config);
3572
+ const recorderKey = perpConfig.perpRelayerKey || this.config.privateKey;
3573
+ this.perpRecorder = new PerpTradeRecorder({
3574
+ privateKey: recorderKey,
3575
+ rpcUrl: this.getRpcUrl(),
3576
+ agentId: BigInt(this.config.agentId),
3577
+ configHash: this.configHash
3578
+ });
3579
+ this.perpWebSocket = new HyperliquidWebSocket(config, this.perpSigner.getAddress(), this.perpClient);
3580
+ this.perpWebSocket.onFillReceived(async (fill) => {
3581
+ console.log(`Perp fill: ${fill.coin} ${fill.side === "B" ? "LONG" : "SHORT"} ${fill.sz}@${fill.px}`);
3582
+ const result = await this.perpRecorder.recordFill(fill);
3583
+ if (result.success) {
3584
+ this.relay?.sendMessage(
3585
+ "perp_fill",
3586
+ "success",
3587
+ "Perp Fill",
3588
+ `${fill.coin} ${fill.side === "B" ? "LONG" : "SHORT"} ${fill.sz} @ $${fill.px}`,
3589
+ { instrument: fill.coin, side: fill.side, size: fill.sz, price: fill.px, txHash: result.txHash }
3590
+ );
3591
+ }
3592
+ });
3593
+ this.perpWebSocket.onLiquidationDetected((instrument, size) => {
3594
+ console.error(`LIQUIDATION: ${instrument} position (${size}) was liquidated`);
3595
+ this.relay?.sendMessage(
3596
+ "perp_liquidation_warning",
3597
+ "error",
3598
+ "Position Liquidated",
3599
+ `${instrument} position of ${Math.abs(size)} was liquidated.`,
3600
+ { instrument, size }
3601
+ );
3602
+ });
3603
+ this.perpWebSocket.onFundingReceived((funding) => {
3604
+ const amount = parseFloat(funding.usdc);
3605
+ if (Math.abs(amount) > 0.01) {
3606
+ this.relay?.sendMessage(
3607
+ "perp_funding",
3608
+ "info",
3609
+ "Funding Payment",
3610
+ `${funding.coin}: ${amount > 0 ? "+" : ""}$${amount.toFixed(4)}`,
3611
+ { instrument: funding.coin, amount: funding.usdc, rate: funding.fundingRate }
3612
+ );
3613
+ }
3614
+ });
3615
+ const onboardingStatus = await this.perpOnboarding.onboard(this.riskUniverse);
3616
+ if (!onboardingStatus.ready) {
3617
+ console.warn(`Perp onboarding incomplete \u2014 trading will be limited`);
3618
+ if (!onboardingStatus.hasBalance) {
3619
+ console.warn(" No USDC balance on Hyperliquid \u2014 deposit required");
3620
+ }
3621
+ if (!onboardingStatus.builderFeeApproved) {
3622
+ console.warn(" Builder fee not approved \u2014 orders may fail");
3623
+ }
3624
+ }
3625
+ try {
3626
+ await this.perpWebSocket.connect();
3627
+ console.log("Perp WebSocket: Connected");
3628
+ } catch (error) {
3629
+ console.warn("Perp WebSocket: Failed to connect (will retry):", error instanceof Error ? error.message : error);
3630
+ }
3631
+ this.perpConnected = true;
3632
+ console.log(`Hyperliquid: Connected (${config.allowedInstruments?.join(", ") || "all instruments"})`);
3633
+ console.log(` Builder: ${config.builderAddress} (${config.builderFeeTenthsBps / 10} bps)`);
3634
+ console.log(` Max leverage: ${config.maxLeverage}x, Max notional: $${config.maxNotionalUSD.toLocaleString()}`);
3635
+ } catch (error) {
3636
+ const message = error instanceof Error ? error.message : String(error);
3637
+ console.error(`Perp initialization failed: ${message}`);
3638
+ console.warn("Perp trading will be disabled for this session");
3639
+ }
3640
+ }
3641
+ /**
3642
+ * Ensure the current wallet is linked to the agent.
3643
+ * If the trading wallet differs from the owner, enters a recovery loop
3644
+ * that waits for the owner to link it from the website.
3645
+ */
3646
+ async ensureWalletLinked() {
3647
+ const agentId = BigInt(this.config.agentId);
3648
+ const address = this.client.address;
3649
+ const isLinked = await this.client.registry.isLinkedWallet(agentId, address);
3650
+ if (!isLinked) {
3651
+ console.log("Wallet not linked, linking now...");
3652
+ const agent = await this.client.registry.getAgent(agentId);
3653
+ if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
3654
+ const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
3655
+ const nonce = await this.client.registry.getNonce(address);
3656
+ const linkMessage = ExagentRegistry.generateLinkMessage(
3657
+ address,
3658
+ agentId,
3659
+ nonce
3660
+ );
3661
+ const linkSignature = await this.client.signMessage({ raw: linkMessage });
3662
+ console.log("");
3663
+ console.log("=== WALLET LINKING REQUIRED ===");
3664
+ console.log("");
3665
+ console.log(" Your trading wallet needs to be linked to your agent.");
3666
+ console.log(" Open the command center and paste the values below.");
3667
+ console.log("");
3668
+ console.log(` Command Center: ${ccUrl}`);
3669
+ console.log("");
3670
+ console.log(" \u2500\u2500 Copy these two values \u2500\u2500");
3671
+ console.log("");
3672
+ console.log(` Wallet: ${address}`);
3673
+ console.log(` Signature: ${linkSignature}`);
3674
+ console.log("");
3675
+ openBrowser(ccUrl);
3676
+ console.log(" Waiting for wallet to be linked... (checking every 15s)");
3677
+ console.log(" Press Ctrl+C to exit.");
3678
+ console.log("");
3679
+ while (true) {
3680
+ await this.sleep(15e3);
3681
+ const linked = await this.client.registry.isLinkedWallet(agentId, address);
3682
+ if (linked) {
3683
+ console.log(" Wallet linked! Continuing setup...");
3684
+ console.log("");
3685
+ return;
3686
+ }
3687
+ process.stdout.write(".");
3688
+ }
3689
+ }
3690
+ await this.client.registry.linkOwnWallet(agentId);
3691
+ console.log("Wallet linked successfully");
3692
+ } else {
3693
+ console.log("Wallet already linked");
3694
+ }
3695
+ }
3696
+ /**
3697
+ * Load risk universe and allowed tokens from on-chain registry.
3698
+ * This prevents the agent from wasting gas on trades that will revert.
3699
+ */
3700
+ async loadTradingRestrictions() {
3701
+ const agentId = BigInt(this.config.agentId);
3702
+ const RISK_UNIVERSE_NAMES = ["Core", "Established", "Derivatives", "Emerging", "Frontier"];
3703
+ try {
3704
+ this.riskUniverse = await this.client.registry.getRiskUniverse(agentId);
3705
+ console.log(`Risk universe: ${RISK_UNIVERSE_NAMES[this.riskUniverse] || this.riskUniverse}`);
3706
+ const configTokens = this.config.allowedTokens || this.getDefaultTokens();
3707
+ const verified = [];
3708
+ for (const token of configTokens) {
3709
+ try {
3710
+ const allowed = await this.client.registry.isTradeAllowed(
3711
+ agentId,
3712
+ token,
3713
+ "0x0000000000000000000000000000000000000000"
3714
+ // zero = check token only
3715
+ );
3716
+ if (allowed) {
3717
+ this.allowedTokens.add(token.toLowerCase());
3718
+ verified.push(token);
3719
+ } else {
3720
+ console.warn(`Token ${token} not allowed for this agent's risk universe \u2014 excluded`);
3721
+ }
3722
+ } catch {
3723
+ this.allowedTokens.add(token.toLowerCase());
3724
+ verified.push(token);
3725
+ }
3726
+ }
3727
+ this.config.allowedTokens = verified;
3728
+ console.log(`Allowed tokens loaded: ${verified.length} tokens verified`);
3729
+ if (this.riskUniverse === 4) {
3730
+ console.warn("Frontier risk universe: vault creation is disabled");
3731
+ }
3732
+ } catch (error) {
3733
+ console.warn(
3734
+ "Could not load trading restrictions from registry (using defaults):",
3735
+ error instanceof Error ? error.message : error
3736
+ );
3737
+ }
3738
+ }
3739
+ /**
3740
+ * Sync the LLM config hash to chain for epoch tracking.
3741
+ * If the wallet has insufficient gas, enters a recovery loop
3742
+ * that waits for the user to fund the wallet.
3743
+ */
3744
+ async syncConfigHash() {
3745
+ const agentId = BigInt(this.config.agentId);
3746
+ const llmMeta = this.llm.getMetadata();
3747
+ this.configHash = ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
3748
+ console.log(`Config hash: ${this.configHash}`);
3749
+ const onChainHash = await this.client.registry.getConfigHash(agentId);
3750
+ if (onChainHash !== this.configHash) {
3751
+ console.log("Config changed, updating on-chain...");
3752
+ try {
3753
+ await this.client.registry.updateConfig(agentId, this.configHash);
3754
+ const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
3755
+ console.log(`Config updated, new epoch started: ${newEpoch}`);
3756
+ } catch (error) {
3757
+ const message = error instanceof Error ? error.message : String(error);
3758
+ if (message.includes("insufficient funds") || message.includes("gas") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
3759
+ const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
3760
+ const chain = base4;
3761
+ const publicClientInstance = createPublicClient5({
3762
+ chain,
3763
+ transport: http5(this.getRpcUrl())
3764
+ });
3765
+ console.log("");
3766
+ console.log("=== ETH NEEDED FOR GAS ===");
3767
+ console.log("");
3768
+ console.log(` Wallet: ${this.client.address}`);
3769
+ console.log(" Your wallet needs ETH to pay for transaction gas.");
3770
+ console.log(" Opening the command center to fund your wallet...");
3771
+ console.log(` ${ccUrl}`);
3772
+ console.log("");
3773
+ openBrowser(ccUrl);
3774
+ console.log(" Waiting for ETH... (checking every 15s)");
3775
+ console.log(" Press Ctrl+C to exit.");
3776
+ console.log("");
3777
+ while (true) {
3778
+ await this.sleep(15e3);
3779
+ const balance = await publicClientInstance.getBalance({
3780
+ address: this.client.address
3781
+ });
3782
+ if (balance > BigInt(0)) {
3783
+ console.log(" ETH detected! Retrying config update...");
3784
+ console.log("");
3785
+ await this.client.registry.updateConfig(agentId, this.configHash);
3786
+ const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
3787
+ console.log(`Config updated, new epoch started: ${newEpoch}`);
3788
+ return;
3789
+ }
3790
+ process.stdout.write(".");
3791
+ }
3792
+ } else {
3793
+ throw error;
3794
+ }
3795
+ }
3796
+ } else {
3797
+ const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
3798
+ console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
3799
+ }
3800
+ }
3801
+ /**
3802
+ * Get the current config hash (for trade execution)
3803
+ */
3804
+ getConfigHash() {
3805
+ return this.configHash;
3806
+ }
3807
+ /**
3808
+ * Start the agent in daemon mode.
3809
+ * The agent enters idle mode and waits for commands from the command center.
3810
+ * Trading begins only when a start_trading command is received.
3811
+ *
3812
+ * If relay is not configured, falls back to immediate trading mode.
3813
+ */
3814
+ async run() {
3815
+ this.processAlive = true;
3816
+ if (this.relay) {
3817
+ console.log("");
3818
+ console.log("Agent is in IDLE mode. Waiting for commands from command center.");
3819
+ console.log("Visit https://exagent.io to start trading from the dashboard.");
3820
+ console.log("");
3821
+ this.mode = "idle";
3822
+ this.sendRelayStatus();
3823
+ this.relay.sendMessage(
3824
+ "system",
3825
+ "success",
3826
+ "Agent Connected",
3827
+ `${this.config.name} is online and waiting for commands.`,
3828
+ { wallet: this.client.address }
3829
+ );
3830
+ while (this.processAlive) {
3831
+ if (this.mode === "trading" && this.isRunning) {
3832
+ try {
3833
+ await this.runCycle();
3834
+ } catch (error) {
3835
+ const message = error instanceof Error ? error.message : String(error);
3836
+ console.error("Error in trading cycle:", message);
3837
+ this.relay?.sendMessage(
3838
+ "system",
3839
+ "error",
3840
+ "Cycle Error",
3841
+ message
3842
+ );
3843
+ }
3844
+ await this.sleep(this.config.trading.tradingIntervalMs);
3845
+ } else {
3846
+ this.sendRelayStatus();
3847
+ await this.sleep(3e4);
3848
+ }
3849
+ }
3850
+ } else {
3851
+ if (this.isRunning) {
3852
+ throw new Error("Agent is already running");
3853
+ }
3854
+ this.isRunning = true;
3855
+ this.mode = "trading";
3856
+ console.log("Starting trading loop...");
3857
+ console.log(`Interval: ${this.config.trading.tradingIntervalMs}ms`);
3858
+ while (this.isRunning) {
3859
+ try {
3860
+ await this.runCycle();
3861
+ } catch (error) {
3862
+ console.error("Error in trading cycle:", error);
3863
+ }
3864
+ await this.sleep(this.config.trading.tradingIntervalMs);
3865
+ }
3866
+ }
3867
+ }
3868
+ /**
3869
+ * Handle a command from the command center
3870
+ */
3871
+ async handleCommand(cmd) {
3872
+ console.log(`Command received: ${cmd.type}`);
3873
+ try {
3874
+ switch (cmd.type) {
3875
+ case "start_trading":
3876
+ if (this.mode === "trading") {
3877
+ this.relay?.sendCommandResult(cmd.id, true, "Already trading");
3878
+ return;
3879
+ }
3880
+ this.mode = "trading";
3881
+ this.isRunning = true;
3882
+ console.log("Trading started via command center");
3883
+ this.relay?.sendCommandResult(cmd.id, true, "Trading started");
3884
+ this.relay?.sendMessage(
3885
+ "system",
3886
+ "success",
3887
+ "Trading Started",
3888
+ "Agent is now actively trading."
3889
+ );
3890
+ this.sendRelayStatus();
3891
+ break;
3892
+ case "stop_trading":
3893
+ if (this.mode === "idle") {
3894
+ this.relay?.sendCommandResult(cmd.id, true, "Already idle");
3895
+ return;
3896
+ }
3897
+ this.mode = "idle";
3898
+ this.isRunning = false;
3899
+ console.log("Trading stopped via command center");
3900
+ this.relay?.sendCommandResult(cmd.id, true, "Trading stopped");
3901
+ this.relay?.sendMessage(
3902
+ "system",
3903
+ "info",
3904
+ "Trading Stopped",
3905
+ "Agent is now idle. Send start_trading to resume."
3906
+ );
3907
+ this.sendRelayStatus();
3908
+ break;
3909
+ case "update_risk_params": {
3910
+ const params = cmd.params || {};
3911
+ let updated = false;
3912
+ if (params.maxPositionSizeBps !== void 0) {
3913
+ const value = Number(params.maxPositionSizeBps);
3914
+ if (isNaN(value) || value < 100 || value > 1e4) {
3915
+ this.relay?.sendCommandResult(cmd.id, false, "maxPositionSizeBps must be 100-10000");
3916
+ break;
3917
+ }
3918
+ this.config.trading.maxPositionSizeBps = value;
3919
+ updated = true;
3920
+ }
3921
+ if (params.maxDailyLossBps !== void 0) {
3922
+ const value = Number(params.maxDailyLossBps);
3923
+ if (isNaN(value) || value < 50 || value > 5e3) {
3924
+ this.relay?.sendCommandResult(cmd.id, false, "maxDailyLossBps must be 50-5000");
3925
+ break;
3926
+ }
3927
+ this.config.trading.maxDailyLossBps = value;
3928
+ updated = true;
3929
+ }
3930
+ if (updated) {
3931
+ this.riskManager = new RiskManager(this.config.trading);
3932
+ console.log("Risk params updated via command center");
3933
+ this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
3934
+ this.relay?.sendMessage(
3935
+ "config_updated",
3936
+ "info",
3937
+ "Risk Parameters Updated",
3938
+ `Max position: ${this.config.trading.maxPositionSizeBps / 100}%, Max daily loss: ${this.config.trading.maxDailyLossBps / 100}%`
3939
+ );
3940
+ } else {
3941
+ this.relay?.sendCommandResult(cmd.id, false, "No valid parameters provided");
3942
+ }
3943
+ break;
3944
+ }
3945
+ case "update_trading_interval": {
3946
+ const intervalMs = Number(cmd.params?.intervalMs);
3947
+ if (intervalMs && intervalMs >= 1e3) {
3948
+ this.config.trading.tradingIntervalMs = intervalMs;
3949
+ console.log(`Trading interval updated to ${intervalMs}ms`);
3950
+ this.relay?.sendCommandResult(cmd.id, true, `Interval set to ${intervalMs}ms`);
3951
+ } else {
3952
+ this.relay?.sendCommandResult(cmd.id, false, "Invalid interval (minimum 1000ms)");
3953
+ }
3954
+ break;
3955
+ }
3956
+ case "create_vault": {
3957
+ const result = await this.createVault();
3958
+ this.relay?.sendCommandResult(
3959
+ cmd.id,
3960
+ result.success,
3961
+ result.success ? `Vault created: ${result.vaultAddress}` : result.error
3962
+ );
3963
+ if (result.success) {
3964
+ this.relay?.sendMessage(
3965
+ "vault_created",
3966
+ "success",
3967
+ "Vault Created",
3968
+ `Vault deployed at ${result.vaultAddress}`,
3969
+ { vaultAddress: result.vaultAddress }
3970
+ );
3971
+ }
3972
+ break;
3973
+ }
3974
+ case "enable_hyperliquid":
3975
+ if (this.perpConnected) {
3976
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid already connected");
3977
+ break;
3978
+ }
3979
+ if (!this.config.perp?.enabled) {
3980
+ this.relay?.sendCommandResult(cmd.id, false, "Perp trading not configured in agent config");
3981
+ break;
3982
+ }
3983
+ if (this.riskUniverse < 2) {
3984
+ this.relay?.sendCommandResult(cmd.id, false, `Risk universe ${this.riskUniverse} too low (need >= 2)`);
3985
+ break;
3986
+ }
3987
+ try {
3988
+ await this.initializePerp();
3989
+ if (this.perpConnected) {
3990
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid connected");
3991
+ this.relay?.sendMessage(
3992
+ "system",
3993
+ "success",
3994
+ "Hyperliquid Enabled",
3995
+ 'Hyperliquid infrastructure connected. Agent can now include perp signals in its strategy. Use "Start Perp Trading" to mandate a dedicated perp cycle.'
3996
+ );
3997
+ } else {
3998
+ this.relay?.sendCommandResult(cmd.id, false, "Failed to connect to Hyperliquid");
3999
+ }
4000
+ } catch (error) {
4001
+ const msg = error instanceof Error ? error.message : String(error);
4002
+ this.relay?.sendCommandResult(cmd.id, false, `Hyperliquid init failed: ${msg}`);
4003
+ }
4004
+ this.sendRelayStatus();
4005
+ break;
4006
+ case "disable_hyperliquid":
4007
+ if (!this.perpConnected) {
4008
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid already disconnected");
4009
+ break;
4010
+ }
4011
+ this.perpTradingActive = false;
4012
+ if (this.perpWebSocket) {
4013
+ this.perpWebSocket.disconnect();
4014
+ }
4015
+ if (this.perpRecorder) {
4016
+ this.perpRecorder.stop();
4017
+ }
4018
+ this.perpConnected = false;
4019
+ console.log("Hyperliquid disabled via command center");
4020
+ this.relay?.sendCommandResult(cmd.id, true, "Hyperliquid disconnected");
4021
+ this.relay?.sendMessage(
4022
+ "system",
4023
+ "info",
4024
+ "Hyperliquid Disabled",
4025
+ "Hyperliquid infrastructure disconnected. Agent will trade spot only."
4026
+ );
4027
+ this.sendRelayStatus();
4028
+ break;
4029
+ case "start_perp_trading":
4030
+ if (!this.perpConnected) {
4031
+ this.relay?.sendCommandResult(cmd.id, false, "Hyperliquid not connected. Enable Hyperliquid first.");
4032
+ break;
4033
+ }
4034
+ if (this.perpTradingActive) {
4035
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading already active");
4036
+ break;
4037
+ }
4038
+ this.perpTradingActive = true;
4039
+ if (this.mode !== "trading") {
4040
+ this.mode = "trading";
4041
+ this.isRunning = true;
4042
+ }
4043
+ console.log("Perp trading mandated via command center");
4044
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading cycle active");
4045
+ this.relay?.sendMessage(
4046
+ "system",
4047
+ "success",
4048
+ "Perp Trading Active",
4049
+ "Dedicated perp trading cycle is now running every interval."
4050
+ );
4051
+ this.sendRelayStatus();
4052
+ break;
4053
+ case "stop_perp_trading":
4054
+ if (!this.perpTradingActive) {
4055
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading already stopped");
4056
+ break;
4057
+ }
4058
+ this.perpTradingActive = false;
4059
+ console.log("Perp trading cycle stopped via command center");
4060
+ this.relay?.sendCommandResult(cmd.id, true, "Perp trading cycle stopped");
4061
+ this.relay?.sendMessage(
4062
+ "system",
4063
+ "info",
4064
+ "Perp Trading Stopped",
4065
+ "Dedicated perp cycle stopped. Hyperliquid remains connected \u2014 strategy can still include perp signals."
4066
+ );
4067
+ this.sendRelayStatus();
4068
+ break;
4069
+ case "update_perp_params": {
4070
+ const perpParams = cmd.params || {};
4071
+ let perpUpdated = false;
4072
+ if (this.config.perp && perpParams.maxLeverage !== void 0) {
4073
+ const val = Number(perpParams.maxLeverage);
4074
+ if (val >= 1 && val <= 50) {
4075
+ this.config.perp.maxLeverage = val;
4076
+ perpUpdated = true;
4077
+ }
4078
+ }
4079
+ if (this.config.perp && perpParams.maxNotionalUSD !== void 0) {
4080
+ const val = Number(perpParams.maxNotionalUSD);
4081
+ if (val >= 100) {
4082
+ this.config.perp.maxNotionalUSD = val;
4083
+ perpUpdated = true;
4084
+ }
4085
+ }
4086
+ if (perpUpdated) {
4087
+ this.relay?.sendCommandResult(cmd.id, true, "Perp parameters updated");
4088
+ this.relay?.sendMessage(
4089
+ "config_updated",
4090
+ "info",
4091
+ "Perp Params Updated",
4092
+ `Max leverage: ${this.config.perp?.maxLeverage}x, Max notional: $${this.config.perp?.maxNotionalUSD?.toLocaleString()}`
4093
+ );
4094
+ } else {
4095
+ this.relay?.sendCommandResult(cmd.id, false, "No valid perp parameters provided");
4096
+ }
4097
+ break;
4098
+ }
4099
+ case "refresh_status":
4100
+ this.sendRelayStatus();
4101
+ this.relay?.sendCommandResult(cmd.id, true, "Status refreshed");
4102
+ break;
4103
+ case "shutdown":
4104
+ console.log("Shutdown requested via command center");
4105
+ this.relay?.sendCommandResult(cmd.id, true, "Shutting down");
4106
+ this.relay?.sendMessage(
4107
+ "system",
4108
+ "info",
4109
+ "Shutting Down",
4110
+ "Agent is shutting down. Restart manually to reconnect."
4111
+ );
4112
+ await this.sleep(1e3);
4113
+ this.stop();
4114
+ break;
4115
+ default:
4116
+ console.warn(`Unknown command: ${cmd.type}`);
4117
+ this.relay?.sendCommandResult(cmd.id, false, `Unknown command: ${cmd.type}`);
4118
+ }
4119
+ } catch (error) {
4120
+ const message = error instanceof Error ? error.message : String(error);
4121
+ console.error(`Command ${cmd.type} failed:`, message);
4122
+ this.relay?.sendCommandResult(cmd.id, false, message);
4123
+ }
4124
+ }
4125
+ /**
4126
+ * Send current status to the relay
4127
+ */
4128
+ sendRelayStatus() {
4129
+ if (!this.relay) return;
4130
+ const vaultConfig = this.config.vault || { policy: "disabled" };
4131
+ const status = {
4132
+ mode: this.mode,
4133
+ agentId: String(this.config.agentId),
4134
+ wallet: this.client?.address,
4135
+ cycleCount: this.cycleCount,
4136
+ lastCycleAt: this.lastCycleAt,
4137
+ tradingIntervalMs: this.config.trading.tradingIntervalMs,
4138
+ portfolioValue: this.lastPortfolioValue,
4139
+ ethBalance: this.lastEthBalance,
4140
+ llm: {
4141
+ provider: this.config.llm.provider,
4142
+ model: this.config.llm.model || "default"
4143
+ },
4144
+ risk: this.riskManager?.getStatus(this.lastPortfolioValue) || {
4145
+ dailyPnL: 0,
4146
+ dailyLossLimit: 0,
4147
+ isLimitHit: false
4148
+ },
4149
+ vault: {
4150
+ policy: vaultConfig.policy,
4151
+ hasVault: false,
4152
+ vaultAddress: null
4153
+ },
4154
+ perp: this.perpConnected ? {
4155
+ enabled: true,
4156
+ trading: this.perpTradingActive,
4157
+ equity: 0,
4158
+ unrealizedPnl: 0,
4159
+ marginUsed: 0,
4160
+ openPositions: 0,
4161
+ effectiveLeverage: 0,
4162
+ pendingRecords: this.perpRecorder?.pendingRetries ?? 0
4163
+ } : void 0
4164
+ };
4165
+ if (this.perpConnected && this.perpPositions && status.perp) {
4166
+ this.perpPositions.getAccountSummary().then((account) => {
4167
+ if (status.perp) {
4168
+ status.perp.equity = account.totalEquity;
4169
+ status.perp.unrealizedPnl = account.totalUnrealizedPnl;
4170
+ status.perp.marginUsed = account.totalMarginUsed;
4171
+ status.perp.effectiveLeverage = account.effectiveLeverage;
4172
+ }
4173
+ }).catch(() => {
4174
+ });
4175
+ this.perpPositions.getPositionCount().then((count) => {
4176
+ if (status.perp) status.perp.openPositions = count;
4177
+ }).catch(() => {
4178
+ });
4179
+ }
4180
+ this.relay.sendHeartbeat(status);
4181
+ }
4182
+ /**
4183
+ * Run a single trading cycle
4184
+ */
4185
+ async runCycle() {
4186
+ console.log(`
4187
+ --- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
4188
+ this.cycleCount++;
4189
+ this.lastCycleAt = Date.now();
4190
+ const tokens = this.config.allowedTokens || this.getDefaultTokens();
4191
+ const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
4192
+ console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
4193
+ this.lastPortfolioValue = marketData.portfolioValue;
4194
+ const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4195
+ this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
4196
+ const fundsOk = this.checkFundsLow(marketData);
4197
+ if (!fundsOk) {
4198
+ console.warn("Skipping trading cycle \u2014 ETH balance critically low");
4199
+ this.sendRelayStatus();
4200
+ return;
4201
+ }
4202
+ let signals;
4203
+ try {
4204
+ signals = await this.strategy(marketData, this.llm, this.config);
4205
+ } catch (error) {
4206
+ const message = error instanceof Error ? error.message : String(error);
4207
+ console.error("LLM/strategy error:", message);
4208
+ this.relay?.sendMessage(
4209
+ "llm_error",
4210
+ "error",
4211
+ "Strategy Error",
4212
+ message
4213
+ );
4214
+ return;
4215
+ }
4216
+ console.log(`Strategy generated ${signals.length} signals`);
4217
+ const filteredSignals = this.riskManager.filterSignals(signals, marketData);
4218
+ console.log(`${filteredSignals.length} signals passed risk checks`);
4219
+ if (this.riskManager.getStatus(marketData.portfolioValue).isLimitHit) {
4220
+ this.relay?.sendMessage(
4221
+ "risk_limit_hit",
4222
+ "warning",
4223
+ "Risk Limit Hit",
4224
+ `Daily loss limit reached: $${this.riskManager.getStatus(marketData.portfolioValue).dailyPnL.toFixed(2)}`
4225
+ );
4226
+ }
4227
+ if (filteredSignals.length > 0) {
4228
+ const vaultStatus = await this.vaultManager?.getVaultStatus();
4229
+ if (vaultStatus?.hasVault && this.vaultManager?.preferVaultTrading) {
4230
+ console.log(`Trading through vault: ${vaultStatus.vaultAddress}`);
4231
+ }
4232
+ const preTradePortfolioValue = marketData.portfolioValue;
4233
+ const results = await this.executor.executeAll(filteredSignals);
4234
+ let totalFeesUSD = 0;
4235
+ for (const result of results) {
4236
+ if (result.success) {
4237
+ console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
4238
+ const feeCostBps = 20;
4239
+ const signalPrice = marketData.prices[result.signal.tokenIn.toLowerCase()] || 0;
4240
+ const amountUSD = Number(result.signal.amountIn) / Math.pow(10, getTokenDecimals(result.signal.tokenIn)) * signalPrice;
4241
+ const feeCostUSD = amountUSD * feeCostBps / 1e4;
4242
+ totalFeesUSD += feeCostUSD;
4243
+ this.riskManager.updateFees(feeCostUSD);
4244
+ this.relay?.sendMessage(
4245
+ "trade_executed",
4246
+ "success",
4247
+ "Trade Executed",
4248
+ `${result.signal.action.toUpperCase()}: ${result.signal.reasoning || "No reason provided"}`,
4249
+ {
4250
+ action: result.signal.action,
4251
+ txHash: result.txHash,
4252
+ tokenIn: result.signal.tokenIn,
4253
+ tokenOut: result.signal.tokenOut
4254
+ }
4255
+ );
4256
+ } else {
4257
+ console.warn(`Trade failed: ${result.error}`);
4258
+ this.relay?.sendMessage(
4259
+ "trade_failed",
4260
+ "error",
4261
+ "Trade Failed",
4262
+ result.error || "Unknown error",
4263
+ { action: result.signal.action }
4264
+ );
4265
+ }
4266
+ }
4267
+ const postTokens = this.config.allowedTokens || this.getDefaultTokens();
4268
+ const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
4269
+ const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
4270
+ this.riskManager.updatePnL(marketPnL);
4271
+ if (marketPnL !== 0) {
4272
+ console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
4273
+ }
4274
+ this.lastPortfolioValue = postTradeData.portfolioValue;
4275
+ const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4276
+ this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
4277
+ }
4278
+ if (this.perpConnected && this.perpTradingActive) {
4279
+ try {
4280
+ await this.runPerpCycle();
4281
+ } catch (error) {
4282
+ const message = error instanceof Error ? error.message : String(error);
4283
+ console.error("Error in perp cycle:", message);
4284
+ this.relay?.sendMessage("system", "error", "Perp Cycle Error", message);
4285
+ }
4286
+ }
4287
+ this.sendRelayStatus();
4288
+ }
4289
+ /**
4290
+ * Run a single perp trading cycle.
4291
+ * Fetches market data, positions, calls perp strategy, applies risk filters, executes.
4292
+ * Fills arrive async via WebSocket and are auto-recorded on Base.
4293
+ */
4294
+ async runPerpCycle() {
4295
+ if (!this.perpClient || !this.perpPositions || !this.perpOrders || !this.perpConnected) return;
4296
+ const perpConfig = this.config.perp;
4297
+ if (!perpConfig?.enabled) return;
4298
+ console.log(" [PERP] Running perp cycle...");
4299
+ const [positions, account] = await Promise.all([
4300
+ this.perpPositions.getPositions(true),
4301
+ this.perpPositions.getAccountSummary(true)
4302
+ ]);
4303
+ console.log(` [PERP] Equity: $${account.totalEquity.toFixed(2)}, Positions: ${positions.length}, Leverage: ${account.effectiveLeverage.toFixed(1)}x`);
4304
+ const dangerousPositions = await this.perpPositions.getDangerousPositions(0.7);
4305
+ for (const pos of dangerousPositions) {
4306
+ console.warn(` [PERP] WARNING: ${pos.instrument} near liquidation`);
4307
+ this.relay?.sendMessage(
4308
+ "perp_liquidation_warning",
4309
+ "warning",
4310
+ "Near Liquidation",
4311
+ `${pos.instrument} ${pos.size > 0 ? "LONG" : "SHORT"} \u2014 close to liquidation price $${pos.liquidationPrice.toFixed(2)}`,
4312
+ { instrument: pos.instrument, liquidationPrice: pos.liquidationPrice, markPrice: pos.markPrice }
4313
+ );
4314
+ }
4315
+ const instruments = perpConfig.allowedInstruments || ["ETH", "BTC", "SOL"];
4316
+ const marketData = await this.perpClient.getMarketData(instruments);
4317
+ if (!this.perpStrategy) {
4318
+ return;
4319
+ }
4320
+ let signals;
4321
+ try {
4322
+ signals = await this.perpStrategy(marketData, positions, account, this.llm, this.config);
4323
+ } catch (error) {
4324
+ const message = error instanceof Error ? error.message : String(error);
4325
+ console.error(" [PERP] Strategy error:", message);
4326
+ return;
4327
+ }
4328
+ console.log(` [PERP] Strategy generated ${signals.length} signals`);
4329
+ const maxLeverage = perpConfig.maxLeverage ?? 10;
4330
+ const maxNotionalUSD = perpConfig.maxNotionalUSD ?? 5e4;
4331
+ const filteredSignals = this.riskManager.filterPerpSignals(signals, positions, account, maxLeverage, maxNotionalUSD);
4332
+ console.log(` [PERP] ${filteredSignals.length} signals passed risk checks`);
4333
+ for (const signal of filteredSignals) {
4334
+ if (signal.action === "hold") continue;
4335
+ if (signal.price === 0) {
4336
+ const md = marketData.find((m) => m.instrument === signal.instrument);
4337
+ if (md) signal.price = md.midPrice;
4338
+ }
4339
+ const result = await this.perpOrders.placeOrder(signal);
4340
+ if (result.success) {
4341
+ console.log(` [PERP] Order placed: ${signal.instrument} ${signal.action} \u2014 ${result.status}`);
4342
+ } else {
4343
+ console.warn(` [PERP] Order failed: ${signal.instrument} ${signal.action} \u2014 ${result.error}`);
4344
+ }
4345
+ }
4346
+ }
4347
+ /**
4348
+ * Check if ETH balance is below threshold and notify.
4349
+ * Returns true if trading should continue, false if ETH is critically low.
4350
+ */
4351
+ checkFundsLow(marketData) {
4352
+ const ethBalance = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4353
+ const ethAmount = Number(ethBalance) / 1e18;
4354
+ if (ethAmount < FUNDS_CRITICAL_THRESHOLD) {
4355
+ console.error(`ETH balance critically low: ${ethAmount.toFixed(6)} ETH \u2014 halting trading`);
4356
+ this.relay?.sendMessage(
4357
+ "funds_low",
4358
+ "error",
4359
+ "Funds Critical",
4360
+ `ETH balance is ${ethAmount.toFixed(6)} ETH (below ${FUNDS_CRITICAL_THRESHOLD} ETH minimum). Trading halted \u2014 fund your wallet.`,
4361
+ {
4362
+ ethBalance: ethAmount.toFixed(6),
4363
+ wallet: this.client.address,
4364
+ threshold: FUNDS_CRITICAL_THRESHOLD
4365
+ }
4366
+ );
4367
+ return false;
4368
+ }
4369
+ if (ethAmount < FUNDS_LOW_THRESHOLD) {
4370
+ this.relay?.sendMessage(
4371
+ "funds_low",
4372
+ "warning",
4373
+ "Low Funds",
4374
+ `ETH balance is ${ethAmount.toFixed(6)} ETH. Fund your trading wallet to continue trading.`,
4375
+ {
4376
+ ethBalance: ethAmount.toFixed(6),
4377
+ wallet: this.client.address,
4378
+ threshold: FUNDS_LOW_THRESHOLD
4379
+ }
4380
+ );
4381
+ }
4382
+ return true;
4383
+ }
4384
+ /**
4385
+ * Stop the agent process completely
4386
+ */
4387
+ stop() {
4388
+ console.log("Stopping agent...");
4389
+ this.isRunning = false;
4390
+ this.processAlive = false;
4391
+ this.mode = "idle";
4392
+ if (this.perpWebSocket) {
4393
+ this.perpWebSocket.disconnect();
4394
+ }
4395
+ if (this.perpRecorder) {
4396
+ this.perpRecorder.stop();
4397
+ }
4398
+ if (this.relay) {
4399
+ this.relay.disconnect();
4400
+ }
4401
+ }
4402
+ /**
4403
+ * Get RPC URL from environment or default
4404
+ */
4405
+ getRpcUrl() {
4406
+ return getRpcUrl();
4407
+ }
4408
+ /**
4409
+ * Default tokens to track.
4410
+ * These are validated against the on-chain registry's isTradeAllowed() during init —
4411
+ * agents in restricted risk universes will have ineligible tokens filtered out.
4412
+ */
4413
+ getDefaultTokens() {
4414
+ return [
4415
+ // Core (0)
4416
+ "0x4200000000000000000000000000000000000006",
4417
+ // WETH
4418
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
4419
+ // USDC
4420
+ "0x2Ae3F1Ec7F1F5012CFEab0185bFC7aa3cf0DEC22",
4421
+ // cbETH
4422
+ "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
4423
+ // cbBTC
4424
+ "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
4425
+ // wstETH
4426
+ "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
4427
+ // DAI
4428
+ "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
4429
+ // USDT
4430
+ "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA",
4431
+ // USDbC
4432
+ "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42",
4433
+ // EURC
4434
+ "0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c",
4435
+ // rETH
4436
+ "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c",
4437
+ // WBTC
4438
+ // Established (1) - filtered by risk universe at init
4439
+ "0x940181a94A35A4569E4529A3CDfB74e38FD98631",
4440
+ // AERO
4441
+ "0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A",
4442
+ // weETH
4443
+ "0x2416092f143378750bb29b79eD961ab195CcEea5",
4444
+ // ezETH
4445
+ "0xA88594D404727625A9437C3f886C7643872296AE",
4446
+ // WELL
4447
+ "0xBAa5CC21fd487B8Fcc2F632f3F4E8D37262a0842",
4448
+ // MORPHO
4449
+ "0x88Fb150BDc53A65fe94Dea0c9BA0a6dAf8C6e196",
4450
+ // LINK
4451
+ "0xc3De830EA07524a0761646a6a4e4be0e114a3C83",
4452
+ // UNI
4453
+ "0x63706e401c06ac8513145b7687A14804d17f814b",
4454
+ // AAVE
4455
+ "0x9e1028F5F1D5eDE59748FFceE5532509976840E0",
4456
+ // COMP
4457
+ "0x4158734D47Fc9692176B5085E0F52ee0Da5d47F1",
4458
+ // BAL
4459
+ "0x8Ee73c484A26e0A5df2Ee2a4960B789967dd0415",
4460
+ // CRV
4461
+ "0x22e6966B799c4D5B13BE962E1D117b56327FDa66",
4462
+ // SNX
4463
+ // Derivatives (2) - filtered by risk universe at init
4464
+ "0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b",
4465
+ // VIRTUAL
4466
+ "0x4F9Fd6Be4a90f2620860d680c0d4d5Fb53d1A825",
4467
+ // AIXBT
4468
+ "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
4469
+ // DEGEN
4470
+ "0x0578d8A44db98B23BF096A382e016e29a5Ce0ffe",
4471
+ // HIGHER
4472
+ "0x1bc0c42215582d5A085795f4baDbaC3ff36d1Bcb",
4473
+ // CLANKER
4474
+ // Emerging (3) - filtered by risk universe at init
4475
+ "0x532f27101965dd16442E59d40670FaF5eBB142E4",
4476
+ // BRETT
4477
+ "0xAC1Bd2486aAf3B5C0fc3Fd868558b082a531B2B4",
4478
+ // TOSHI
4479
+ "0x6921B130D297cc43754afba22e5EAc0FBf8Db75b",
4480
+ // DOGINME
4481
+ "0xB1a03EdA10342529bBF8EB700a06C60441fEf25d",
4482
+ // MIGGLES
4483
+ "0x7F12d13B34F5F4f0a9449c16Bcd42f0da47AF200",
4484
+ // NORMIE
4485
+ "0x27D2DECb4bFC9C76F0309b8E88dec3a601Fe25a8",
4486
+ // BALD
4487
+ "0x768BE13e1680b5ebE0024C42c896E3dB59ec0149"
4488
+ // SKI
4489
+ ];
4490
+ }
4491
+ sleep(ms) {
4492
+ return new Promise((resolve) => setTimeout(resolve, ms));
4493
+ }
4494
+ /**
4495
+ * Get current status
4496
+ */
4497
+ getStatus() {
4498
+ const vaultConfig = this.config.vault || { policy: "disabled" };
4499
+ return {
4500
+ isRunning: this.isRunning,
4501
+ mode: this.mode,
4502
+ agentId: Number(this.config.agentId),
4503
+ wallet: this.client?.address || "not initialized",
4504
+ llm: {
4505
+ provider: this.config.llm.provider,
4506
+ model: this.config.llm.model || "default"
4507
+ },
4508
+ configHash: this.configHash || "not initialized",
4509
+ risk: this.riskManager?.getStatus(this.lastPortfolioValue) || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false },
4510
+ vault: {
4511
+ policy: vaultConfig.policy,
4512
+ hasVault: false,
4513
+ // Updated async via getVaultStatus
4514
+ vaultAddress: null
4515
+ },
4516
+ relay: {
4517
+ connected: this.relay?.isConnected || false
4518
+ },
4519
+ cycleCount: this.cycleCount
4520
+ };
4521
+ }
4522
+ /**
4523
+ * Get detailed vault status (async)
4524
+ */
4525
+ async getVaultStatus() {
4526
+ if (!this.vaultManager) {
4527
+ return null;
4528
+ }
4529
+ return this.vaultManager.getVaultStatus();
4530
+ }
4531
+ /**
4532
+ * Manually trigger vault creation (for 'manual' policy)
4533
+ */
4534
+ async createVault() {
4535
+ if (!this.vaultManager) {
4536
+ return { success: false, error: "Vault manager not initialized" };
4537
+ }
4538
+ const policy = this.config.vault?.policy || "disabled";
4539
+ if (policy === "disabled") {
4540
+ return { success: false, error: "Vault creation is disabled by policy" };
4541
+ }
4542
+ return this.vaultManager.createVault();
4543
+ }
4544
+ };
4545
+
4546
+ // src/secure-env.ts
4547
+ import * as crypto from "crypto";
4548
+ import * as fs from "fs";
4549
+ import * as path from "path";
4550
+ var ALGORITHM = "aes-256-gcm";
4551
+ var PBKDF2_ITERATIONS = 1e5;
4552
+ var SALT_LENGTH = 32;
4553
+ var IV_LENGTH = 16;
4554
+ var KEY_LENGTH = 32;
4555
+ var SENSITIVE_PATTERNS = [
4556
+ /PRIVATE_KEY$/i,
4557
+ /_API_KEY$/i,
4558
+ /API_KEY$/i,
4559
+ /_SECRET$/i,
4560
+ /^OPENAI_API_KEY$/i,
4561
+ /^ANTHROPIC_API_KEY$/i,
4562
+ /^GOOGLE_AI_API_KEY$/i,
4563
+ /^DEEPSEEK_API_KEY$/i,
4564
+ /^MISTRAL_API_KEY$/i,
4565
+ /^GROQ_API_KEY$/i,
4566
+ /^TOGETHER_API_KEY$/i
4567
+ ];
4568
+ function isSensitiveKey(key) {
4569
+ return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
4570
+ }
4571
+ function deriveKey(passphrase, salt) {
4572
+ return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
4573
+ }
4574
+ function encryptValue(value, key) {
4575
+ const iv = crypto.randomBytes(IV_LENGTH);
4576
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
4577
+ let encrypted = cipher.update(value, "utf8", "hex");
4578
+ encrypted += cipher.final("hex");
4579
+ const tag = cipher.getAuthTag();
4580
+ return {
4581
+ iv: iv.toString("hex"),
4582
+ encrypted,
4583
+ tag: tag.toString("hex")
4584
+ };
4585
+ }
4586
+ function decryptValue(encrypted, key, iv, tag) {
4587
+ const decipher = crypto.createDecipheriv(
4588
+ ALGORITHM,
4589
+ key,
4590
+ Buffer.from(iv, "hex")
4591
+ );
4592
+ decipher.setAuthTag(Buffer.from(tag, "hex"));
4593
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
4594
+ decrypted += decipher.final("utf8");
4595
+ return decrypted;
4596
+ }
4597
+ function parseEnvFile(content) {
4598
+ const entries = [];
4599
+ for (const line of content.split("\n")) {
4600
+ const trimmed = line.trim();
4601
+ if (!trimmed || trimmed.startsWith("#")) continue;
4602
+ const eqIndex = trimmed.indexOf("=");
4603
+ if (eqIndex === -1) continue;
4604
+ const key = trimmed.slice(0, eqIndex).trim();
4605
+ let value = trimmed.slice(eqIndex + 1).trim();
4606
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4607
+ value = value.slice(1, -1);
4608
+ }
4609
+ if (key && value) {
4610
+ entries.push({ key, value });
4611
+ }
4612
+ }
4613
+ return entries;
4614
+ }
4615
+ function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
4616
+ if (!fs.existsSync(envPath)) {
4617
+ throw new Error(`File not found: ${envPath}`);
4618
+ }
4619
+ const content = fs.readFileSync(envPath, "utf-8");
4620
+ const entries = parseEnvFile(content);
4621
+ if (entries.length === 0) {
4622
+ throw new Error("No environment variables found in file");
4623
+ }
4624
+ const salt = crypto.randomBytes(SALT_LENGTH);
4625
+ const key = deriveKey(passphrase, salt);
4626
+ const encryptedEntries = entries.map(({ key: envKey, value }) => {
4627
+ if (isSensitiveKey(envKey)) {
4628
+ const { iv, encrypted, tag } = encryptValue(value, key);
4629
+ return {
4630
+ key: envKey,
4631
+ value: encrypted,
4632
+ encrypted: true,
4633
+ iv,
4634
+ tag
4635
+ };
4636
+ }
4637
+ return {
4638
+ key: envKey,
4639
+ value,
4640
+ encrypted: false
4641
+ };
4642
+ });
4643
+ const encryptedEnv = {
4644
+ version: 1,
4645
+ salt: salt.toString("hex"),
4646
+ entries: encryptedEntries
4647
+ };
4648
+ const encPath = envPath + ".enc";
4649
+ fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
4650
+ if (deleteOriginal) {
4651
+ fs.unlinkSync(envPath);
4652
+ }
4653
+ const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
4654
+ const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
4655
+ console.log(
4656
+ `Encrypted ${sensitiveCount} sensitive values (${plainCount} non-sensitive kept as plaintext)`
4657
+ );
4658
+ return encPath;
4659
+ }
4660
+ function decryptEnvFile(encPath, passphrase) {
4661
+ if (!fs.existsSync(encPath)) {
4662
+ throw new Error(`Encrypted env file not found: ${encPath}`);
4663
+ }
4664
+ const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
4665
+ if (content.version !== 1) {
4666
+ throw new Error(`Unsupported encrypted env version: ${content.version}`);
4667
+ }
4668
+ const salt = Buffer.from(content.salt, "hex");
4669
+ const key = deriveKey(passphrase, salt);
4670
+ const result = {};
4671
+ for (const entry of content.entries) {
4672
+ if (entry.encrypted) {
4673
+ if (!entry.iv || !entry.tag) {
4674
+ throw new Error(`Missing encryption metadata for ${entry.key}`);
4675
+ }
4676
+ try {
4677
+ result[entry.key] = decryptValue(entry.value, key, entry.iv, entry.tag);
4678
+ } catch {
4679
+ throw new Error(
4680
+ `Failed to decrypt ${entry.key}. Wrong passphrase or corrupted data.`
4681
+ );
4682
+ }
4683
+ } else {
4684
+ result[entry.key] = entry.value;
4685
+ }
4686
+ }
4687
+ return result;
4688
+ }
4689
+ function loadSecureEnv(basePath, passphrase) {
4690
+ const encPath = path.join(basePath, ".env.enc");
4691
+ const envPath = path.join(basePath, ".env");
4692
+ if (fs.existsSync(encPath)) {
4693
+ if (!passphrase) {
4694
+ passphrase = process.env.EXAGENT_PASSPHRASE;
4695
+ }
4696
+ if (!passphrase) {
4697
+ console.warn("");
4698
+ console.warn("WARNING: Found .env.enc but no passphrase provided.");
4699
+ console.warn(" Set EXAGENT_PASSPHRASE environment variable or");
4700
+ console.warn(" pass --passphrase when running the agent.");
4701
+ console.warn(" Falling back to plaintext .env file.");
4702
+ console.warn("");
4703
+ } else {
4704
+ const vars = decryptEnvFile(encPath, passphrase);
4705
+ for (const [key, value] of Object.entries(vars)) {
4706
+ process.env[key] = value;
4707
+ }
4708
+ return true;
4709
+ }
4710
+ }
4711
+ if (fs.existsSync(envPath)) {
4712
+ const content = fs.readFileSync(envPath, "utf-8");
4713
+ const entries = parseEnvFile(content);
4714
+ const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
4715
+ if (sensitiveKeys.length > 0) {
4716
+ console.warn("");
4717
+ console.warn("WARNING: Sensitive values stored in plaintext .env file:");
4718
+ for (const key of sensitiveKeys) {
4719
+ console.warn(` - ${key}`);
4720
+ }
4721
+ console.warn("");
4722
+ console.warn(' Run "npx @exagent/agent encrypt" to secure your keys.');
4723
+ console.warn("");
4724
+ }
4725
+ return false;
4726
+ }
4727
+ return false;
4728
+ }
4729
+
4730
+ export {
4731
+ BaseLLMAdapter,
4732
+ OpenAIAdapter,
4733
+ AnthropicAdapter,
4734
+ GoogleAdapter,
4735
+ DeepSeekAdapter,
4736
+ MistralAdapter,
4737
+ GroqAdapter,
4738
+ TogetherAdapter,
4739
+ OllamaAdapter,
4740
+ createLLMAdapter,
4741
+ loadStrategy,
4742
+ validateStrategy,
4743
+ STRATEGY_TEMPLATES,
4744
+ getStrategyTemplate,
4745
+ getAllStrategyTemplates,
4746
+ LLMProviderSchema,
4747
+ LLMConfigSchema,
4748
+ RiskUniverseSchema,
4749
+ TradingConfigSchema,
4750
+ VaultPolicySchema,
4751
+ VaultConfigSchema,
4752
+ RelayConfigSchema,
4753
+ PerpConfigSchema,
4754
+ AgentConfigSchema,
4755
+ loadConfig,
4756
+ validateConfig,
4757
+ createSampleConfig,
4758
+ TradeExecutor,
4759
+ MarketDataService,
4760
+ RiskManager,
4761
+ VaultManager,
4762
+ RelayClient,
4763
+ HyperliquidClient,
4764
+ HYPERLIQUID_DOMAIN,
4765
+ getNextNonce,
4766
+ HyperliquidSigner,
4767
+ fillHashToBytes32,
4768
+ fillOidToBytes32,
4769
+ OrderManager,
4770
+ PositionManager,
4771
+ HyperliquidWebSocket,
4772
+ PerpTradeRecorder,
4773
+ PerpOnboarding,
4774
+ AgentRuntime,
4775
+ encryptEnvFile,
4776
+ decryptEnvFile,
4777
+ loadSecureEnv
4778
+ };