@exagent/agent 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-YWPRVCRB.mjs +3009 -0
- package/dist/cli.js +155 -151
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +184 -180
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,3009 @@
|
|
|
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 AgentConfigSchema = z.object({
|
|
866
|
+
// Identity (from on-chain registration)
|
|
867
|
+
agentId: z.union([z.number().positive(), z.string()]),
|
|
868
|
+
name: z.string().min(3).max(32),
|
|
869
|
+
// Network
|
|
870
|
+
network: z.literal("mainnet").default("mainnet"),
|
|
871
|
+
// Wallet setup preference
|
|
872
|
+
wallet: WalletConfigSchema,
|
|
873
|
+
// LLM
|
|
874
|
+
llm: LLMConfigSchema,
|
|
875
|
+
// Trading parameters
|
|
876
|
+
riskUniverse: RiskUniverseSchema.default("established"),
|
|
877
|
+
trading: TradingConfigSchema.default({}),
|
|
878
|
+
// Vault configuration (copy trading)
|
|
879
|
+
vault: VaultConfigSchema.default({}),
|
|
880
|
+
// Relay configuration (command center)
|
|
881
|
+
relay: RelayConfigSchema,
|
|
882
|
+
// Allowed tokens (addresses)
|
|
883
|
+
allowedTokens: z.array(z.string()).optional()
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
// src/config.ts
|
|
887
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
888
|
+
import { join as join2 } from "path";
|
|
889
|
+
import { config as loadEnv } from "dotenv";
|
|
890
|
+
function loadConfig(configPath) {
|
|
891
|
+
loadEnv();
|
|
892
|
+
const configFile = configPath || process.env.EXAGENT_CONFIG || "agent-config.json";
|
|
893
|
+
const fullPath = configFile.startsWith("/") ? configFile : join2(process.cwd(), configFile);
|
|
894
|
+
if (!existsSync2(fullPath)) {
|
|
895
|
+
throw new Error(`Config file not found: ${fullPath}`);
|
|
896
|
+
}
|
|
897
|
+
const rawConfig = JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
898
|
+
const config = AgentConfigSchema.parse(rawConfig);
|
|
899
|
+
const privateKey = process.env.EXAGENT_PRIVATE_KEY;
|
|
900
|
+
if (privateKey && (!privateKey.startsWith("0x") || privateKey.length !== 66)) {
|
|
901
|
+
throw new Error("EXAGENT_PRIVATE_KEY must be a valid 32-byte hex string starting with 0x");
|
|
902
|
+
}
|
|
903
|
+
const llmConfig = { ...config.llm };
|
|
904
|
+
if (process.env.OPENAI_API_KEY && config.llm.provider === "openai") {
|
|
905
|
+
llmConfig.apiKey = process.env.OPENAI_API_KEY;
|
|
906
|
+
}
|
|
907
|
+
if (process.env.ANTHROPIC_API_KEY && config.llm.provider === "anthropic") {
|
|
908
|
+
llmConfig.apiKey = process.env.ANTHROPIC_API_KEY;
|
|
909
|
+
}
|
|
910
|
+
if (process.env.GOOGLE_AI_API_KEY && config.llm.provider === "google") {
|
|
911
|
+
llmConfig.apiKey = process.env.GOOGLE_AI_API_KEY;
|
|
912
|
+
}
|
|
913
|
+
if (process.env.DEEPSEEK_API_KEY && config.llm.provider === "deepseek") {
|
|
914
|
+
llmConfig.apiKey = process.env.DEEPSEEK_API_KEY;
|
|
915
|
+
}
|
|
916
|
+
if (process.env.MISTRAL_API_KEY && config.llm.provider === "mistral") {
|
|
917
|
+
llmConfig.apiKey = process.env.MISTRAL_API_KEY;
|
|
918
|
+
}
|
|
919
|
+
if (process.env.GROQ_API_KEY && config.llm.provider === "groq") {
|
|
920
|
+
llmConfig.apiKey = process.env.GROQ_API_KEY;
|
|
921
|
+
}
|
|
922
|
+
if (process.env.TOGETHER_API_KEY && config.llm.provider === "together") {
|
|
923
|
+
llmConfig.apiKey = process.env.TOGETHER_API_KEY;
|
|
924
|
+
}
|
|
925
|
+
if (process.env.EXAGENT_LLM_URL) {
|
|
926
|
+
llmConfig.endpoint = process.env.EXAGENT_LLM_URL;
|
|
927
|
+
}
|
|
928
|
+
if (process.env.EXAGENT_LLM_MODEL) {
|
|
929
|
+
llmConfig.model = process.env.EXAGENT_LLM_MODEL;
|
|
930
|
+
}
|
|
931
|
+
const network = process.env.EXAGENT_NETWORK || config.network;
|
|
932
|
+
return {
|
|
933
|
+
...config,
|
|
934
|
+
llm: llmConfig,
|
|
935
|
+
network,
|
|
936
|
+
privateKey: privateKey || ""
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function validateConfig(config) {
|
|
940
|
+
if (!config.privateKey) {
|
|
941
|
+
throw new Error("Private key is required");
|
|
942
|
+
}
|
|
943
|
+
if (config.llm.provider !== "ollama" && !config.llm.apiKey) {
|
|
944
|
+
throw new Error(`API key required for ${config.llm.provider} provider`);
|
|
945
|
+
}
|
|
946
|
+
if (config.llm.provider === "ollama" && !config.llm.endpoint) {
|
|
947
|
+
config.llm.endpoint = "http://localhost:11434";
|
|
948
|
+
}
|
|
949
|
+
if (config.llm.provider === "custom" && !config.llm.endpoint) {
|
|
950
|
+
throw new Error("Endpoint required for custom LLM provider");
|
|
951
|
+
}
|
|
952
|
+
if (!config.agentId || Number(config.agentId) <= 0) {
|
|
953
|
+
throw new Error("Valid agent ID required");
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
var DEFAULT_RPC_URL = "https://base-rpc.publicnode.com";
|
|
957
|
+
function getRpcUrl() {
|
|
958
|
+
return process.env.BASE_RPC_URL || process.env.EXAGENT_RPC_URL || DEFAULT_RPC_URL;
|
|
959
|
+
}
|
|
960
|
+
function createSampleConfig(agentId, name) {
|
|
961
|
+
return {
|
|
962
|
+
agentId,
|
|
963
|
+
name,
|
|
964
|
+
network: "mainnet",
|
|
965
|
+
llm: {
|
|
966
|
+
provider: "openai",
|
|
967
|
+
model: "gpt-4.1",
|
|
968
|
+
temperature: 0.7,
|
|
969
|
+
maxTokens: 4096
|
|
970
|
+
},
|
|
971
|
+
riskUniverse: "established",
|
|
972
|
+
trading: {
|
|
973
|
+
timeHorizon: "swing",
|
|
974
|
+
maxPositionSizeBps: 1e3,
|
|
975
|
+
maxDailyLossBps: 500,
|
|
976
|
+
maxConcurrentPositions: 5,
|
|
977
|
+
tradingIntervalMs: 6e4,
|
|
978
|
+
maxSlippageBps: 100,
|
|
979
|
+
minTradeValueUSD: 1
|
|
980
|
+
},
|
|
981
|
+
vault: {
|
|
982
|
+
// Default to manual - user must explicitly enable auto-creation
|
|
983
|
+
policy: "manual",
|
|
984
|
+
// Will use agent name for vault name if not set
|
|
985
|
+
preferVaultTrading: true
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/trading/executor.ts
|
|
991
|
+
var TradeExecutor = class {
|
|
992
|
+
client;
|
|
993
|
+
config;
|
|
994
|
+
allowedTokens;
|
|
995
|
+
constructor(client, config) {
|
|
996
|
+
this.client = client;
|
|
997
|
+
this.config = config;
|
|
998
|
+
this.allowedTokens = new Set(
|
|
999
|
+
(config.allowedTokens || []).map((t) => t.toLowerCase())
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Execute a single trade signal
|
|
1004
|
+
*/
|
|
1005
|
+
async execute(signal) {
|
|
1006
|
+
if (signal.action === "hold") {
|
|
1007
|
+
return { success: true };
|
|
1008
|
+
}
|
|
1009
|
+
try {
|
|
1010
|
+
console.log(`Executing ${signal.action}: ${signal.tokenIn} -> ${signal.tokenOut}`);
|
|
1011
|
+
console.log(`Amount: ${signal.amountIn.toString()}, Confidence: ${signal.confidence}`);
|
|
1012
|
+
if (!this.validateSignal(signal)) {
|
|
1013
|
+
return { success: false, error: "Signal exceeds position limits" };
|
|
1014
|
+
}
|
|
1015
|
+
const result = await this.client.trade({
|
|
1016
|
+
tokenIn: signal.tokenIn,
|
|
1017
|
+
tokenOut: signal.tokenOut,
|
|
1018
|
+
amountIn: signal.amountIn,
|
|
1019
|
+
maxSlippageBps: this.config.trading?.maxSlippageBps ?? 100
|
|
1020
|
+
});
|
|
1021
|
+
console.log(`Trade executed: ${result.hash}`);
|
|
1022
|
+
return { success: true, txHash: result.hash };
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1025
|
+
console.error(`Trade failed: ${message}`);
|
|
1026
|
+
return { success: false, error: message };
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Execute multiple trade signals
|
|
1031
|
+
* Returns results for each signal
|
|
1032
|
+
*/
|
|
1033
|
+
async executeAll(signals) {
|
|
1034
|
+
const results = [];
|
|
1035
|
+
for (const signal of signals) {
|
|
1036
|
+
const result = await this.execute(signal);
|
|
1037
|
+
results.push({ signal, ...result });
|
|
1038
|
+
if (signals.indexOf(signal) < signals.length - 1) {
|
|
1039
|
+
await this.delay(1e3);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return results;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Validate a signal against config limits and token restrictions
|
|
1046
|
+
*/
|
|
1047
|
+
validateSignal(signal) {
|
|
1048
|
+
if (signal.confidence < 0.5) {
|
|
1049
|
+
console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
if (this.allowedTokens.size > 0) {
|
|
1053
|
+
const tokenInAllowed = this.allowedTokens.has(signal.tokenIn.toLowerCase());
|
|
1054
|
+
const tokenOutAllowed = this.allowedTokens.has(signal.tokenOut.toLowerCase());
|
|
1055
|
+
if (!tokenInAllowed) {
|
|
1056
|
+
console.warn(`Token ${signal.tokenIn} not in allowed list for this agent's risk universe \u2014 skipping`);
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
if (!tokenOutAllowed) {
|
|
1060
|
+
console.warn(`Token ${signal.tokenOut} not in allowed list for this agent's risk universe \u2014 skipping`);
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
delay(ms) {
|
|
1067
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
// src/trading/market.ts
|
|
1072
|
+
import { createPublicClient, http, erc20Abi } from "viem";
|
|
1073
|
+
var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
1074
|
+
var TOKEN_DECIMALS = {
|
|
1075
|
+
// Base Mainnet — Core tokens
|
|
1076
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
|
|
1077
|
+
// USDC
|
|
1078
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
|
|
1079
|
+
// USDbC
|
|
1080
|
+
"0x4200000000000000000000000000000000000006": 18,
|
|
1081
|
+
// WETH
|
|
1082
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
|
|
1083
|
+
// DAI
|
|
1084
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
|
|
1085
|
+
// cbETH
|
|
1086
|
+
[NATIVE_ETH.toLowerCase()]: 18,
|
|
1087
|
+
// Native ETH
|
|
1088
|
+
// Base Mainnet — Established tokens
|
|
1089
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
|
|
1090
|
+
// AERO (Aerodrome)
|
|
1091
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
|
|
1092
|
+
// BRETT
|
|
1093
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
|
|
1094
|
+
// DEGEN
|
|
1095
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
|
|
1096
|
+
// VIRTUAL
|
|
1097
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
|
|
1098
|
+
// TOSHI
|
|
1099
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
|
|
1100
|
+
// cbBTC
|
|
1101
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": 18,
|
|
1102
|
+
// ezETH (Renzo)
|
|
1103
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
|
|
1104
|
+
// wstETH (Lido)
|
|
1105
|
+
};
|
|
1106
|
+
function getTokenDecimals(address) {
|
|
1107
|
+
const decimals = TOKEN_DECIMALS[address.toLowerCase()];
|
|
1108
|
+
if (decimals === void 0) {
|
|
1109
|
+
console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
|
|
1110
|
+
return 18;
|
|
1111
|
+
}
|
|
1112
|
+
return decimals;
|
|
1113
|
+
}
|
|
1114
|
+
var TOKEN_TO_COINGECKO = {
|
|
1115
|
+
// Core
|
|
1116
|
+
"0x4200000000000000000000000000000000000006": "ethereum",
|
|
1117
|
+
// WETH
|
|
1118
|
+
[NATIVE_ETH.toLowerCase()]: "ethereum",
|
|
1119
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
|
|
1120
|
+
// USDC
|
|
1121
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
|
|
1122
|
+
// USDbC
|
|
1123
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
|
|
1124
|
+
// cbETH
|
|
1125
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
|
|
1126
|
+
// DAI
|
|
1127
|
+
// Established
|
|
1128
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
|
|
1129
|
+
// AERO
|
|
1130
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
|
|
1131
|
+
// BRETT
|
|
1132
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
|
|
1133
|
+
// DEGEN
|
|
1134
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
|
|
1135
|
+
// VIRTUAL
|
|
1136
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
|
|
1137
|
+
// TOSHI
|
|
1138
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
|
|
1139
|
+
// cbBTC
|
|
1140
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
|
|
1141
|
+
// ezETH
|
|
1142
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
|
|
1143
|
+
// wstETH
|
|
1144
|
+
};
|
|
1145
|
+
var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
|
|
1146
|
+
var PRICE_STALENESS_MS = 6e4;
|
|
1147
|
+
var MarketDataService = class {
|
|
1148
|
+
rpcUrl;
|
|
1149
|
+
client;
|
|
1150
|
+
/** Cached prices from last fetch */
|
|
1151
|
+
cachedPrices = {};
|
|
1152
|
+
/** Timestamp of last successful price fetch */
|
|
1153
|
+
lastPriceFetchAt = 0;
|
|
1154
|
+
constructor(rpcUrl) {
|
|
1155
|
+
this.rpcUrl = rpcUrl;
|
|
1156
|
+
this.client = createPublicClient({
|
|
1157
|
+
transport: http(rpcUrl)
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
/** Cached volume data */
|
|
1161
|
+
cachedVolume24h = {};
|
|
1162
|
+
/** Cached price change data */
|
|
1163
|
+
cachedPriceChange24h = {};
|
|
1164
|
+
/**
|
|
1165
|
+
* Fetch current market data for the agent
|
|
1166
|
+
*/
|
|
1167
|
+
async fetchMarketData(walletAddress, tokenAddresses) {
|
|
1168
|
+
const prices = await this.fetchPrices(tokenAddresses);
|
|
1169
|
+
const balances = await this.fetchBalances(walletAddress, tokenAddresses);
|
|
1170
|
+
const portfolioValue = this.calculatePortfolioValue(balances, prices);
|
|
1171
|
+
let gasPrice;
|
|
1172
|
+
try {
|
|
1173
|
+
gasPrice = await this.client.getGasPrice();
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
return {
|
|
1177
|
+
timestamp: Date.now(),
|
|
1178
|
+
prices,
|
|
1179
|
+
balances,
|
|
1180
|
+
portfolioValue,
|
|
1181
|
+
volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
|
|
1182
|
+
priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
|
|
1183
|
+
gasPrice,
|
|
1184
|
+
network: {
|
|
1185
|
+
chainId: this.client.chain?.id ?? 8453
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Check if cached prices are still fresh
|
|
1191
|
+
*/
|
|
1192
|
+
get pricesAreFresh() {
|
|
1193
|
+
return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Fetch token prices from CoinGecko free API
|
|
1197
|
+
* Returns cached prices if still fresh (<60s old)
|
|
1198
|
+
*/
|
|
1199
|
+
async fetchPrices(tokenAddresses) {
|
|
1200
|
+
if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
|
|
1201
|
+
const prices2 = { ...this.cachedPrices };
|
|
1202
|
+
for (const addr of tokenAddresses) {
|
|
1203
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1204
|
+
if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
|
|
1205
|
+
prices2[addr.toLowerCase()] = 1;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return prices2;
|
|
1209
|
+
}
|
|
1210
|
+
const prices = {};
|
|
1211
|
+
const idsToFetch = /* @__PURE__ */ new Set();
|
|
1212
|
+
for (const addr of tokenAddresses) {
|
|
1213
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1214
|
+
if (cgId && !STABLECOIN_IDS.has(cgId)) {
|
|
1215
|
+
idsToFetch.add(cgId);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
idsToFetch.add("ethereum");
|
|
1219
|
+
if (idsToFetch.size > 0) {
|
|
1220
|
+
try {
|
|
1221
|
+
const ids = Array.from(idsToFetch).join(",");
|
|
1222
|
+
const response = await fetch(
|
|
1223
|
+
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
|
|
1224
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
1225
|
+
);
|
|
1226
|
+
if (response.ok) {
|
|
1227
|
+
const data = await response.json();
|
|
1228
|
+
for (const [cgId, priceData] of Object.entries(data)) {
|
|
1229
|
+
for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
|
|
1230
|
+
if (id === cgId) {
|
|
1231
|
+
const key = addr.toLowerCase();
|
|
1232
|
+
prices[key] = priceData.usd;
|
|
1233
|
+
if (priceData.usd_24h_vol !== void 0) {
|
|
1234
|
+
this.cachedVolume24h[key] = priceData.usd_24h_vol;
|
|
1235
|
+
}
|
|
1236
|
+
if (priceData.usd_24h_change !== void 0) {
|
|
1237
|
+
this.cachedPriceChange24h[key] = priceData.usd_24h_change;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
this.lastPriceFetchAt = Date.now();
|
|
1243
|
+
} else {
|
|
1244
|
+
console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
|
|
1245
|
+
}
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
for (const addr of tokenAddresses) {
|
|
1251
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1252
|
+
if (cgId && STABLECOIN_IDS.has(cgId)) {
|
|
1253
|
+
prices[addr.toLowerCase()] = 1;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
const missingAddrs = tokenAddresses.filter(
|
|
1257
|
+
(addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
|
|
1258
|
+
);
|
|
1259
|
+
if (missingAddrs.length > 0) {
|
|
1260
|
+
try {
|
|
1261
|
+
const coins = missingAddrs.map((a) => `base:${a}`).join(",");
|
|
1262
|
+
const llamaResponse = await fetch(
|
|
1263
|
+
`https://coins.llama.fi/prices/current/${coins}`,
|
|
1264
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
1265
|
+
);
|
|
1266
|
+
if (llamaResponse.ok) {
|
|
1267
|
+
const llamaData = await llamaResponse.json();
|
|
1268
|
+
for (const [key, data] of Object.entries(llamaData.coins)) {
|
|
1269
|
+
const addr = key.replace("base:", "").toLowerCase();
|
|
1270
|
+
if (data.price && data.confidence > 0.5) {
|
|
1271
|
+
prices[addr] = data.price;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
|
|
1275
|
+
}
|
|
1276
|
+
} catch {
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (Object.keys(prices).length > 0) {
|
|
1280
|
+
this.cachedPrices = prices;
|
|
1281
|
+
}
|
|
1282
|
+
if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
|
|
1283
|
+
console.warn("Using cached prices (last successful fetch was stale)");
|
|
1284
|
+
return { ...this.cachedPrices };
|
|
1285
|
+
}
|
|
1286
|
+
for (const addr of tokenAddresses) {
|
|
1287
|
+
if (!prices[addr.toLowerCase()]) {
|
|
1288
|
+
console.warn(`No price available for ${addr}, using 0`);
|
|
1289
|
+
prices[addr.toLowerCase()] = 0;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return prices;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Fetch real on-chain balances: native ETH + ERC-20 tokens
|
|
1296
|
+
*/
|
|
1297
|
+
async fetchBalances(walletAddress, tokenAddresses) {
|
|
1298
|
+
const balances = {};
|
|
1299
|
+
const wallet = walletAddress;
|
|
1300
|
+
try {
|
|
1301
|
+
const nativeBalance = await this.client.getBalance({ address: wallet });
|
|
1302
|
+
balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
|
|
1303
|
+
const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
|
|
1304
|
+
try {
|
|
1305
|
+
const balance = await this.client.readContract({
|
|
1306
|
+
address: tokenAddress,
|
|
1307
|
+
abi: erc20Abi,
|
|
1308
|
+
functionName: "balanceOf",
|
|
1309
|
+
args: [wallet]
|
|
1310
|
+
});
|
|
1311
|
+
return { address: tokenAddress.toLowerCase(), balance };
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
return { address: tokenAddress.toLowerCase(), balance: 0n };
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
const results = await Promise.all(erc20Promises);
|
|
1317
|
+
for (const { address, balance } of results) {
|
|
1318
|
+
balances[address] = balance;
|
|
1319
|
+
}
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
|
|
1322
|
+
balances[NATIVE_ETH.toLowerCase()] = 0n;
|
|
1323
|
+
for (const address of tokenAddresses) {
|
|
1324
|
+
balances[address.toLowerCase()] = 0n;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return balances;
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Calculate total portfolio value in USD
|
|
1331
|
+
*/
|
|
1332
|
+
calculatePortfolioValue(balances, prices) {
|
|
1333
|
+
let total = 0;
|
|
1334
|
+
for (const [address, balance] of Object.entries(balances)) {
|
|
1335
|
+
const price = prices[address.toLowerCase()] || 0;
|
|
1336
|
+
const decimals = getTokenDecimals(address);
|
|
1337
|
+
const amount = Number(balance) / Math.pow(10, decimals);
|
|
1338
|
+
total += amount * price;
|
|
1339
|
+
}
|
|
1340
|
+
return total;
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
// src/trading/risk.ts
|
|
1345
|
+
var RiskManager = class {
|
|
1346
|
+
config;
|
|
1347
|
+
dailyPnL = 0;
|
|
1348
|
+
dailyFees = 0;
|
|
1349
|
+
lastResetDate = "";
|
|
1350
|
+
/** Minimum trade value in USD — trades below this are rejected as dust */
|
|
1351
|
+
minTradeValueUSD;
|
|
1352
|
+
constructor(config) {
|
|
1353
|
+
this.config = config;
|
|
1354
|
+
this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Filter signals through risk checks
|
|
1358
|
+
* Returns only signals that pass all guardrails
|
|
1359
|
+
*/
|
|
1360
|
+
filterSignals(signals, marketData) {
|
|
1361
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1362
|
+
if (today !== this.lastResetDate) {
|
|
1363
|
+
this.dailyPnL = 0;
|
|
1364
|
+
this.dailyFees = 0;
|
|
1365
|
+
this.lastResetDate = today;
|
|
1366
|
+
}
|
|
1367
|
+
if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
|
|
1368
|
+
console.warn("Daily loss limit reached - no new trades");
|
|
1369
|
+
return [];
|
|
1370
|
+
}
|
|
1371
|
+
return signals.filter((signal) => this.validateSignal(signal, marketData));
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Validate individual signal against risk limits
|
|
1375
|
+
*/
|
|
1376
|
+
validateSignal(signal, marketData) {
|
|
1377
|
+
if (signal.action === "hold") {
|
|
1378
|
+
return true;
|
|
1379
|
+
}
|
|
1380
|
+
const signalValue = this.estimateSignalValue(signal, marketData);
|
|
1381
|
+
const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
|
|
1382
|
+
if (signalValue > maxPositionValue) {
|
|
1383
|
+
console.warn(
|
|
1384
|
+
`Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
|
|
1385
|
+
);
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
if (signal.confidence < 0.5) {
|
|
1389
|
+
console.warn(`Signal confidence too low: ${signal.confidence}`);
|
|
1390
|
+
return false;
|
|
1391
|
+
}
|
|
1392
|
+
if (signal.action === "buy" && this.config.maxConcurrentPositions) {
|
|
1393
|
+
const activePositions = this.countActivePositions(marketData);
|
|
1394
|
+
if (activePositions >= this.config.maxConcurrentPositions) {
|
|
1395
|
+
console.warn(
|
|
1396
|
+
`Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
|
|
1397
|
+
);
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (signalValue < this.minTradeValueUSD) {
|
|
1402
|
+
console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
|
|
1403
|
+
return false;
|
|
1404
|
+
}
|
|
1405
|
+
return true;
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
|
|
1409
|
+
*/
|
|
1410
|
+
countActivePositions(marketData) {
|
|
1411
|
+
const NATIVE_ETH_KEY = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
1412
|
+
let count = 0;
|
|
1413
|
+
for (const [address, balance] of Object.entries(marketData.balances)) {
|
|
1414
|
+
if (address.toLowerCase() === NATIVE_ETH_KEY) continue;
|
|
1415
|
+
if (balance > 0n) count++;
|
|
1416
|
+
}
|
|
1417
|
+
return count;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Check if daily loss limit has been hit
|
|
1421
|
+
*/
|
|
1422
|
+
isDailyLossLimitHit(portfolioValue) {
|
|
1423
|
+
const maxLoss = portfolioValue * this.config.maxDailyLossBps / 1e4;
|
|
1424
|
+
return this.dailyPnL < -maxLoss;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Estimate USD value of a trade signal
|
|
1428
|
+
*/
|
|
1429
|
+
estimateSignalValue(signal, marketData) {
|
|
1430
|
+
const price = marketData.prices[signal.tokenIn.toLowerCase()] || 0;
|
|
1431
|
+
const tokenDecimals = getTokenDecimals(signal.tokenIn);
|
|
1432
|
+
const amount = Number(signal.amountIn) / Math.pow(10, tokenDecimals);
|
|
1433
|
+
return amount * price;
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Update daily PnL after a trade (market gains/losses only)
|
|
1437
|
+
*/
|
|
1438
|
+
updatePnL(pnl) {
|
|
1439
|
+
this.dailyPnL += pnl;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Update daily fees (trading fees, gas costs, etc.)
|
|
1443
|
+
* Fees are tracked separately and do NOT count toward the daily loss limit.
|
|
1444
|
+
* This prevents protocol fees from triggering circuit breakers.
|
|
1445
|
+
*/
|
|
1446
|
+
updateFees(fees) {
|
|
1447
|
+
this.dailyFees += fees;
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Get current risk status
|
|
1451
|
+
* @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
|
|
1452
|
+
*/
|
|
1453
|
+
getStatus(portfolioValue) {
|
|
1454
|
+
const pv = portfolioValue || 0;
|
|
1455
|
+
const maxLossUSD = pv * this.config.maxDailyLossBps / 1e4;
|
|
1456
|
+
return {
|
|
1457
|
+
dailyPnL: this.dailyPnL,
|
|
1458
|
+
dailyFees: this.dailyFees,
|
|
1459
|
+
dailyNetPnL: this.dailyPnL - this.dailyFees,
|
|
1460
|
+
dailyLossLimit: maxLossUSD,
|
|
1461
|
+
// Only market PnL triggers the limit — fees are excluded
|
|
1462
|
+
isLimitHit: pv > 0 ? this.dailyPnL < -maxLossUSD : false
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
// src/vault/manager.ts
|
|
1468
|
+
import { createPublicClient as createPublicClient2, createWalletClient, http as http2 } from "viem";
|
|
1469
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
1470
|
+
import { base } from "viem/chains";
|
|
1471
|
+
var ADDRESSES = {
|
|
1472
|
+
mainnet: {
|
|
1473
|
+
vaultFactory: process.env.EXAGENT_VAULT_FACTORY_ADDRESS || "0x0000000000000000000000000000000000000000",
|
|
1474
|
+
registry: process.env.EXAGENT_REGISTRY_ADDRESS || "0x0000000000000000000000000000000000000000",
|
|
1475
|
+
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
var VAULT_FACTORY_ABI = [
|
|
1479
|
+
{
|
|
1480
|
+
type: "function",
|
|
1481
|
+
name: "vaults",
|
|
1482
|
+
inputs: [{ name: "agentId", type: "uint256" }, { name: "asset", type: "address" }],
|
|
1483
|
+
outputs: [{ type: "address" }],
|
|
1484
|
+
stateMutability: "view"
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
type: "function",
|
|
1488
|
+
name: "canCreateVault",
|
|
1489
|
+
inputs: [{ name: "creator", type: "address" }],
|
|
1490
|
+
outputs: [{ name: "canCreate", type: "bool" }, { name: "reason", type: "string" }],
|
|
1491
|
+
stateMutability: "view"
|
|
1492
|
+
},
|
|
1493
|
+
{
|
|
1494
|
+
type: "function",
|
|
1495
|
+
name: "createVault",
|
|
1496
|
+
inputs: [
|
|
1497
|
+
{ name: "agentId", type: "uint256" },
|
|
1498
|
+
{ name: "asset", type: "address" },
|
|
1499
|
+
{ name: "seedAmount", type: "uint256" },
|
|
1500
|
+
{ name: "name", type: "string" },
|
|
1501
|
+
{ name: "symbol", type: "string" },
|
|
1502
|
+
{ name: "feeRecipient", type: "address" }
|
|
1503
|
+
],
|
|
1504
|
+
outputs: [{ type: "address" }],
|
|
1505
|
+
stateMutability: "nonpayable"
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
type: "function",
|
|
1509
|
+
name: "minimumVeEXARequired",
|
|
1510
|
+
inputs: [],
|
|
1511
|
+
outputs: [{ type: "uint256" }],
|
|
1512
|
+
stateMutability: "view"
|
|
1513
|
+
}
|
|
1514
|
+
];
|
|
1515
|
+
var VAULT_ABI = [
|
|
1516
|
+
{
|
|
1517
|
+
type: "function",
|
|
1518
|
+
name: "totalAssets",
|
|
1519
|
+
inputs: [],
|
|
1520
|
+
outputs: [{ type: "uint256" }],
|
|
1521
|
+
stateMutability: "view"
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
type: "function",
|
|
1525
|
+
name: "executeTrade",
|
|
1526
|
+
inputs: [
|
|
1527
|
+
{ name: "tokenIn", type: "address" },
|
|
1528
|
+
{ name: "tokenOut", type: "address" },
|
|
1529
|
+
{ name: "amountIn", type: "uint256" },
|
|
1530
|
+
{ name: "minAmountOut", type: "uint256" },
|
|
1531
|
+
{ name: "aggregator", type: "address" },
|
|
1532
|
+
{ name: "swapData", type: "bytes" },
|
|
1533
|
+
{ name: "deadline", type: "uint256" }
|
|
1534
|
+
],
|
|
1535
|
+
outputs: [{ type: "uint256" }],
|
|
1536
|
+
stateMutability: "nonpayable"
|
|
1537
|
+
}
|
|
1538
|
+
];
|
|
1539
|
+
var VaultManager = class {
|
|
1540
|
+
config;
|
|
1541
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1542
|
+
publicClient;
|
|
1543
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1544
|
+
walletClient;
|
|
1545
|
+
addresses;
|
|
1546
|
+
account;
|
|
1547
|
+
chain;
|
|
1548
|
+
cachedVaultAddress = null;
|
|
1549
|
+
lastVaultCheck = 0;
|
|
1550
|
+
VAULT_CACHE_TTL = 6e4;
|
|
1551
|
+
// 1 minute
|
|
1552
|
+
enabled = true;
|
|
1553
|
+
constructor(config) {
|
|
1554
|
+
this.config = config;
|
|
1555
|
+
this.addresses = ADDRESSES[config.network];
|
|
1556
|
+
this.account = privateKeyToAccount(config.walletKey);
|
|
1557
|
+
this.chain = base;
|
|
1558
|
+
const rpcUrl = getRpcUrl();
|
|
1559
|
+
this.publicClient = createPublicClient2({
|
|
1560
|
+
chain: this.chain,
|
|
1561
|
+
transport: http2(rpcUrl)
|
|
1562
|
+
});
|
|
1563
|
+
this.walletClient = createWalletClient({
|
|
1564
|
+
account: this.account,
|
|
1565
|
+
chain: this.chain,
|
|
1566
|
+
transport: http2(rpcUrl)
|
|
1567
|
+
});
|
|
1568
|
+
if (this.addresses.vaultFactory === "0x0000000000000000000000000000000000000000") {
|
|
1569
|
+
console.warn("VaultFactory address is zero \u2014 vault operations will be disabled");
|
|
1570
|
+
this.enabled = false;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Get the agent's vault policy
|
|
1575
|
+
*/
|
|
1576
|
+
get policy() {
|
|
1577
|
+
return this.config.vaultConfig.policy;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Check if vault trading is preferred when a vault exists
|
|
1581
|
+
*/
|
|
1582
|
+
get preferVaultTrading() {
|
|
1583
|
+
return this.config.vaultConfig.preferVaultTrading;
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Get comprehensive vault status
|
|
1587
|
+
*/
|
|
1588
|
+
async getVaultStatus() {
|
|
1589
|
+
if (!this.enabled) {
|
|
1590
|
+
return {
|
|
1591
|
+
hasVault: false,
|
|
1592
|
+
vaultAddress: null,
|
|
1593
|
+
totalAssets: BigInt(0),
|
|
1594
|
+
canCreateVault: false,
|
|
1595
|
+
cannotCreateReason: "Vault operations disabled (contract address not set)",
|
|
1596
|
+
requirementsMet: false,
|
|
1597
|
+
requirements: { veXARequired: BigInt(0), isBypassed: false }
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
const vaultAddress = await this.getVaultAddress();
|
|
1601
|
+
const hasVault = vaultAddress !== null;
|
|
1602
|
+
let totalAssets = BigInt(0);
|
|
1603
|
+
if (hasVault && vaultAddress) {
|
|
1604
|
+
try {
|
|
1605
|
+
totalAssets = await this.publicClient.readContract({
|
|
1606
|
+
address: vaultAddress,
|
|
1607
|
+
abi: VAULT_ABI,
|
|
1608
|
+
functionName: "totalAssets"
|
|
1609
|
+
});
|
|
1610
|
+
} catch {
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
const [canCreateResult, requirements] = await Promise.all([
|
|
1614
|
+
this.publicClient.readContract({
|
|
1615
|
+
address: this.addresses.vaultFactory,
|
|
1616
|
+
abi: VAULT_FACTORY_ABI,
|
|
1617
|
+
functionName: "canCreateVault",
|
|
1618
|
+
args: [this.account.address]
|
|
1619
|
+
}),
|
|
1620
|
+
this.getRequirements()
|
|
1621
|
+
]);
|
|
1622
|
+
return {
|
|
1623
|
+
hasVault,
|
|
1624
|
+
vaultAddress,
|
|
1625
|
+
totalAssets,
|
|
1626
|
+
canCreateVault: canCreateResult[0],
|
|
1627
|
+
cannotCreateReason: canCreateResult[0] ? null : canCreateResult[1],
|
|
1628
|
+
requirementsMet: canCreateResult[0] || requirements.isBypassed,
|
|
1629
|
+
requirements
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Get vault creation requirements
|
|
1634
|
+
* Note: No burnFee on mainnet — vault creation requires USDC seed instead
|
|
1635
|
+
*/
|
|
1636
|
+
async getRequirements() {
|
|
1637
|
+
const veXARequired = await this.publicClient.readContract({
|
|
1638
|
+
address: this.addresses.vaultFactory,
|
|
1639
|
+
abi: VAULT_FACTORY_ABI,
|
|
1640
|
+
functionName: "minimumVeEXARequired"
|
|
1641
|
+
});
|
|
1642
|
+
const isBypassed = veXARequired === BigInt(0);
|
|
1643
|
+
return { veXARequired, isBypassed };
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Get the agent's vault address (cached)
|
|
1647
|
+
*/
|
|
1648
|
+
async getVaultAddress() {
|
|
1649
|
+
const now = Date.now();
|
|
1650
|
+
if (this.cachedVaultAddress && now - this.lastVaultCheck < this.VAULT_CACHE_TTL) {
|
|
1651
|
+
return this.cachedVaultAddress;
|
|
1652
|
+
}
|
|
1653
|
+
const vaultAddress = await this.publicClient.readContract({
|
|
1654
|
+
address: this.addresses.vaultFactory,
|
|
1655
|
+
abi: VAULT_FACTORY_ABI,
|
|
1656
|
+
functionName: "vaults",
|
|
1657
|
+
args: [this.config.agentId, this.addresses.usdc]
|
|
1658
|
+
});
|
|
1659
|
+
this.lastVaultCheck = now;
|
|
1660
|
+
if (vaultAddress === "0x0000000000000000000000000000000000000000") {
|
|
1661
|
+
this.cachedVaultAddress = null;
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
this.cachedVaultAddress = vaultAddress;
|
|
1665
|
+
return vaultAddress;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Create a vault for the agent
|
|
1669
|
+
* @param seedAmount - USDC seed amount in raw units (default: 100e6 = 100 USDC)
|
|
1670
|
+
* @returns Vault address if successful
|
|
1671
|
+
*/
|
|
1672
|
+
async createVault(seedAmount) {
|
|
1673
|
+
if (!this.enabled) {
|
|
1674
|
+
return { success: false, error: "Vault operations disabled (contract address not set)" };
|
|
1675
|
+
}
|
|
1676
|
+
if (this.policy === "disabled") {
|
|
1677
|
+
return { success: false, error: "Vault creation disabled by policy" };
|
|
1678
|
+
}
|
|
1679
|
+
const existingVault = await this.getVaultAddress();
|
|
1680
|
+
if (existingVault) {
|
|
1681
|
+
return { success: false, error: "Vault already exists", vaultAddress: existingVault };
|
|
1682
|
+
}
|
|
1683
|
+
const status = await this.getVaultStatus();
|
|
1684
|
+
if (!status.canCreateVault) {
|
|
1685
|
+
return { success: false, error: status.cannotCreateReason || "Requirements not met" };
|
|
1686
|
+
}
|
|
1687
|
+
const seed = seedAmount || BigInt(1e8);
|
|
1688
|
+
const vaultName = this.config.vaultConfig.defaultName || `${this.config.agentName} Trading Vault`;
|
|
1689
|
+
const vaultSymbol = this.config.vaultConfig.defaultSymbol || `ex${this.config.agentName.replace(/[^a-zA-Z]/g, "").slice(0, 4).toUpperCase()}`;
|
|
1690
|
+
const feeRecipient = this.config.vaultConfig.feeRecipient || this.account.address;
|
|
1691
|
+
try {
|
|
1692
|
+
const hash = await this.walletClient.writeContract({
|
|
1693
|
+
address: this.addresses.vaultFactory,
|
|
1694
|
+
abi: VAULT_FACTORY_ABI,
|
|
1695
|
+
functionName: "createVault",
|
|
1696
|
+
args: [
|
|
1697
|
+
this.config.agentId,
|
|
1698
|
+
this.addresses.usdc,
|
|
1699
|
+
seed,
|
|
1700
|
+
vaultName,
|
|
1701
|
+
vaultSymbol,
|
|
1702
|
+
feeRecipient
|
|
1703
|
+
],
|
|
1704
|
+
chain: this.chain,
|
|
1705
|
+
account: this.account
|
|
1706
|
+
});
|
|
1707
|
+
const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
|
|
1708
|
+
if (receipt.status !== "success") {
|
|
1709
|
+
return { success: false, error: "Transaction failed", txHash: hash };
|
|
1710
|
+
}
|
|
1711
|
+
const vaultAddress = await this.getVaultAddress();
|
|
1712
|
+
this.cachedVaultAddress = vaultAddress;
|
|
1713
|
+
return {
|
|
1714
|
+
success: true,
|
|
1715
|
+
vaultAddress,
|
|
1716
|
+
txHash: hash
|
|
1717
|
+
};
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
return {
|
|
1720
|
+
success: false,
|
|
1721
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Execute a trade through the vault (if it exists and policy allows)
|
|
1727
|
+
* Returns null if should use direct trading instead
|
|
1728
|
+
*/
|
|
1729
|
+
async executeVaultTrade(params) {
|
|
1730
|
+
if (!this.preferVaultTrading) {
|
|
1731
|
+
return null;
|
|
1732
|
+
}
|
|
1733
|
+
const vaultAddress = await this.getVaultAddress();
|
|
1734
|
+
if (!vaultAddress) {
|
|
1735
|
+
return null;
|
|
1736
|
+
}
|
|
1737
|
+
const deadline = params.deadline || BigInt(Math.floor(Date.now() / 1e3) + 3600);
|
|
1738
|
+
try {
|
|
1739
|
+
const hash = await this.walletClient.writeContract({
|
|
1740
|
+
address: vaultAddress,
|
|
1741
|
+
abi: VAULT_ABI,
|
|
1742
|
+
functionName: "executeTrade",
|
|
1743
|
+
args: [
|
|
1744
|
+
params.tokenIn,
|
|
1745
|
+
params.tokenOut,
|
|
1746
|
+
params.amountIn,
|
|
1747
|
+
params.minAmountOut,
|
|
1748
|
+
params.aggregator,
|
|
1749
|
+
params.swapData,
|
|
1750
|
+
deadline
|
|
1751
|
+
],
|
|
1752
|
+
chain: this.chain,
|
|
1753
|
+
account: this.account
|
|
1754
|
+
});
|
|
1755
|
+
return { usedVault: true, txHash: hash };
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
return {
|
|
1758
|
+
usedVault: true,
|
|
1759
|
+
error: error instanceof Error ? error.message : "Vault trade failed"
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
// src/relay.ts
|
|
1766
|
+
import WebSocket from "ws";
|
|
1767
|
+
import { privateKeyToAccount as privateKeyToAccount2, signMessage } from "viem/accounts";
|
|
1768
|
+
import { SDK_VERSION } from "@exagent/sdk";
|
|
1769
|
+
var RelayClient = class {
|
|
1770
|
+
config;
|
|
1771
|
+
ws = null;
|
|
1772
|
+
authenticated = false;
|
|
1773
|
+
authRejected = false;
|
|
1774
|
+
reconnectAttempts = 0;
|
|
1775
|
+
maxReconnectAttempts = 50;
|
|
1776
|
+
reconnectTimer = null;
|
|
1777
|
+
heartbeatTimer = null;
|
|
1778
|
+
stopped = false;
|
|
1779
|
+
constructor(config) {
|
|
1780
|
+
this.config = config;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Connect to the relay server
|
|
1784
|
+
*/
|
|
1785
|
+
async connect() {
|
|
1786
|
+
if (this.stopped) return;
|
|
1787
|
+
const wsUrl = this.config.relay.apiUrl.replace(/^https?:\/\//, (m) => m.includes("https") ? "wss://" : "ws://").replace(/\/$/, "") + "/ws/agent";
|
|
1788
|
+
return new Promise((resolve, reject) => {
|
|
1789
|
+
try {
|
|
1790
|
+
this.ws = new WebSocket(wsUrl);
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
console.error("Relay: Failed to create WebSocket:", error);
|
|
1793
|
+
this.scheduleReconnect();
|
|
1794
|
+
reject(error);
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
const connectTimeout = setTimeout(() => {
|
|
1798
|
+
if (!this.authenticated) {
|
|
1799
|
+
console.error("Relay: Connection timeout");
|
|
1800
|
+
this.ws?.close();
|
|
1801
|
+
this.scheduleReconnect();
|
|
1802
|
+
reject(new Error("Connection timeout"));
|
|
1803
|
+
}
|
|
1804
|
+
}, 15e3);
|
|
1805
|
+
this.ws.on("open", async () => {
|
|
1806
|
+
this.authRejected = false;
|
|
1807
|
+
console.log("Relay: Connected, authenticating...");
|
|
1808
|
+
try {
|
|
1809
|
+
await this.authenticate();
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
console.error("Relay: Authentication failed:", error);
|
|
1812
|
+
this.ws?.close();
|
|
1813
|
+
clearTimeout(connectTimeout);
|
|
1814
|
+
reject(error);
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
this.ws.on("message", (raw) => {
|
|
1818
|
+
try {
|
|
1819
|
+
const data = JSON.parse(raw.toString());
|
|
1820
|
+
this.handleMessage(data);
|
|
1821
|
+
if (data.type === "auth_success") {
|
|
1822
|
+
clearTimeout(connectTimeout);
|
|
1823
|
+
this.authenticated = true;
|
|
1824
|
+
this.reconnectAttempts = 0;
|
|
1825
|
+
this.startHeartbeat();
|
|
1826
|
+
console.log("Relay: Authenticated successfully");
|
|
1827
|
+
resolve();
|
|
1828
|
+
} else if (data.type === "auth_error") {
|
|
1829
|
+
clearTimeout(connectTimeout);
|
|
1830
|
+
this.authRejected = true;
|
|
1831
|
+
console.error(`Relay: Auth rejected: ${data.message}`);
|
|
1832
|
+
reject(new Error(data.message));
|
|
1833
|
+
}
|
|
1834
|
+
} catch {
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
this.ws.on("close", (code, reason) => {
|
|
1838
|
+
clearTimeout(connectTimeout);
|
|
1839
|
+
this.authenticated = false;
|
|
1840
|
+
this.stopHeartbeat();
|
|
1841
|
+
if (!this.stopped) {
|
|
1842
|
+
if (!this.authRejected) {
|
|
1843
|
+
console.log(`Relay: Disconnected (${code}: ${reason.toString() || "unknown"})`);
|
|
1844
|
+
}
|
|
1845
|
+
this.scheduleReconnect();
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
this.ws.on("error", (error) => {
|
|
1849
|
+
if (!this.stopped) {
|
|
1850
|
+
console.error("Relay: WebSocket error:", error.message);
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Authenticate with the relay server using wallet signature
|
|
1857
|
+
*/
|
|
1858
|
+
async authenticate() {
|
|
1859
|
+
const account = privateKeyToAccount2(this.config.privateKey);
|
|
1860
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
1861
|
+
const message = `ExagentRelay:${this.config.agentId}:${timestamp}`;
|
|
1862
|
+
const signature = await signMessage({
|
|
1863
|
+
message,
|
|
1864
|
+
privateKey: this.config.privateKey
|
|
1865
|
+
});
|
|
1866
|
+
this.send({
|
|
1867
|
+
type: "auth",
|
|
1868
|
+
agentId: this.config.agentId,
|
|
1869
|
+
wallet: account.address,
|
|
1870
|
+
timestamp,
|
|
1871
|
+
signature,
|
|
1872
|
+
sdkVersion: SDK_VERSION
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Handle incoming messages from the relay server
|
|
1877
|
+
*/
|
|
1878
|
+
handleMessage(data) {
|
|
1879
|
+
switch (data.type) {
|
|
1880
|
+
case "command":
|
|
1881
|
+
if (data.command && this.config.onCommand) {
|
|
1882
|
+
this.config.onCommand(data.command);
|
|
1883
|
+
}
|
|
1884
|
+
break;
|
|
1885
|
+
case "auth_success":
|
|
1886
|
+
case "auth_error":
|
|
1887
|
+
break;
|
|
1888
|
+
case "error":
|
|
1889
|
+
console.error(`Relay: Server error: ${data.message}`);
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Send a status heartbeat
|
|
1895
|
+
*/
|
|
1896
|
+
sendHeartbeat(status) {
|
|
1897
|
+
if (!this.authenticated) return;
|
|
1898
|
+
this.send({
|
|
1899
|
+
type: "heartbeat",
|
|
1900
|
+
agentId: this.config.agentId,
|
|
1901
|
+
status
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Send a status update (outside of regular heartbeat)
|
|
1906
|
+
*/
|
|
1907
|
+
sendStatusUpdate(status) {
|
|
1908
|
+
if (!this.authenticated) return;
|
|
1909
|
+
this.send({
|
|
1910
|
+
type: "status_update",
|
|
1911
|
+
agentId: this.config.agentId,
|
|
1912
|
+
status
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Send a message to the command center
|
|
1917
|
+
*/
|
|
1918
|
+
sendMessage(messageType, level, title, body, data) {
|
|
1919
|
+
if (!this.authenticated) return;
|
|
1920
|
+
this.send({
|
|
1921
|
+
type: "message",
|
|
1922
|
+
agentId: this.config.agentId,
|
|
1923
|
+
messageType,
|
|
1924
|
+
level,
|
|
1925
|
+
title,
|
|
1926
|
+
body,
|
|
1927
|
+
data
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Send a command execution result
|
|
1932
|
+
*/
|
|
1933
|
+
sendCommandResult(commandId, success, result) {
|
|
1934
|
+
if (!this.authenticated) return;
|
|
1935
|
+
this.send({
|
|
1936
|
+
type: "command_result",
|
|
1937
|
+
agentId: this.config.agentId,
|
|
1938
|
+
commandId,
|
|
1939
|
+
success,
|
|
1940
|
+
result
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Start the heartbeat timer
|
|
1945
|
+
*/
|
|
1946
|
+
startHeartbeat() {
|
|
1947
|
+
this.stopHeartbeat();
|
|
1948
|
+
const interval = this.config.relay.heartbeatIntervalMs || 3e4;
|
|
1949
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1950
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1951
|
+
this.ws.ping();
|
|
1952
|
+
}
|
|
1953
|
+
}, interval);
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Stop the heartbeat timer
|
|
1957
|
+
*/
|
|
1958
|
+
stopHeartbeat() {
|
|
1959
|
+
if (this.heartbeatTimer) {
|
|
1960
|
+
clearInterval(this.heartbeatTimer);
|
|
1961
|
+
this.heartbeatTimer = null;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Schedule a reconnection with exponential backoff
|
|
1966
|
+
*/
|
|
1967
|
+
scheduleReconnect() {
|
|
1968
|
+
if (this.stopped) return;
|
|
1969
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1970
|
+
console.error("Relay: Max reconnection attempts reached. Giving up.");
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
1974
|
+
this.reconnectAttempts++;
|
|
1975
|
+
console.log(
|
|
1976
|
+
`Relay: Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
|
1977
|
+
);
|
|
1978
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1979
|
+
this.connect().catch(() => {
|
|
1980
|
+
});
|
|
1981
|
+
}, delay);
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Send a JSON message to the WebSocket
|
|
1985
|
+
*/
|
|
1986
|
+
send(data) {
|
|
1987
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1988
|
+
this.ws.send(JSON.stringify(data));
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Check if connected and authenticated
|
|
1993
|
+
*/
|
|
1994
|
+
get isConnected() {
|
|
1995
|
+
return this.authenticated && this.ws?.readyState === WebSocket.OPEN;
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Disconnect and stop reconnecting
|
|
1999
|
+
*/
|
|
2000
|
+
disconnect() {
|
|
2001
|
+
this.stopped = true;
|
|
2002
|
+
this.stopHeartbeat();
|
|
2003
|
+
if (this.reconnectTimer) {
|
|
2004
|
+
clearTimeout(this.reconnectTimer);
|
|
2005
|
+
this.reconnectTimer = null;
|
|
2006
|
+
}
|
|
2007
|
+
if (this.ws) {
|
|
2008
|
+
this.ws.close(1e3, "Agent shutting down");
|
|
2009
|
+
this.ws = null;
|
|
2010
|
+
}
|
|
2011
|
+
this.authenticated = false;
|
|
2012
|
+
console.log("Relay: Disconnected");
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
// src/runtime.ts
|
|
2017
|
+
import { ExagentClient, ExagentRegistry } from "@exagent/sdk";
|
|
2018
|
+
import { createPublicClient as createPublicClient3, http as http3 } from "viem";
|
|
2019
|
+
import { base as base2 } from "viem/chains";
|
|
2020
|
+
|
|
2021
|
+
// src/browser-open.ts
|
|
2022
|
+
import { exec } from "child_process";
|
|
2023
|
+
function openBrowser(url) {
|
|
2024
|
+
const platform = process.platform;
|
|
2025
|
+
try {
|
|
2026
|
+
if (platform === "darwin") {
|
|
2027
|
+
exec(`open "${url}"`);
|
|
2028
|
+
} else if (platform === "win32") {
|
|
2029
|
+
exec(`start "" "${url}"`);
|
|
2030
|
+
} else {
|
|
2031
|
+
exec(`xdg-open "${url}"`);
|
|
2032
|
+
}
|
|
2033
|
+
} catch {
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// src/runtime.ts
|
|
2038
|
+
var FUNDS_LOW_THRESHOLD = 5e-3;
|
|
2039
|
+
var FUNDS_CRITICAL_THRESHOLD = 1e-3;
|
|
2040
|
+
var AgentRuntime = class {
|
|
2041
|
+
config;
|
|
2042
|
+
client;
|
|
2043
|
+
llm;
|
|
2044
|
+
strategy;
|
|
2045
|
+
executor;
|
|
2046
|
+
riskManager;
|
|
2047
|
+
marketData;
|
|
2048
|
+
vaultManager;
|
|
2049
|
+
relay = null;
|
|
2050
|
+
isRunning = false;
|
|
2051
|
+
mode = "idle";
|
|
2052
|
+
configHash;
|
|
2053
|
+
cycleCount = 0;
|
|
2054
|
+
lastCycleAt = 0;
|
|
2055
|
+
lastPortfolioValue = 0;
|
|
2056
|
+
lastEthBalance = "0";
|
|
2057
|
+
processAlive = true;
|
|
2058
|
+
riskUniverse = 0;
|
|
2059
|
+
allowedTokens = /* @__PURE__ */ new Set();
|
|
2060
|
+
constructor(config) {
|
|
2061
|
+
this.config = config;
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Initialize the agent runtime
|
|
2065
|
+
*/
|
|
2066
|
+
async initialize() {
|
|
2067
|
+
console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
|
|
2068
|
+
this.client = new ExagentClient({
|
|
2069
|
+
privateKey: this.config.privateKey,
|
|
2070
|
+
network: this.config.network
|
|
2071
|
+
});
|
|
2072
|
+
console.log(`Wallet: ${this.client.address}`);
|
|
2073
|
+
const agent = await this.client.registry.getAgent(BigInt(this.config.agentId));
|
|
2074
|
+
if (!agent) {
|
|
2075
|
+
throw new Error(`Agent ID ${this.config.agentId} not found on-chain. Please register first.`);
|
|
2076
|
+
}
|
|
2077
|
+
console.log(`Agent verified: ${agent.name}`);
|
|
2078
|
+
await this.ensureWalletLinked();
|
|
2079
|
+
await this.loadTradingRestrictions();
|
|
2080
|
+
console.log(`Initializing LLM: ${this.config.llm.provider}`);
|
|
2081
|
+
this.llm = await createLLMAdapter(this.config.llm);
|
|
2082
|
+
const llmMeta = this.llm.getMetadata();
|
|
2083
|
+
console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
|
|
2084
|
+
await this.syncConfigHash();
|
|
2085
|
+
this.strategy = await loadStrategy();
|
|
2086
|
+
this.executor = new TradeExecutor(this.client, this.config);
|
|
2087
|
+
this.riskManager = new RiskManager(this.config.trading);
|
|
2088
|
+
this.marketData = new MarketDataService(this.getRpcUrl());
|
|
2089
|
+
await this.initializeVaultManager();
|
|
2090
|
+
await this.initializeRelay();
|
|
2091
|
+
console.log("Agent initialized successfully");
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Initialize the relay client for command center connectivity
|
|
2095
|
+
*/
|
|
2096
|
+
async initializeRelay() {
|
|
2097
|
+
const relayConfig = this.config.relay;
|
|
2098
|
+
const relayEnabled = process.env.EXAGENT_RELAY_ENABLED !== "false";
|
|
2099
|
+
if (!relayConfig?.enabled || !relayEnabled) {
|
|
2100
|
+
console.log("Relay: Disabled");
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
const apiUrl = process.env.EXAGENT_API_URL || relayConfig.apiUrl;
|
|
2104
|
+
if (!apiUrl) {
|
|
2105
|
+
console.log("Relay: No API URL configured, skipping");
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
this.relay = new RelayClient({
|
|
2109
|
+
agentId: String(this.config.agentId),
|
|
2110
|
+
privateKey: this.config.privateKey,
|
|
2111
|
+
relay: {
|
|
2112
|
+
...relayConfig,
|
|
2113
|
+
apiUrl
|
|
2114
|
+
},
|
|
2115
|
+
onCommand: (cmd) => this.handleCommand(cmd)
|
|
2116
|
+
});
|
|
2117
|
+
try {
|
|
2118
|
+
await this.relay.connect();
|
|
2119
|
+
console.log("Relay: Connected to command center");
|
|
2120
|
+
this.sendRelayStatus();
|
|
2121
|
+
} catch (error) {
|
|
2122
|
+
console.warn(
|
|
2123
|
+
"Relay: Failed to connect (agent will work locally):",
|
|
2124
|
+
error instanceof Error ? error.message : error
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Initialize the vault manager based on config
|
|
2130
|
+
*/
|
|
2131
|
+
async initializeVaultManager() {
|
|
2132
|
+
const vaultConfig = this.config.vault || { policy: "disabled", preferVaultTrading: false };
|
|
2133
|
+
this.vaultManager = new VaultManager({
|
|
2134
|
+
agentId: BigInt(this.config.agentId),
|
|
2135
|
+
agentName: this.config.name,
|
|
2136
|
+
network: this.config.network,
|
|
2137
|
+
walletKey: this.config.privateKey,
|
|
2138
|
+
vaultConfig
|
|
2139
|
+
});
|
|
2140
|
+
console.log(`Vault policy: ${vaultConfig.policy}`);
|
|
2141
|
+
const status = await this.vaultManager.getVaultStatus();
|
|
2142
|
+
if (status.hasVault) {
|
|
2143
|
+
console.log(`Vault exists: ${status.vaultAddress}`);
|
|
2144
|
+
console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
|
|
2145
|
+
} else {
|
|
2146
|
+
console.log("No vault exists for this agent");
|
|
2147
|
+
if (vaultConfig.policy === "manual") {
|
|
2148
|
+
console.log("Vault creation is manual \u2014 use the command center to create one");
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Ensure the current wallet is linked to the agent.
|
|
2154
|
+
* If the trading wallet differs from the owner, enters a recovery loop
|
|
2155
|
+
* that waits for the owner to link it from the website.
|
|
2156
|
+
*/
|
|
2157
|
+
async ensureWalletLinked() {
|
|
2158
|
+
const agentId = BigInt(this.config.agentId);
|
|
2159
|
+
const address = this.client.address;
|
|
2160
|
+
const isLinked = await this.client.registry.isLinkedWallet(agentId, address);
|
|
2161
|
+
if (!isLinked) {
|
|
2162
|
+
console.log("Wallet not linked, linking now...");
|
|
2163
|
+
const agent = await this.client.registry.getAgent(agentId);
|
|
2164
|
+
if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
|
|
2165
|
+
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
2166
|
+
const nonce = await this.client.registry.getNonce(address);
|
|
2167
|
+
const linkMessage = ExagentRegistry.generateLinkMessage(
|
|
2168
|
+
address,
|
|
2169
|
+
agentId,
|
|
2170
|
+
nonce
|
|
2171
|
+
);
|
|
2172
|
+
const linkSignature = await this.client.signMessage({ raw: linkMessage });
|
|
2173
|
+
console.log("");
|
|
2174
|
+
console.log("=== WALLET LINKING REQUIRED ===");
|
|
2175
|
+
console.log("");
|
|
2176
|
+
console.log(" Your trading wallet needs to be linked to your agent.");
|
|
2177
|
+
console.log(" Open the command center and paste the values below.");
|
|
2178
|
+
console.log("");
|
|
2179
|
+
console.log(` Command Center: ${ccUrl}`);
|
|
2180
|
+
console.log("");
|
|
2181
|
+
console.log(" \u2500\u2500 Copy these two values \u2500\u2500");
|
|
2182
|
+
console.log("");
|
|
2183
|
+
console.log(` Wallet: ${address}`);
|
|
2184
|
+
console.log(` Signature: ${linkSignature}`);
|
|
2185
|
+
console.log("");
|
|
2186
|
+
openBrowser(ccUrl);
|
|
2187
|
+
console.log(" Waiting for wallet to be linked... (checking every 15s)");
|
|
2188
|
+
console.log(" Press Ctrl+C to exit.");
|
|
2189
|
+
console.log("");
|
|
2190
|
+
while (true) {
|
|
2191
|
+
await this.sleep(15e3);
|
|
2192
|
+
const linked = await this.client.registry.isLinkedWallet(agentId, address);
|
|
2193
|
+
if (linked) {
|
|
2194
|
+
console.log(" Wallet linked! Continuing setup...");
|
|
2195
|
+
console.log("");
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
process.stdout.write(".");
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
await this.client.registry.linkOwnWallet(agentId);
|
|
2202
|
+
console.log("Wallet linked successfully");
|
|
2203
|
+
} else {
|
|
2204
|
+
console.log("Wallet already linked");
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Load risk universe and allowed tokens from on-chain registry.
|
|
2209
|
+
* This prevents the agent from wasting gas on trades that will revert.
|
|
2210
|
+
*/
|
|
2211
|
+
async loadTradingRestrictions() {
|
|
2212
|
+
const agentId = BigInt(this.config.agentId);
|
|
2213
|
+
const RISK_UNIVERSE_NAMES = ["Core", "Established", "Derivatives", "Emerging", "Frontier"];
|
|
2214
|
+
try {
|
|
2215
|
+
this.riskUniverse = await this.client.registry.getRiskUniverse(agentId);
|
|
2216
|
+
console.log(`Risk universe: ${RISK_UNIVERSE_NAMES[this.riskUniverse] || this.riskUniverse}`);
|
|
2217
|
+
const configTokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
2218
|
+
const verified = [];
|
|
2219
|
+
for (const token of configTokens) {
|
|
2220
|
+
try {
|
|
2221
|
+
const allowed = await this.client.registry.isTradeAllowed(
|
|
2222
|
+
agentId,
|
|
2223
|
+
token,
|
|
2224
|
+
"0x0000000000000000000000000000000000000000"
|
|
2225
|
+
// zero = check token only
|
|
2226
|
+
);
|
|
2227
|
+
if (allowed) {
|
|
2228
|
+
this.allowedTokens.add(token.toLowerCase());
|
|
2229
|
+
verified.push(token);
|
|
2230
|
+
} else {
|
|
2231
|
+
console.warn(`Token ${token} not allowed for this agent's risk universe \u2014 excluded`);
|
|
2232
|
+
}
|
|
2233
|
+
} catch {
|
|
2234
|
+
this.allowedTokens.add(token.toLowerCase());
|
|
2235
|
+
verified.push(token);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
this.config.allowedTokens = verified;
|
|
2239
|
+
console.log(`Allowed tokens loaded: ${verified.length} tokens verified`);
|
|
2240
|
+
if (this.riskUniverse === 4) {
|
|
2241
|
+
console.warn("Frontier risk universe: vault creation is disabled");
|
|
2242
|
+
}
|
|
2243
|
+
} catch (error) {
|
|
2244
|
+
console.warn(
|
|
2245
|
+
"Could not load trading restrictions from registry (using defaults):",
|
|
2246
|
+
error instanceof Error ? error.message : error
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Sync the LLM config hash to chain for epoch tracking.
|
|
2252
|
+
* If the wallet has insufficient gas, enters a recovery loop
|
|
2253
|
+
* that waits for the user to fund the wallet.
|
|
2254
|
+
*/
|
|
2255
|
+
async syncConfigHash() {
|
|
2256
|
+
const agentId = BigInt(this.config.agentId);
|
|
2257
|
+
const llmMeta = this.llm.getMetadata();
|
|
2258
|
+
this.configHash = ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
|
|
2259
|
+
console.log(`Config hash: ${this.configHash}`);
|
|
2260
|
+
const onChainHash = await this.client.registry.getConfigHash(agentId);
|
|
2261
|
+
if (onChainHash !== this.configHash) {
|
|
2262
|
+
console.log("Config changed, updating on-chain...");
|
|
2263
|
+
try {
|
|
2264
|
+
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
2265
|
+
const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
|
|
2266
|
+
console.log(`Config updated, new epoch started: ${newEpoch}`);
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2269
|
+
if (message.includes("insufficient funds") || message.includes("gas") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
|
|
2270
|
+
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
2271
|
+
const chain = base2;
|
|
2272
|
+
const publicClientInstance = createPublicClient3({
|
|
2273
|
+
chain,
|
|
2274
|
+
transport: http3(this.getRpcUrl())
|
|
2275
|
+
});
|
|
2276
|
+
console.log("");
|
|
2277
|
+
console.log("=== ETH NEEDED FOR GAS ===");
|
|
2278
|
+
console.log("");
|
|
2279
|
+
console.log(` Wallet: ${this.client.address}`);
|
|
2280
|
+
console.log(" Your wallet needs ETH to pay for transaction gas.");
|
|
2281
|
+
console.log(" Opening the command center to fund your wallet...");
|
|
2282
|
+
console.log(` ${ccUrl}`);
|
|
2283
|
+
console.log("");
|
|
2284
|
+
openBrowser(ccUrl);
|
|
2285
|
+
console.log(" Waiting for ETH... (checking every 15s)");
|
|
2286
|
+
console.log(" Press Ctrl+C to exit.");
|
|
2287
|
+
console.log("");
|
|
2288
|
+
while (true) {
|
|
2289
|
+
await this.sleep(15e3);
|
|
2290
|
+
const balance = await publicClientInstance.getBalance({
|
|
2291
|
+
address: this.client.address
|
|
2292
|
+
});
|
|
2293
|
+
if (balance > BigInt(0)) {
|
|
2294
|
+
console.log(" ETH detected! Retrying config update...");
|
|
2295
|
+
console.log("");
|
|
2296
|
+
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
2297
|
+
const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
|
|
2298
|
+
console.log(`Config updated, new epoch started: ${newEpoch}`);
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
process.stdout.write(".");
|
|
2302
|
+
}
|
|
2303
|
+
} else {
|
|
2304
|
+
throw error;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
} else {
|
|
2308
|
+
const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
|
|
2309
|
+
console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Get the current config hash (for trade execution)
|
|
2314
|
+
*/
|
|
2315
|
+
getConfigHash() {
|
|
2316
|
+
return this.configHash;
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Start the agent in daemon mode.
|
|
2320
|
+
* The agent enters idle mode and waits for commands from the command center.
|
|
2321
|
+
* Trading begins only when a start_trading command is received.
|
|
2322
|
+
*
|
|
2323
|
+
* If relay is not configured, falls back to immediate trading mode.
|
|
2324
|
+
*/
|
|
2325
|
+
async run() {
|
|
2326
|
+
this.processAlive = true;
|
|
2327
|
+
if (this.relay) {
|
|
2328
|
+
console.log("");
|
|
2329
|
+
console.log("Agent is in IDLE mode. Waiting for commands from command center.");
|
|
2330
|
+
console.log("Visit https://exagent.io to start trading from the dashboard.");
|
|
2331
|
+
console.log("");
|
|
2332
|
+
this.mode = "idle";
|
|
2333
|
+
this.sendRelayStatus();
|
|
2334
|
+
this.relay.sendMessage(
|
|
2335
|
+
"system",
|
|
2336
|
+
"success",
|
|
2337
|
+
"Agent Connected",
|
|
2338
|
+
`${this.config.name} is online and waiting for commands.`,
|
|
2339
|
+
{ wallet: this.client.address }
|
|
2340
|
+
);
|
|
2341
|
+
while (this.processAlive) {
|
|
2342
|
+
if (this.mode === "trading" && this.isRunning) {
|
|
2343
|
+
try {
|
|
2344
|
+
await this.runCycle();
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2347
|
+
console.error("Error in trading cycle:", message);
|
|
2348
|
+
this.relay?.sendMessage(
|
|
2349
|
+
"system",
|
|
2350
|
+
"error",
|
|
2351
|
+
"Cycle Error",
|
|
2352
|
+
message
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
await this.sleep(this.config.trading.tradingIntervalMs);
|
|
2356
|
+
} else {
|
|
2357
|
+
this.sendRelayStatus();
|
|
2358
|
+
await this.sleep(3e4);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
} else {
|
|
2362
|
+
if (this.isRunning) {
|
|
2363
|
+
throw new Error("Agent is already running");
|
|
2364
|
+
}
|
|
2365
|
+
this.isRunning = true;
|
|
2366
|
+
this.mode = "trading";
|
|
2367
|
+
console.log("Starting trading loop...");
|
|
2368
|
+
console.log(`Interval: ${this.config.trading.tradingIntervalMs}ms`);
|
|
2369
|
+
while (this.isRunning) {
|
|
2370
|
+
try {
|
|
2371
|
+
await this.runCycle();
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
console.error("Error in trading cycle:", error);
|
|
2374
|
+
}
|
|
2375
|
+
await this.sleep(this.config.trading.tradingIntervalMs);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Handle a command from the command center
|
|
2381
|
+
*/
|
|
2382
|
+
async handleCommand(cmd) {
|
|
2383
|
+
console.log(`Command received: ${cmd.type}`);
|
|
2384
|
+
try {
|
|
2385
|
+
switch (cmd.type) {
|
|
2386
|
+
case "start_trading":
|
|
2387
|
+
if (this.mode === "trading") {
|
|
2388
|
+
this.relay?.sendCommandResult(cmd.id, true, "Already trading");
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
this.mode = "trading";
|
|
2392
|
+
this.isRunning = true;
|
|
2393
|
+
console.log("Trading started via command center");
|
|
2394
|
+
this.relay?.sendCommandResult(cmd.id, true, "Trading started");
|
|
2395
|
+
this.relay?.sendMessage(
|
|
2396
|
+
"system",
|
|
2397
|
+
"success",
|
|
2398
|
+
"Trading Started",
|
|
2399
|
+
"Agent is now actively trading."
|
|
2400
|
+
);
|
|
2401
|
+
this.sendRelayStatus();
|
|
2402
|
+
break;
|
|
2403
|
+
case "stop_trading":
|
|
2404
|
+
if (this.mode === "idle") {
|
|
2405
|
+
this.relay?.sendCommandResult(cmd.id, true, "Already idle");
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
this.mode = "idle";
|
|
2409
|
+
this.isRunning = false;
|
|
2410
|
+
console.log("Trading stopped via command center");
|
|
2411
|
+
this.relay?.sendCommandResult(cmd.id, true, "Trading stopped");
|
|
2412
|
+
this.relay?.sendMessage(
|
|
2413
|
+
"system",
|
|
2414
|
+
"info",
|
|
2415
|
+
"Trading Stopped",
|
|
2416
|
+
"Agent is now idle. Send start_trading to resume."
|
|
2417
|
+
);
|
|
2418
|
+
this.sendRelayStatus();
|
|
2419
|
+
break;
|
|
2420
|
+
case "update_risk_params": {
|
|
2421
|
+
const params = cmd.params || {};
|
|
2422
|
+
let updated = false;
|
|
2423
|
+
if (params.maxPositionSizeBps !== void 0) {
|
|
2424
|
+
const value = Number(params.maxPositionSizeBps);
|
|
2425
|
+
if (isNaN(value) || value < 100 || value > 1e4) {
|
|
2426
|
+
this.relay?.sendCommandResult(cmd.id, false, "maxPositionSizeBps must be 100-10000");
|
|
2427
|
+
break;
|
|
2428
|
+
}
|
|
2429
|
+
this.config.trading.maxPositionSizeBps = value;
|
|
2430
|
+
updated = true;
|
|
2431
|
+
}
|
|
2432
|
+
if (params.maxDailyLossBps !== void 0) {
|
|
2433
|
+
const value = Number(params.maxDailyLossBps);
|
|
2434
|
+
if (isNaN(value) || value < 50 || value > 5e3) {
|
|
2435
|
+
this.relay?.sendCommandResult(cmd.id, false, "maxDailyLossBps must be 50-5000");
|
|
2436
|
+
break;
|
|
2437
|
+
}
|
|
2438
|
+
this.config.trading.maxDailyLossBps = value;
|
|
2439
|
+
updated = true;
|
|
2440
|
+
}
|
|
2441
|
+
if (updated) {
|
|
2442
|
+
this.riskManager = new RiskManager(this.config.trading);
|
|
2443
|
+
console.log("Risk params updated via command center");
|
|
2444
|
+
this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
|
|
2445
|
+
this.relay?.sendMessage(
|
|
2446
|
+
"config_updated",
|
|
2447
|
+
"info",
|
|
2448
|
+
"Risk Parameters Updated",
|
|
2449
|
+
`Max position: ${this.config.trading.maxPositionSizeBps / 100}%, Max daily loss: ${this.config.trading.maxDailyLossBps / 100}%`
|
|
2450
|
+
);
|
|
2451
|
+
} else {
|
|
2452
|
+
this.relay?.sendCommandResult(cmd.id, false, "No valid parameters provided");
|
|
2453
|
+
}
|
|
2454
|
+
break;
|
|
2455
|
+
}
|
|
2456
|
+
case "update_trading_interval": {
|
|
2457
|
+
const intervalMs = Number(cmd.params?.intervalMs);
|
|
2458
|
+
if (intervalMs && intervalMs >= 1e3) {
|
|
2459
|
+
this.config.trading.tradingIntervalMs = intervalMs;
|
|
2460
|
+
console.log(`Trading interval updated to ${intervalMs}ms`);
|
|
2461
|
+
this.relay?.sendCommandResult(cmd.id, true, `Interval set to ${intervalMs}ms`);
|
|
2462
|
+
} else {
|
|
2463
|
+
this.relay?.sendCommandResult(cmd.id, false, "Invalid interval (minimum 1000ms)");
|
|
2464
|
+
}
|
|
2465
|
+
break;
|
|
2466
|
+
}
|
|
2467
|
+
case "create_vault": {
|
|
2468
|
+
const result = await this.createVault();
|
|
2469
|
+
this.relay?.sendCommandResult(
|
|
2470
|
+
cmd.id,
|
|
2471
|
+
result.success,
|
|
2472
|
+
result.success ? `Vault created: ${result.vaultAddress}` : result.error
|
|
2473
|
+
);
|
|
2474
|
+
if (result.success) {
|
|
2475
|
+
this.relay?.sendMessage(
|
|
2476
|
+
"vault_created",
|
|
2477
|
+
"success",
|
|
2478
|
+
"Vault Created",
|
|
2479
|
+
`Vault deployed at ${result.vaultAddress}`,
|
|
2480
|
+
{ vaultAddress: result.vaultAddress }
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
break;
|
|
2484
|
+
}
|
|
2485
|
+
case "refresh_status":
|
|
2486
|
+
this.sendRelayStatus();
|
|
2487
|
+
this.relay?.sendCommandResult(cmd.id, true, "Status refreshed");
|
|
2488
|
+
break;
|
|
2489
|
+
case "shutdown":
|
|
2490
|
+
console.log("Shutdown requested via command center");
|
|
2491
|
+
this.relay?.sendCommandResult(cmd.id, true, "Shutting down");
|
|
2492
|
+
this.relay?.sendMessage(
|
|
2493
|
+
"system",
|
|
2494
|
+
"info",
|
|
2495
|
+
"Shutting Down",
|
|
2496
|
+
"Agent is shutting down. Restart manually to reconnect."
|
|
2497
|
+
);
|
|
2498
|
+
await this.sleep(1e3);
|
|
2499
|
+
this.stop();
|
|
2500
|
+
break;
|
|
2501
|
+
default:
|
|
2502
|
+
console.warn(`Unknown command: ${cmd.type}`);
|
|
2503
|
+
this.relay?.sendCommandResult(cmd.id, false, `Unknown command: ${cmd.type}`);
|
|
2504
|
+
}
|
|
2505
|
+
} catch (error) {
|
|
2506
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2507
|
+
console.error(`Command ${cmd.type} failed:`, message);
|
|
2508
|
+
this.relay?.sendCommandResult(cmd.id, false, message);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Send current status to the relay
|
|
2513
|
+
*/
|
|
2514
|
+
sendRelayStatus() {
|
|
2515
|
+
if (!this.relay) return;
|
|
2516
|
+
const vaultConfig = this.config.vault || { policy: "disabled" };
|
|
2517
|
+
const status = {
|
|
2518
|
+
mode: this.mode,
|
|
2519
|
+
agentId: String(this.config.agentId),
|
|
2520
|
+
wallet: this.client?.address,
|
|
2521
|
+
cycleCount: this.cycleCount,
|
|
2522
|
+
lastCycleAt: this.lastCycleAt,
|
|
2523
|
+
tradingIntervalMs: this.config.trading.tradingIntervalMs,
|
|
2524
|
+
portfolioValue: this.lastPortfolioValue,
|
|
2525
|
+
ethBalance: this.lastEthBalance,
|
|
2526
|
+
llm: {
|
|
2527
|
+
provider: this.config.llm.provider,
|
|
2528
|
+
model: this.config.llm.model || "default"
|
|
2529
|
+
},
|
|
2530
|
+
risk: this.riskManager?.getStatus(this.lastPortfolioValue) || {
|
|
2531
|
+
dailyPnL: 0,
|
|
2532
|
+
dailyLossLimit: 0,
|
|
2533
|
+
isLimitHit: false
|
|
2534
|
+
},
|
|
2535
|
+
vault: {
|
|
2536
|
+
policy: vaultConfig.policy,
|
|
2537
|
+
hasVault: false,
|
|
2538
|
+
vaultAddress: null
|
|
2539
|
+
}
|
|
2540
|
+
};
|
|
2541
|
+
this.relay.sendHeartbeat(status);
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Run a single trading cycle
|
|
2545
|
+
*/
|
|
2546
|
+
async runCycle() {
|
|
2547
|
+
console.log(`
|
|
2548
|
+
--- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
|
|
2549
|
+
this.cycleCount++;
|
|
2550
|
+
this.lastCycleAt = Date.now();
|
|
2551
|
+
const tokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
2552
|
+
const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
|
|
2553
|
+
console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
|
|
2554
|
+
this.lastPortfolioValue = marketData.portfolioValue;
|
|
2555
|
+
const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
2556
|
+
this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
|
|
2557
|
+
const fundsOk = this.checkFundsLow(marketData);
|
|
2558
|
+
if (!fundsOk) {
|
|
2559
|
+
console.warn("Skipping trading cycle \u2014 ETH balance critically low");
|
|
2560
|
+
this.sendRelayStatus();
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
let signals;
|
|
2564
|
+
try {
|
|
2565
|
+
signals = await this.strategy(marketData, this.llm, this.config);
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2568
|
+
console.error("LLM/strategy error:", message);
|
|
2569
|
+
this.relay?.sendMessage(
|
|
2570
|
+
"llm_error",
|
|
2571
|
+
"error",
|
|
2572
|
+
"Strategy Error",
|
|
2573
|
+
message
|
|
2574
|
+
);
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
console.log(`Strategy generated ${signals.length} signals`);
|
|
2578
|
+
const filteredSignals = this.riskManager.filterSignals(signals, marketData);
|
|
2579
|
+
console.log(`${filteredSignals.length} signals passed risk checks`);
|
|
2580
|
+
if (this.riskManager.getStatus(marketData.portfolioValue).isLimitHit) {
|
|
2581
|
+
this.relay?.sendMessage(
|
|
2582
|
+
"risk_limit_hit",
|
|
2583
|
+
"warning",
|
|
2584
|
+
"Risk Limit Hit",
|
|
2585
|
+
`Daily loss limit reached: $${this.riskManager.getStatus(marketData.portfolioValue).dailyPnL.toFixed(2)}`
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
if (filteredSignals.length > 0) {
|
|
2589
|
+
const vaultStatus = await this.vaultManager?.getVaultStatus();
|
|
2590
|
+
if (vaultStatus?.hasVault && this.vaultManager?.preferVaultTrading) {
|
|
2591
|
+
console.log(`Trading through vault: ${vaultStatus.vaultAddress}`);
|
|
2592
|
+
}
|
|
2593
|
+
const preTradePortfolioValue = marketData.portfolioValue;
|
|
2594
|
+
const results = await this.executor.executeAll(filteredSignals);
|
|
2595
|
+
let totalFeesUSD = 0;
|
|
2596
|
+
for (const result of results) {
|
|
2597
|
+
if (result.success) {
|
|
2598
|
+
console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
|
|
2599
|
+
const feeCostBps = 20;
|
|
2600
|
+
const signalPrice = marketData.prices[result.signal.tokenIn.toLowerCase()] || 0;
|
|
2601
|
+
const amountUSD = Number(result.signal.amountIn) / Math.pow(10, getTokenDecimals(result.signal.tokenIn)) * signalPrice;
|
|
2602
|
+
const feeCostUSD = amountUSD * feeCostBps / 1e4;
|
|
2603
|
+
totalFeesUSD += feeCostUSD;
|
|
2604
|
+
this.riskManager.updateFees(feeCostUSD);
|
|
2605
|
+
this.relay?.sendMessage(
|
|
2606
|
+
"trade_executed",
|
|
2607
|
+
"success",
|
|
2608
|
+
"Trade Executed",
|
|
2609
|
+
`${result.signal.action.toUpperCase()}: ${result.signal.reasoning || "No reason provided"}`,
|
|
2610
|
+
{
|
|
2611
|
+
action: result.signal.action,
|
|
2612
|
+
txHash: result.txHash,
|
|
2613
|
+
tokenIn: result.signal.tokenIn,
|
|
2614
|
+
tokenOut: result.signal.tokenOut
|
|
2615
|
+
}
|
|
2616
|
+
);
|
|
2617
|
+
} else {
|
|
2618
|
+
console.warn(`Trade failed: ${result.error}`);
|
|
2619
|
+
this.relay?.sendMessage(
|
|
2620
|
+
"trade_failed",
|
|
2621
|
+
"error",
|
|
2622
|
+
"Trade Failed",
|
|
2623
|
+
result.error || "Unknown error",
|
|
2624
|
+
{ action: result.signal.action }
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
const postTokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
2629
|
+
const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
|
|
2630
|
+
const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
|
|
2631
|
+
this.riskManager.updatePnL(marketPnL);
|
|
2632
|
+
if (marketPnL !== 0) {
|
|
2633
|
+
console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
|
|
2634
|
+
}
|
|
2635
|
+
this.lastPortfolioValue = postTradeData.portfolioValue;
|
|
2636
|
+
const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
2637
|
+
this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
|
|
2638
|
+
}
|
|
2639
|
+
this.sendRelayStatus();
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2642
|
+
* Check if ETH balance is below threshold and notify.
|
|
2643
|
+
* Returns true if trading should continue, false if ETH is critically low.
|
|
2644
|
+
*/
|
|
2645
|
+
checkFundsLow(marketData) {
|
|
2646
|
+
const ethBalance = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
2647
|
+
const ethAmount = Number(ethBalance) / 1e18;
|
|
2648
|
+
if (ethAmount < FUNDS_CRITICAL_THRESHOLD) {
|
|
2649
|
+
console.error(`ETH balance critically low: ${ethAmount.toFixed(6)} ETH \u2014 halting trading`);
|
|
2650
|
+
this.relay?.sendMessage(
|
|
2651
|
+
"funds_low",
|
|
2652
|
+
"error",
|
|
2653
|
+
"Funds Critical",
|
|
2654
|
+
`ETH balance is ${ethAmount.toFixed(6)} ETH (below ${FUNDS_CRITICAL_THRESHOLD} ETH minimum). Trading halted \u2014 fund your wallet.`,
|
|
2655
|
+
{
|
|
2656
|
+
ethBalance: ethAmount.toFixed(6),
|
|
2657
|
+
wallet: this.client.address,
|
|
2658
|
+
threshold: FUNDS_CRITICAL_THRESHOLD
|
|
2659
|
+
}
|
|
2660
|
+
);
|
|
2661
|
+
return false;
|
|
2662
|
+
}
|
|
2663
|
+
if (ethAmount < FUNDS_LOW_THRESHOLD) {
|
|
2664
|
+
this.relay?.sendMessage(
|
|
2665
|
+
"funds_low",
|
|
2666
|
+
"warning",
|
|
2667
|
+
"Low Funds",
|
|
2668
|
+
`ETH balance is ${ethAmount.toFixed(6)} ETH. Fund your trading wallet to continue trading.`,
|
|
2669
|
+
{
|
|
2670
|
+
ethBalance: ethAmount.toFixed(6),
|
|
2671
|
+
wallet: this.client.address,
|
|
2672
|
+
threshold: FUNDS_LOW_THRESHOLD
|
|
2673
|
+
}
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
return true;
|
|
2677
|
+
}
|
|
2678
|
+
/**
|
|
2679
|
+
* Stop the agent process completely
|
|
2680
|
+
*/
|
|
2681
|
+
stop() {
|
|
2682
|
+
console.log("Stopping agent...");
|
|
2683
|
+
this.isRunning = false;
|
|
2684
|
+
this.processAlive = false;
|
|
2685
|
+
this.mode = "idle";
|
|
2686
|
+
if (this.relay) {
|
|
2687
|
+
this.relay.disconnect();
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Get RPC URL from environment or default
|
|
2692
|
+
*/
|
|
2693
|
+
getRpcUrl() {
|
|
2694
|
+
return getRpcUrl();
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Default tokens to track.
|
|
2698
|
+
* These are validated against the on-chain registry's isTradeAllowed() during init —
|
|
2699
|
+
* agents in restricted risk universes will have ineligible tokens filtered out.
|
|
2700
|
+
*/
|
|
2701
|
+
getDefaultTokens() {
|
|
2702
|
+
return [
|
|
2703
|
+
// Core
|
|
2704
|
+
"0x4200000000000000000000000000000000000006",
|
|
2705
|
+
// WETH
|
|
2706
|
+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
2707
|
+
// USDC
|
|
2708
|
+
"0x2Ae3F1Ec7F1F5012CFEab0185bFC7aa3cf0DEC22",
|
|
2709
|
+
// cbETH
|
|
2710
|
+
"0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA",
|
|
2711
|
+
// USDbC
|
|
2712
|
+
"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
|
|
2713
|
+
// DAI
|
|
2714
|
+
// Established
|
|
2715
|
+
"0x940181a94A35A4569E4529A3CDfB74e38FD98631",
|
|
2716
|
+
// AERO
|
|
2717
|
+
"0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
|
|
2718
|
+
// cbBTC
|
|
2719
|
+
"0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
|
|
2720
|
+
// wstETH
|
|
2721
|
+
"0x2416092f143378750bb29b79eD961ab195CcEea5",
|
|
2722
|
+
// ezETH
|
|
2723
|
+
// Emerging (filtered by risk universe at init)
|
|
2724
|
+
"0x532f27101965dd16442E59d40670FaF5eBB142E4",
|
|
2725
|
+
// BRETT
|
|
2726
|
+
"0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
|
|
2727
|
+
// DEGEN
|
|
2728
|
+
"0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b",
|
|
2729
|
+
// VIRTUAL
|
|
2730
|
+
"0xAC1Bd2486Aaf3B5C0fc3Fd868558b082a531B2B4"
|
|
2731
|
+
// TOSHI
|
|
2732
|
+
];
|
|
2733
|
+
}
|
|
2734
|
+
sleep(ms) {
|
|
2735
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Get current status
|
|
2739
|
+
*/
|
|
2740
|
+
getStatus() {
|
|
2741
|
+
const vaultConfig = this.config.vault || { policy: "disabled" };
|
|
2742
|
+
return {
|
|
2743
|
+
isRunning: this.isRunning,
|
|
2744
|
+
mode: this.mode,
|
|
2745
|
+
agentId: Number(this.config.agentId),
|
|
2746
|
+
wallet: this.client?.address || "not initialized",
|
|
2747
|
+
llm: {
|
|
2748
|
+
provider: this.config.llm.provider,
|
|
2749
|
+
model: this.config.llm.model || "default"
|
|
2750
|
+
},
|
|
2751
|
+
configHash: this.configHash || "not initialized",
|
|
2752
|
+
risk: this.riskManager?.getStatus(this.lastPortfolioValue) || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false },
|
|
2753
|
+
vault: {
|
|
2754
|
+
policy: vaultConfig.policy,
|
|
2755
|
+
hasVault: false,
|
|
2756
|
+
// Updated async via getVaultStatus
|
|
2757
|
+
vaultAddress: null
|
|
2758
|
+
},
|
|
2759
|
+
relay: {
|
|
2760
|
+
connected: this.relay?.isConnected || false
|
|
2761
|
+
},
|
|
2762
|
+
cycleCount: this.cycleCount
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Get detailed vault status (async)
|
|
2767
|
+
*/
|
|
2768
|
+
async getVaultStatus() {
|
|
2769
|
+
if (!this.vaultManager) {
|
|
2770
|
+
return null;
|
|
2771
|
+
}
|
|
2772
|
+
return this.vaultManager.getVaultStatus();
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Manually trigger vault creation (for 'manual' policy)
|
|
2776
|
+
*/
|
|
2777
|
+
async createVault() {
|
|
2778
|
+
if (!this.vaultManager) {
|
|
2779
|
+
return { success: false, error: "Vault manager not initialized" };
|
|
2780
|
+
}
|
|
2781
|
+
const policy = this.config.vault?.policy || "disabled";
|
|
2782
|
+
if (policy === "disabled") {
|
|
2783
|
+
return { success: false, error: "Vault creation is disabled by policy" };
|
|
2784
|
+
}
|
|
2785
|
+
return this.vaultManager.createVault();
|
|
2786
|
+
}
|
|
2787
|
+
};
|
|
2788
|
+
|
|
2789
|
+
// src/secure-env.ts
|
|
2790
|
+
import * as crypto from "crypto";
|
|
2791
|
+
import * as fs from "fs";
|
|
2792
|
+
import * as path from "path";
|
|
2793
|
+
var ALGORITHM = "aes-256-gcm";
|
|
2794
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
2795
|
+
var SALT_LENGTH = 32;
|
|
2796
|
+
var IV_LENGTH = 16;
|
|
2797
|
+
var KEY_LENGTH = 32;
|
|
2798
|
+
var SENSITIVE_PATTERNS = [
|
|
2799
|
+
/PRIVATE_KEY$/i,
|
|
2800
|
+
/_API_KEY$/i,
|
|
2801
|
+
/API_KEY$/i,
|
|
2802
|
+
/_SECRET$/i,
|
|
2803
|
+
/^OPENAI_API_KEY$/i,
|
|
2804
|
+
/^ANTHROPIC_API_KEY$/i,
|
|
2805
|
+
/^GOOGLE_AI_API_KEY$/i,
|
|
2806
|
+
/^DEEPSEEK_API_KEY$/i,
|
|
2807
|
+
/^MISTRAL_API_KEY$/i,
|
|
2808
|
+
/^GROQ_API_KEY$/i,
|
|
2809
|
+
/^TOGETHER_API_KEY$/i
|
|
2810
|
+
];
|
|
2811
|
+
function isSensitiveKey(key) {
|
|
2812
|
+
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
|
|
2813
|
+
}
|
|
2814
|
+
function deriveKey(passphrase, salt) {
|
|
2815
|
+
return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
2816
|
+
}
|
|
2817
|
+
function encryptValue(value, key) {
|
|
2818
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
2819
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
2820
|
+
let encrypted = cipher.update(value, "utf8", "hex");
|
|
2821
|
+
encrypted += cipher.final("hex");
|
|
2822
|
+
const tag = cipher.getAuthTag();
|
|
2823
|
+
return {
|
|
2824
|
+
iv: iv.toString("hex"),
|
|
2825
|
+
encrypted,
|
|
2826
|
+
tag: tag.toString("hex")
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
function decryptValue(encrypted, key, iv, tag) {
|
|
2830
|
+
const decipher = crypto.createDecipheriv(
|
|
2831
|
+
ALGORITHM,
|
|
2832
|
+
key,
|
|
2833
|
+
Buffer.from(iv, "hex")
|
|
2834
|
+
);
|
|
2835
|
+
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
2836
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
2837
|
+
decrypted += decipher.final("utf8");
|
|
2838
|
+
return decrypted;
|
|
2839
|
+
}
|
|
2840
|
+
function parseEnvFile(content) {
|
|
2841
|
+
const entries = [];
|
|
2842
|
+
for (const line of content.split("\n")) {
|
|
2843
|
+
const trimmed = line.trim();
|
|
2844
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2845
|
+
const eqIndex = trimmed.indexOf("=");
|
|
2846
|
+
if (eqIndex === -1) continue;
|
|
2847
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
2848
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
2849
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2850
|
+
value = value.slice(1, -1);
|
|
2851
|
+
}
|
|
2852
|
+
if (key && value) {
|
|
2853
|
+
entries.push({ key, value });
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return entries;
|
|
2857
|
+
}
|
|
2858
|
+
function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
|
|
2859
|
+
if (!fs.existsSync(envPath)) {
|
|
2860
|
+
throw new Error(`File not found: ${envPath}`);
|
|
2861
|
+
}
|
|
2862
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
2863
|
+
const entries = parseEnvFile(content);
|
|
2864
|
+
if (entries.length === 0) {
|
|
2865
|
+
throw new Error("No environment variables found in file");
|
|
2866
|
+
}
|
|
2867
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
2868
|
+
const key = deriveKey(passphrase, salt);
|
|
2869
|
+
const encryptedEntries = entries.map(({ key: envKey, value }) => {
|
|
2870
|
+
if (isSensitiveKey(envKey)) {
|
|
2871
|
+
const { iv, encrypted, tag } = encryptValue(value, key);
|
|
2872
|
+
return {
|
|
2873
|
+
key: envKey,
|
|
2874
|
+
value: encrypted,
|
|
2875
|
+
encrypted: true,
|
|
2876
|
+
iv,
|
|
2877
|
+
tag
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
return {
|
|
2881
|
+
key: envKey,
|
|
2882
|
+
value,
|
|
2883
|
+
encrypted: false
|
|
2884
|
+
};
|
|
2885
|
+
});
|
|
2886
|
+
const encryptedEnv = {
|
|
2887
|
+
version: 1,
|
|
2888
|
+
salt: salt.toString("hex"),
|
|
2889
|
+
entries: encryptedEntries
|
|
2890
|
+
};
|
|
2891
|
+
const encPath = envPath + ".enc";
|
|
2892
|
+
fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
|
|
2893
|
+
if (deleteOriginal) {
|
|
2894
|
+
fs.unlinkSync(envPath);
|
|
2895
|
+
}
|
|
2896
|
+
const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
|
|
2897
|
+
const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
|
|
2898
|
+
console.log(
|
|
2899
|
+
`Encrypted ${sensitiveCount} sensitive values (${plainCount} non-sensitive kept as plaintext)`
|
|
2900
|
+
);
|
|
2901
|
+
return encPath;
|
|
2902
|
+
}
|
|
2903
|
+
function decryptEnvFile(encPath, passphrase) {
|
|
2904
|
+
if (!fs.existsSync(encPath)) {
|
|
2905
|
+
throw new Error(`Encrypted env file not found: ${encPath}`);
|
|
2906
|
+
}
|
|
2907
|
+
const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
|
|
2908
|
+
if (content.version !== 1) {
|
|
2909
|
+
throw new Error(`Unsupported encrypted env version: ${content.version}`);
|
|
2910
|
+
}
|
|
2911
|
+
const salt = Buffer.from(content.salt, "hex");
|
|
2912
|
+
const key = deriveKey(passphrase, salt);
|
|
2913
|
+
const result = {};
|
|
2914
|
+
for (const entry of content.entries) {
|
|
2915
|
+
if (entry.encrypted) {
|
|
2916
|
+
if (!entry.iv || !entry.tag) {
|
|
2917
|
+
throw new Error(`Missing encryption metadata for ${entry.key}`);
|
|
2918
|
+
}
|
|
2919
|
+
try {
|
|
2920
|
+
result[entry.key] = decryptValue(entry.value, key, entry.iv, entry.tag);
|
|
2921
|
+
} catch {
|
|
2922
|
+
throw new Error(
|
|
2923
|
+
`Failed to decrypt ${entry.key}. Wrong passphrase or corrupted data.`
|
|
2924
|
+
);
|
|
2925
|
+
}
|
|
2926
|
+
} else {
|
|
2927
|
+
result[entry.key] = entry.value;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
return result;
|
|
2931
|
+
}
|
|
2932
|
+
function loadSecureEnv(basePath, passphrase) {
|
|
2933
|
+
const encPath = path.join(basePath, ".env.enc");
|
|
2934
|
+
const envPath = path.join(basePath, ".env");
|
|
2935
|
+
if (fs.existsSync(encPath)) {
|
|
2936
|
+
if (!passphrase) {
|
|
2937
|
+
passphrase = process.env.EXAGENT_PASSPHRASE;
|
|
2938
|
+
}
|
|
2939
|
+
if (!passphrase) {
|
|
2940
|
+
console.warn("");
|
|
2941
|
+
console.warn("WARNING: Found .env.enc but no passphrase provided.");
|
|
2942
|
+
console.warn(" Set EXAGENT_PASSPHRASE environment variable or");
|
|
2943
|
+
console.warn(" pass --passphrase when running the agent.");
|
|
2944
|
+
console.warn(" Falling back to plaintext .env file.");
|
|
2945
|
+
console.warn("");
|
|
2946
|
+
} else {
|
|
2947
|
+
const vars = decryptEnvFile(encPath, passphrase);
|
|
2948
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
2949
|
+
process.env[key] = value;
|
|
2950
|
+
}
|
|
2951
|
+
return true;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
if (fs.existsSync(envPath)) {
|
|
2955
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
2956
|
+
const entries = parseEnvFile(content);
|
|
2957
|
+
const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
|
|
2958
|
+
if (sensitiveKeys.length > 0) {
|
|
2959
|
+
console.warn("");
|
|
2960
|
+
console.warn("WARNING: Sensitive values stored in plaintext .env file:");
|
|
2961
|
+
for (const key of sensitiveKeys) {
|
|
2962
|
+
console.warn(` - ${key}`);
|
|
2963
|
+
}
|
|
2964
|
+
console.warn("");
|
|
2965
|
+
console.warn(' Run "npx @exagent/agent encrypt" to secure your keys.');
|
|
2966
|
+
console.warn("");
|
|
2967
|
+
}
|
|
2968
|
+
return false;
|
|
2969
|
+
}
|
|
2970
|
+
return false;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
export {
|
|
2974
|
+
BaseLLMAdapter,
|
|
2975
|
+
OpenAIAdapter,
|
|
2976
|
+
AnthropicAdapter,
|
|
2977
|
+
GoogleAdapter,
|
|
2978
|
+
DeepSeekAdapter,
|
|
2979
|
+
MistralAdapter,
|
|
2980
|
+
GroqAdapter,
|
|
2981
|
+
TogetherAdapter,
|
|
2982
|
+
OllamaAdapter,
|
|
2983
|
+
createLLMAdapter,
|
|
2984
|
+
loadStrategy,
|
|
2985
|
+
validateStrategy,
|
|
2986
|
+
STRATEGY_TEMPLATES,
|
|
2987
|
+
getStrategyTemplate,
|
|
2988
|
+
getAllStrategyTemplates,
|
|
2989
|
+
LLMProviderSchema,
|
|
2990
|
+
LLMConfigSchema,
|
|
2991
|
+
RiskUniverseSchema,
|
|
2992
|
+
TradingConfigSchema,
|
|
2993
|
+
VaultPolicySchema,
|
|
2994
|
+
VaultConfigSchema,
|
|
2995
|
+
RelayConfigSchema,
|
|
2996
|
+
AgentConfigSchema,
|
|
2997
|
+
loadConfig,
|
|
2998
|
+
validateConfig,
|
|
2999
|
+
createSampleConfig,
|
|
3000
|
+
TradeExecutor,
|
|
3001
|
+
MarketDataService,
|
|
3002
|
+
RiskManager,
|
|
3003
|
+
VaultManager,
|
|
3004
|
+
RelayClient,
|
|
3005
|
+
AgentRuntime,
|
|
3006
|
+
encryptEnvFile,
|
|
3007
|
+
decryptEnvFile,
|
|
3008
|
+
loadSecureEnv
|
|
3009
|
+
};
|