@exagent/agent 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -31,105 +31,641 @@ var fs2 = __toESM(require("fs"));
31
31
  var path2 = __toESM(require("path"));
32
32
 
33
33
  // src/runtime.ts
34
- var import_sdk2 = require("@exagent/sdk");
34
+ var import_sdk = require("@exagent/sdk");
35
35
  var import_viem6 = require("viem");
36
36
  var import_chains4 = require("viem/chains");
37
37
  var import_accounts5 = require("viem/accounts");
38
38
 
39
- // src/llm/openai.ts
40
- var import_openai = __toESM(require("openai"));
41
-
42
- // src/llm/base.ts
43
- var BaseLLMAdapter = class {
44
- config;
45
- constructor(config) {
46
- this.config = config;
39
+ // src/trading/market.ts
40
+ var import_viem = require("viem");
41
+ var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
42
+ var TOKEN_DECIMALS = {
43
+ // Base Mainnet Core tokens
44
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
45
+ // USDC
46
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
47
+ // USDbC
48
+ "0x4200000000000000000000000000000000000006": 18,
49
+ // WETH
50
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
51
+ // DAI
52
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
53
+ // cbETH
54
+ [NATIVE_ETH.toLowerCase()]: 18,
55
+ // Native ETH
56
+ // Base Mainnet — Established tokens
57
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
58
+ // AERO (Aerodrome)
59
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
60
+ // BRETT
61
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
62
+ // DEGEN
63
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
64
+ // VIRTUAL
65
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
66
+ // TOSHI
67
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
68
+ // cbBTC
69
+ "0x2416092f143378750bb29b79ed961ab195cceea5": 18,
70
+ // ezETH (Renzo)
71
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
72
+ // wstETH (Lido)
73
+ };
74
+ function getTokenDecimals(address) {
75
+ const decimals = TOKEN_DECIMALS[address.toLowerCase()];
76
+ if (decimals === void 0) {
77
+ console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
78
+ return 18;
47
79
  }
48
- getMetadata() {
80
+ return decimals;
81
+ }
82
+ var TOKEN_TO_COINGECKO = {
83
+ // Core
84
+ "0x4200000000000000000000000000000000000006": "ethereum",
85
+ // WETH
86
+ [NATIVE_ETH.toLowerCase()]: "ethereum",
87
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
88
+ // USDC
89
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
90
+ // USDbC
91
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
92
+ // cbETH
93
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
94
+ // DAI
95
+ // Established
96
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
97
+ // AERO
98
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
99
+ // BRETT
100
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
101
+ // DEGEN
102
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
103
+ // VIRTUAL
104
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
105
+ // TOSHI
106
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
107
+ // cbBTC
108
+ "0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
109
+ // ezETH
110
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
111
+ // wstETH
112
+ };
113
+ var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
114
+ var PRICE_STALENESS_MS = 6e4;
115
+ var MarketDataService = class {
116
+ rpcUrl;
117
+ client;
118
+ /** Cached prices from last fetch */
119
+ cachedPrices = {};
120
+ /** Timestamp of last successful price fetch */
121
+ lastPriceFetchAt = 0;
122
+ constructor(rpcUrl) {
123
+ this.rpcUrl = rpcUrl;
124
+ this.client = (0, import_viem.createPublicClient)({
125
+ transport: (0, import_viem.http)(rpcUrl)
126
+ });
127
+ }
128
+ /** Cached volume data */
129
+ cachedVolume24h = {};
130
+ /** Cached price change data */
131
+ cachedPriceChange24h = {};
132
+ /**
133
+ * Fetch current market data for the agent
134
+ */
135
+ async fetchMarketData(walletAddress, tokenAddresses) {
136
+ const prices = await this.fetchPrices(tokenAddresses);
137
+ const balances = await this.fetchBalances(walletAddress, tokenAddresses);
138
+ const portfolioValue = this.calculatePortfolioValue(balances, prices);
139
+ let gasPrice;
140
+ try {
141
+ gasPrice = await this.client.getGasPrice();
142
+ } catch {
143
+ }
49
144
  return {
50
- provider: this.config.provider,
51
- model: this.config.model || "unknown",
52
- isLocal: this.config.provider === "ollama"
145
+ timestamp: Date.now(),
146
+ prices,
147
+ balances,
148
+ portfolioValue,
149
+ volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
150
+ priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
151
+ gasPrice,
152
+ network: {
153
+ chainId: this.client.chain?.id ?? 8453
154
+ }
53
155
  };
54
156
  }
55
157
  /**
56
- * Format model name for display
158
+ * Check if cached prices are still fresh
57
159
  */
58
- getDisplayModel() {
59
- if (this.config.provider === "ollama") {
60
- return `Local (${this.config.model || "ollama"})`;
61
- }
62
- return this.config.model || this.config.provider;
160
+ get pricesAreFresh() {
161
+ return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
63
162
  }
64
- };
65
-
66
- // src/llm/openai.ts
67
- var OpenAIAdapter = class extends BaseLLMAdapter {
68
- client;
69
- constructor(config) {
70
- super(config);
71
- if (!config.apiKey && !config.endpoint) {
72
- throw new Error("OpenAI API key or custom endpoint required");
163
+ /**
164
+ * Fetch token prices from CoinGecko free API
165
+ * Returns cached prices if still fresh (<60s old)
166
+ */
167
+ async fetchPrices(tokenAddresses) {
168
+ if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
169
+ const prices2 = { ...this.cachedPrices };
170
+ for (const addr of tokenAddresses) {
171
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
172
+ if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
173
+ prices2[addr.toLowerCase()] = 1;
174
+ }
175
+ }
176
+ return prices2;
73
177
  }
74
- this.client = new import_openai.default({
75
- apiKey: config.apiKey || "not-needed-for-custom",
76
- baseURL: config.endpoint
77
- });
78
- }
79
- async chat(messages) {
80
- try {
81
- const response = await this.client.chat.completions.create({
82
- model: this.config.model || "gpt-4.1",
83
- messages: messages.map((m) => ({
84
- role: m.role,
85
- content: m.content
86
- })),
87
- temperature: this.config.temperature,
88
- max_tokens: this.config.maxTokens
89
- });
90
- const choice = response.choices[0];
91
- if (!choice || !choice.message) {
92
- throw new Error("No response from OpenAI");
178
+ const prices = {};
179
+ const idsToFetch = /* @__PURE__ */ new Set();
180
+ for (const addr of tokenAddresses) {
181
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
182
+ if (cgId && !STABLECOIN_IDS.has(cgId)) {
183
+ idsToFetch.add(cgId);
93
184
  }
94
- return {
95
- content: choice.message.content || "",
96
- usage: response.usage ? {
97
- promptTokens: response.usage.prompt_tokens,
98
- completionTokens: response.usage.completion_tokens,
99
- totalTokens: response.usage.total_tokens
100
- } : void 0
101
- };
102
- } catch (error) {
103
- if (error instanceof import_openai.default.APIError) {
104
- throw new Error(`OpenAI API error: ${error.message}`);
185
+ }
186
+ idsToFetch.add("ethereum");
187
+ if (idsToFetch.size > 0) {
188
+ try {
189
+ const ids = Array.from(idsToFetch).join(",");
190
+ const response = await fetch(
191
+ `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
192
+ { signal: AbortSignal.timeout(5e3) }
193
+ );
194
+ if (response.ok) {
195
+ const data = await response.json();
196
+ for (const [cgId, priceData] of Object.entries(data)) {
197
+ for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
198
+ if (id === cgId) {
199
+ const key = addr.toLowerCase();
200
+ prices[key] = priceData.usd;
201
+ if (priceData.usd_24h_vol !== void 0) {
202
+ this.cachedVolume24h[key] = priceData.usd_24h_vol;
203
+ }
204
+ if (priceData.usd_24h_change !== void 0) {
205
+ this.cachedPriceChange24h[key] = priceData.usd_24h_change;
206
+ }
207
+ }
208
+ }
209
+ }
210
+ this.lastPriceFetchAt = Date.now();
211
+ } else {
212
+ console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
213
+ }
214
+ } catch (error) {
215
+ console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
105
216
  }
106
- throw error;
107
217
  }
108
- }
109
- };
110
-
111
- // src/llm/anthropic.ts
112
- var AnthropicAdapter = class extends BaseLLMAdapter {
113
- apiKey;
114
- baseUrl;
115
- constructor(config) {
116
- super(config);
117
- if (!config.apiKey) {
118
- throw new Error("Anthropic API key required");
218
+ for (const addr of tokenAddresses) {
219
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
220
+ if (cgId && STABLECOIN_IDS.has(cgId)) {
221
+ prices[addr.toLowerCase()] = 1;
222
+ }
119
223
  }
120
- this.apiKey = config.apiKey;
121
- this.baseUrl = config.endpoint || "https://api.anthropic.com";
122
- }
123
- async chat(messages) {
124
- const systemMessage = messages.find((m) => m.role === "system");
125
- const chatMessages = messages.filter((m) => m.role !== "system");
126
- const body = {
127
- model: this.config.model || "claude-opus-4-5-20251101",
128
- max_tokens: this.config.maxTokens || 4096,
129
- temperature: this.config.temperature,
130
- system: systemMessage?.content,
131
- messages: chatMessages.map((m) => ({
132
- role: m.role,
224
+ const missingAddrs = tokenAddresses.filter(
225
+ (addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
226
+ );
227
+ if (missingAddrs.length > 0) {
228
+ try {
229
+ const coins = missingAddrs.map((a) => `base:${a}`).join(",");
230
+ const llamaResponse = await fetch(
231
+ `https://coins.llama.fi/prices/current/${coins}`,
232
+ { signal: AbortSignal.timeout(5e3) }
233
+ );
234
+ if (llamaResponse.ok) {
235
+ const llamaData = await llamaResponse.json();
236
+ for (const [key, data] of Object.entries(llamaData.coins)) {
237
+ const addr = key.replace("base:", "").toLowerCase();
238
+ if (data.price && data.confidence > 0.5) {
239
+ prices[addr] = data.price;
240
+ }
241
+ }
242
+ if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
243
+ }
244
+ } catch {
245
+ }
246
+ }
247
+ if (Object.keys(prices).length > 0) {
248
+ this.cachedPrices = prices;
249
+ }
250
+ if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
251
+ console.warn("Using cached prices (last successful fetch was stale)");
252
+ return { ...this.cachedPrices };
253
+ }
254
+ for (const addr of tokenAddresses) {
255
+ if (!prices[addr.toLowerCase()]) {
256
+ console.warn(`No price available for ${addr}, using 0`);
257
+ prices[addr.toLowerCase()] = 0;
258
+ }
259
+ }
260
+ return prices;
261
+ }
262
+ /**
263
+ * Fetch real on-chain balances: native ETH + ERC-20 tokens
264
+ */
265
+ async fetchBalances(walletAddress, tokenAddresses) {
266
+ const balances = {};
267
+ const wallet = walletAddress;
268
+ try {
269
+ const nativeBalance = await this.client.getBalance({ address: wallet });
270
+ balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
271
+ const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
272
+ try {
273
+ const balance = await this.client.readContract({
274
+ address: tokenAddress,
275
+ abi: import_viem.erc20Abi,
276
+ functionName: "balanceOf",
277
+ args: [wallet]
278
+ });
279
+ return { address: tokenAddress.toLowerCase(), balance };
280
+ } catch (error) {
281
+ return { address: tokenAddress.toLowerCase(), balance: 0n };
282
+ }
283
+ });
284
+ const results = await Promise.all(erc20Promises);
285
+ for (const { address, balance } of results) {
286
+ balances[address] = balance;
287
+ }
288
+ } catch (error) {
289
+ console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
290
+ balances[NATIVE_ETH.toLowerCase()] = 0n;
291
+ for (const address of tokenAddresses) {
292
+ balances[address.toLowerCase()] = 0n;
293
+ }
294
+ }
295
+ return balances;
296
+ }
297
+ /**
298
+ * Calculate total portfolio value in USD
299
+ */
300
+ calculatePortfolioValue(balances, prices) {
301
+ let total = 0;
302
+ for (const [address, balance] of Object.entries(balances)) {
303
+ const price = prices[address.toLowerCase()] || 0;
304
+ const decimals = getTokenDecimals(address);
305
+ const amount = Number(balance) / Math.pow(10, decimals);
306
+ total += amount * price;
307
+ }
308
+ return total;
309
+ }
310
+ };
311
+
312
+ // src/position-tracker.ts
313
+ var BASE_ASSETS = /* @__PURE__ */ new Set([
314
+ NATIVE_ETH.toLowerCase(),
315
+ // Native ETH
316
+ "0x4200000000000000000000000000000000000006",
317
+ // WETH
318
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
319
+ // USDC
320
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca",
321
+ // USDbC
322
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb",
323
+ // DAI
324
+ "0xfde4c96c8593536e31f229ea8f37b2ada2699bb2",
325
+ // USDT
326
+ "0x60a3e35cc302bfa44cb36dc100b2587cd09b9c83"
327
+ // EURC
328
+ ]);
329
+ var TOKEN_SYMBOLS = {
330
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": "AERO",
331
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": "BRETT",
332
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "DEGEN",
333
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "VIRTUAL",
334
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "TOSHI",
335
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "cbBTC",
336
+ "0x2416092f143378750bb29b79ed961ab195cceea5": "ezETH",
337
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wstETH",
338
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "cbETH",
339
+ "0x13403fb738c97cf7564f279288468c140aaed05c": "EXA"
340
+ };
341
+ var KEY_POSITIONS = "__positions";
342
+ var KEY_TRADE_HISTORY = "__trade_history";
343
+ var KEY_RISK_STATE = "__risk_state";
344
+ var PositionTracker = class {
345
+ store;
346
+ positions;
347
+ tradeHistory;
348
+ maxTradeHistory;
349
+ constructor(store, options) {
350
+ this.store = store;
351
+ this.maxTradeHistory = options?.maxTradeHistory ?? 50;
352
+ this.positions = store.get(KEY_POSITIONS) || {};
353
+ this.tradeHistory = store.get(KEY_TRADE_HISTORY) || [];
354
+ const posCount = Object.keys(this.positions).length;
355
+ if (posCount > 0 || this.tradeHistory.length > 0) {
356
+ console.log(`Position tracker loaded: ${posCount} positions, ${this.tradeHistory.length} trade records`);
357
+ }
358
+ }
359
+ // ============================================================
360
+ // TRADE RECORDING (called by runtime after execution)
361
+ // ============================================================
362
+ /**
363
+ * Record a trade result. On buy: creates/updates position with cost-basis
364
+ * weighted average. On sell: calculates realized PnL and removes if fully sold.
365
+ */
366
+ recordTrade(params) {
367
+ const { action, tokenIn, tokenOut, amountIn, priceIn, priceOut, txHash, reasoning, success } = params;
368
+ const decimalsIn = getTokenDecimals(tokenIn);
369
+ const amountInUnits = Number(amountIn) / Math.pow(10, decimalsIn);
370
+ const tradeValueUSD = amountInUnits * priceIn;
371
+ const record = {
372
+ timestamp: Date.now(),
373
+ action,
374
+ tokenIn: tokenIn.toLowerCase(),
375
+ tokenOut: tokenOut.toLowerCase(),
376
+ amountIn: amountIn.toString(),
377
+ priceUSD: tradeValueUSD,
378
+ txHash,
379
+ reasoning,
380
+ success
381
+ };
382
+ if (success) {
383
+ if (action === "buy") {
384
+ this.handleBuy(tokenOut.toLowerCase(), tradeValueUSD, priceOut, txHash);
385
+ } else if (action === "sell") {
386
+ const realizedPnL = this.handleSell(tokenIn.toLowerCase(), tradeValueUSD);
387
+ record.realizedPnL = realizedPnL;
388
+ }
389
+ }
390
+ this.tradeHistory.unshift(record);
391
+ if (this.tradeHistory.length > this.maxTradeHistory) {
392
+ this.tradeHistory = this.tradeHistory.slice(0, this.maxTradeHistory);
393
+ }
394
+ this.persist();
395
+ }
396
+ /**
397
+ * Handle a buy: create or update position with cost-basis weighted average.
398
+ */
399
+ handleBuy(token, costUSD, priceUSD, txHash) {
400
+ if (BASE_ASSETS.has(token) || priceUSD <= 0 || costUSD <= 0) return;
401
+ const existing = this.positions[token];
402
+ const acquiredAmount = costUSD / priceUSD;
403
+ if (existing) {
404
+ const newTotalCost = existing.totalCostBasis + costUSD;
405
+ const newTotalAmount = existing.totalAmountAcquired + acquiredAmount;
406
+ existing.averageEntryPrice = newTotalAmount > 0 ? newTotalCost / newTotalAmount : priceUSD;
407
+ existing.totalCostBasis = newTotalCost;
408
+ existing.totalAmountAcquired = newTotalAmount;
409
+ existing.lastUpdateTimestamp = Date.now();
410
+ if (txHash) {
411
+ existing.txHashes.push(txHash);
412
+ if (existing.txHashes.length > 10) existing.txHashes.shift();
413
+ }
414
+ } else {
415
+ this.positions[token] = {
416
+ token,
417
+ symbol: TOKEN_SYMBOLS[token],
418
+ entryPrice: priceUSD,
419
+ averageEntryPrice: priceUSD,
420
+ totalCostBasis: costUSD,
421
+ totalAmountAcquired: acquiredAmount,
422
+ currentAmount: acquiredAmount,
423
+ entryTimestamp: Date.now(),
424
+ lastUpdateTimestamp: Date.now(),
425
+ txHashes: txHash ? [txHash] : []
426
+ };
427
+ }
428
+ }
429
+ /**
430
+ * Handle a sell: calculate realized PnL and remove position if fully sold.
431
+ * Returns the realized PnL in USD.
432
+ */
433
+ handleSell(token, saleValueUSD) {
434
+ if (BASE_ASSETS.has(token)) return 0;
435
+ const position = this.positions[token];
436
+ if (!position || position.averageEntryPrice <= 0) return 0;
437
+ const estimatedUnitsSold = position.currentAmount > 0 ? Math.min(position.currentAmount, saleValueUSD / position.averageEntryPrice) : saleValueUSD / position.averageEntryPrice;
438
+ const costBasisOfSold = estimatedUnitsSold * position.averageEntryPrice;
439
+ const realizedPnL = saleValueUSD - costBasisOfSold;
440
+ position.totalAmountAcquired = Math.max(0, position.totalAmountAcquired - estimatedUnitsSold);
441
+ position.totalCostBasis = Math.max(0, position.totalCostBasis - costBasisOfSold);
442
+ position.lastUpdateTimestamp = Date.now();
443
+ return realizedPnL;
444
+ }
445
+ // ============================================================
446
+ // BALANCE SYNC (called by runtime each cycle)
447
+ // ============================================================
448
+ /**
449
+ * Sync tracked positions with on-chain balances.
450
+ * Updates currentAmount, detects new tokens (airdrops), removes zeroed positions.
451
+ */
452
+ syncBalances(balances, prices) {
453
+ let changed = false;
454
+ for (const [address, balance] of Object.entries(balances)) {
455
+ const token = address.toLowerCase();
456
+ if (BASE_ASSETS.has(token)) continue;
457
+ const decimals = getTokenDecimals(token);
458
+ const amount = Number(balance) / Math.pow(10, decimals);
459
+ if (amount > 0) {
460
+ if (this.positions[token]) {
461
+ if (this.positions[token].currentAmount !== amount) {
462
+ this.positions[token].currentAmount = amount;
463
+ this.positions[token].lastUpdateTimestamp = Date.now();
464
+ changed = true;
465
+ }
466
+ } else {
467
+ const price = prices[token] || 0;
468
+ this.positions[token] = {
469
+ token,
470
+ symbol: TOKEN_SYMBOLS[token],
471
+ entryPrice: price,
472
+ averageEntryPrice: price,
473
+ totalCostBasis: amount * price,
474
+ totalAmountAcquired: amount,
475
+ currentAmount: amount,
476
+ entryTimestamp: Date.now(),
477
+ lastUpdateTimestamp: Date.now(),
478
+ txHashes: []
479
+ };
480
+ if (price > 0) {
481
+ console.log(`Position tracker: detected new holding ${TOKEN_SYMBOLS[token] || token.slice(0, 10)} (${amount.toFixed(4)} units @ $${price.toFixed(4)})`);
482
+ }
483
+ changed = true;
484
+ }
485
+ } else if (this.positions[token]) {
486
+ delete this.positions[token];
487
+ changed = true;
488
+ }
489
+ }
490
+ if (changed) {
491
+ this.persist();
492
+ }
493
+ }
494
+ // ============================================================
495
+ // QUERY METHODS (for strategies)
496
+ // ============================================================
497
+ /** Get all tracked positions */
498
+ getPositions() {
499
+ return Object.values(this.positions);
500
+ }
501
+ /** Get a single position by token address */
502
+ getPosition(token) {
503
+ return this.positions[token.toLowerCase()];
504
+ }
505
+ /** Get trade history (newest first) */
506
+ getTradeHistory(limit) {
507
+ return limit ? this.tradeHistory.slice(0, limit) : [...this.tradeHistory];
508
+ }
509
+ /** Get unrealized PnL per position given current prices */
510
+ getUnrealizedPnL(prices) {
511
+ const pnl = {};
512
+ for (const pos of Object.values(this.positions)) {
513
+ const currentPrice = prices[pos.token] || 0;
514
+ if (currentPrice > 0 && pos.averageEntryPrice > 0 && pos.currentAmount > 0) {
515
+ const currentValue = pos.currentAmount * currentPrice;
516
+ const costBasis = pos.currentAmount * pos.averageEntryPrice;
517
+ pnl[pos.token] = currentValue - costBasis;
518
+ }
519
+ }
520
+ return pnl;
521
+ }
522
+ /** Get total unrealized PnL across all positions */
523
+ getTotalUnrealizedPnL(prices) {
524
+ const pnl = this.getUnrealizedPnL(prices);
525
+ return Object.values(pnl).reduce((sum, v) => sum + v, 0);
526
+ }
527
+ // ============================================================
528
+ // RISK STATE PERSISTENCE
529
+ // ============================================================
530
+ /** Load persisted risk state */
531
+ getRiskState() {
532
+ return this.store.get(KEY_RISK_STATE) || {
533
+ dailyPnL: 0,
534
+ dailyFees: 0,
535
+ lastResetDate: ""
536
+ };
537
+ }
538
+ /** Save risk state to persistent store */
539
+ saveRiskState(state) {
540
+ this.store.set(KEY_RISK_STATE, state);
541
+ }
542
+ // ============================================================
543
+ // RELAY SUMMARY
544
+ // ============================================================
545
+ /** Get a compact summary for relay heartbeats */
546
+ getPositionSummary(prices) {
547
+ const unrealizedPnL = this.getUnrealizedPnL(prices);
548
+ const now = Date.now();
549
+ const topPositions = Object.values(this.positions).map((pos) => ({
550
+ token: pos.token,
551
+ symbol: pos.symbol,
552
+ unrealizedPnL: unrealizedPnL[pos.token] || 0,
553
+ holdingDuration: now - pos.entryTimestamp
554
+ })).sort((a, b) => Math.abs(b.unrealizedPnL) - Math.abs(a.unrealizedPnL)).slice(0, 5);
555
+ const oneDayAgo = now - 24 * 60 * 60 * 1e3;
556
+ const recentTrades = this.tradeHistory.filter((t) => t.timestamp > oneDayAgo).length;
557
+ const totalRealizedPnL = this.tradeHistory.filter((t) => t.realizedPnL !== void 0).reduce((sum, t) => sum + (t.realizedPnL || 0), 0);
558
+ return {
559
+ openPositions: Object.keys(this.positions).length,
560
+ totalUnrealizedPnL: Object.values(unrealizedPnL).reduce((s, v) => s + v, 0),
561
+ topPositions,
562
+ recentTrades,
563
+ totalRealizedPnL
564
+ };
565
+ }
566
+ // ============================================================
567
+ // INTERNAL
568
+ // ============================================================
569
+ persist() {
570
+ this.store.set(KEY_POSITIONS, this.positions);
571
+ this.store.set(KEY_TRADE_HISTORY, this.tradeHistory);
572
+ }
573
+ };
574
+
575
+ // src/llm/openai.ts
576
+ var import_openai = __toESM(require("openai"));
577
+
578
+ // src/llm/base.ts
579
+ var BaseLLMAdapter = class {
580
+ config;
581
+ constructor(config) {
582
+ this.config = config;
583
+ }
584
+ getMetadata() {
585
+ return {
586
+ provider: this.config.provider,
587
+ model: this.config.model || "unknown",
588
+ isLocal: this.config.provider === "ollama"
589
+ };
590
+ }
591
+ /**
592
+ * Format model name for display
593
+ */
594
+ getDisplayModel() {
595
+ if (this.config.provider === "ollama") {
596
+ return `Local (${this.config.model || "ollama"})`;
597
+ }
598
+ return this.config.model || this.config.provider;
599
+ }
600
+ };
601
+
602
+ // src/llm/openai.ts
603
+ var OpenAIAdapter = class extends BaseLLMAdapter {
604
+ client;
605
+ constructor(config) {
606
+ super(config);
607
+ if (!config.apiKey && !config.endpoint) {
608
+ throw new Error("OpenAI API key or custom endpoint required");
609
+ }
610
+ this.client = new import_openai.default({
611
+ apiKey: config.apiKey || "not-needed-for-custom",
612
+ baseURL: config.endpoint
613
+ });
614
+ }
615
+ async chat(messages) {
616
+ try {
617
+ const response = await this.client.chat.completions.create({
618
+ model: this.config.model || "gpt-4.1",
619
+ messages: messages.map((m) => ({
620
+ role: m.role,
621
+ content: m.content
622
+ })),
623
+ temperature: this.config.temperature,
624
+ max_tokens: this.config.maxTokens
625
+ });
626
+ const choice = response.choices[0];
627
+ if (!choice || !choice.message) {
628
+ throw new Error("No response from OpenAI");
629
+ }
630
+ return {
631
+ content: choice.message.content || "",
632
+ usage: response.usage ? {
633
+ promptTokens: response.usage.prompt_tokens,
634
+ completionTokens: response.usage.completion_tokens,
635
+ totalTokens: response.usage.total_tokens
636
+ } : void 0
637
+ };
638
+ } catch (error) {
639
+ if (error instanceof import_openai.default.APIError) {
640
+ throw new Error(`OpenAI API error: ${error.message}`);
641
+ }
642
+ throw error;
643
+ }
644
+ }
645
+ };
646
+
647
+ // src/llm/anthropic.ts
648
+ var AnthropicAdapter = class extends BaseLLMAdapter {
649
+ apiKey;
650
+ baseUrl;
651
+ constructor(config) {
652
+ super(config);
653
+ if (!config.apiKey) {
654
+ throw new Error("Anthropic API key required");
655
+ }
656
+ this.apiKey = config.apiKey;
657
+ this.baseUrl = config.endpoint || "https://api.anthropic.com";
658
+ }
659
+ async chat(messages) {
660
+ const systemMessage = messages.find((m) => m.role === "system");
661
+ const chatMessages = messages.filter((m) => m.role !== "system");
662
+ const body = {
663
+ model: this.config.model || "claude-opus-4-5-20251101",
664
+ max_tokens: this.config.maxTokens || 4096,
665
+ temperature: this.config.temperature,
666
+ system: systemMessage?.content,
667
+ messages: chatMessages.map((m) => ({
668
+ role: m.role,
133
669
  content: m.content
134
670
  }))
135
671
  };
@@ -1048,8 +1584,9 @@ var TradeExecutor = class {
1048
1584
  return { success: true, txHash: result.hash };
1049
1585
  } catch (error) {
1050
1586
  const message = error instanceof Error ? error.message : "Unknown error";
1051
- console.error(`Trade failed: ${message}`);
1052
- return { success: false, error: message };
1587
+ const classified = classifyTradeError(message);
1588
+ console.error(`Trade failed (${classified.category}): ${classified.userMessage}`);
1589
+ return { success: false, error: classified.userMessage };
1053
1590
  }
1054
1591
  }
1055
1592
  /**
@@ -1093,335 +1630,122 @@ var TradeExecutor = class {
1093
1630
  return new Promise((resolve) => setTimeout(resolve, ms));
1094
1631
  }
1095
1632
  };
1096
-
1097
- // src/trading/market.ts
1098
- var import_viem = require("viem");
1099
- var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
1100
- var TOKEN_DECIMALS = {
1101
- // Base Mainnet Core tokens
1102
- "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
1103
- // USDC
1104
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
1105
- // USDbC
1106
- "0x4200000000000000000000000000000000000006": 18,
1107
- // WETH
1108
- "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
1109
- // DAI
1110
- "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
1111
- // cbETH
1112
- [NATIVE_ETH.toLowerCase()]: 18,
1113
- // Native ETH
1114
- // Base Mainnet — Established tokens
1115
- "0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
1116
- // AERO (Aerodrome)
1117
- "0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
1118
- // BRETT
1119
- "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
1120
- // DEGEN
1121
- "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
1122
- // VIRTUAL
1123
- "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
1124
- // TOSHI
1125
- "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
1126
- // cbBTC
1127
- "0x2416092f143378750bb29b79ed961ab195cceea5": 18,
1128
- // ezETH (Renzo)
1129
- "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
1130
- // wstETH (Lido)
1131
- };
1132
- function getTokenDecimals(address) {
1133
- const decimals = TOKEN_DECIMALS[address.toLowerCase()];
1134
- if (decimals === void 0) {
1135
- console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
1136
- return 18;
1633
+ function classifyTradeError(message) {
1634
+ const lower = message.toLowerCase();
1635
+ if (lower.includes("configmismatch") || lower.includes("config mismatch")) {
1636
+ return {
1637
+ category: "config_mismatch",
1638
+ userMessage: "Config hash mismatch \u2014 on-chain config does not match. Restart the agent to re-sync."
1639
+ };
1137
1640
  }
1138
- return decimals;
1139
- }
1140
- var TOKEN_TO_COINGECKO = {
1141
- // Core
1142
- "0x4200000000000000000000000000000000000006": "ethereum",
1143
- // WETH
1144
- [NATIVE_ETH.toLowerCase()]: "ethereum",
1145
- "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
1146
- // USDC
1147
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
1148
- // USDbC
1149
- "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
1150
- // cbETH
1151
- "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
1152
- // DAI
1153
- // Established
1154
- "0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
1155
- // AERO
1156
- "0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
1157
- // BRETT
1158
- "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
1159
- // DEGEN
1160
- "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
1161
- // VIRTUAL
1162
- "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
1163
- // TOSHI
1164
- "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
1165
- // cbBTC
1166
- "0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
1167
- // ezETH
1168
- "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
1169
- // wstETH
1170
- };
1171
- var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
1172
- var PRICE_STALENESS_MS = 6e4;
1173
- var MarketDataService = class {
1174
- rpcUrl;
1175
- client;
1176
- /** Cached prices from last fetch */
1177
- cachedPrices = {};
1178
- /** Timestamp of last successful price fetch */
1179
- lastPriceFetchAt = 0;
1180
- constructor(rpcUrl) {
1181
- this.rpcUrl = rpcUrl;
1182
- this.client = (0, import_viem.createPublicClient)({
1183
- transport: (0, import_viem.http)(rpcUrl)
1184
- });
1641
+ if (lower.includes("insufficient funds") || lower.includes("exceeds the balance")) {
1642
+ return {
1643
+ category: "insufficient_funds",
1644
+ userMessage: "Insufficient ETH for gas \u2014 fund your trading wallet."
1645
+ };
1185
1646
  }
1186
- /** Cached volume data */
1187
- cachedVolume24h = {};
1188
- /** Cached price change data */
1189
- cachedPriceChange24h = {};
1190
- /**
1191
- * Fetch current market data for the agent
1192
- */
1193
- async fetchMarketData(walletAddress, tokenAddresses) {
1194
- const prices = await this.fetchPrices(tokenAddresses);
1195
- const balances = await this.fetchBalances(walletAddress, tokenAddresses);
1196
- const portfolioValue = this.calculatePortfolioValue(balances, prices);
1197
- let gasPrice;
1198
- try {
1199
- gasPrice = await this.client.getGasPrice();
1200
- } catch {
1201
- }
1647
+ if (lower.includes("out of gas") || lower.includes("intrinsic gas too low")) {
1202
1648
  return {
1203
- timestamp: Date.now(),
1204
- prices,
1205
- balances,
1206
- portfolioValue,
1207
- volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
1208
- priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
1209
- gasPrice,
1210
- network: {
1211
- chainId: this.client.chain?.id ?? 8453
1212
- }
1649
+ category: "out_of_gas",
1650
+ userMessage: "Transaction ran out of gas during execution. This is usually transient \u2014 retrying with more gas."
1213
1651
  };
1214
1652
  }
1653
+ if (lower.includes("minamountnotmet") || lower.includes("min amount") || lower.includes("slippage")) {
1654
+ return {
1655
+ category: "slippage",
1656
+ userMessage: "Swap failed due to slippage \u2014 price moved beyond tolerance. Will retry next cycle."
1657
+ };
1658
+ }
1659
+ if (lower.includes("notauthorizedforagent") || lower.includes("not authorized")) {
1660
+ return {
1661
+ category: "not_authorized",
1662
+ userMessage: "Wallet not authorized for this agent. Ensure your wallet is linked via the registry."
1663
+ };
1664
+ }
1665
+ if (lower.includes("aggregatornotwhitelisted")) {
1666
+ return {
1667
+ category: "aggregator_error",
1668
+ userMessage: "DEX aggregator not whitelisted on the router. Contact support."
1669
+ };
1670
+ }
1671
+ if (lower.includes("reverted") || lower.includes("execution reverted")) {
1672
+ return {
1673
+ category: "reverted",
1674
+ userMessage: `Transaction reverted: ${message.slice(0, 200)}`
1675
+ };
1676
+ }
1677
+ if (lower.includes("timeout") || lower.includes("econnrefused") || lower.includes("network")) {
1678
+ return {
1679
+ category: "network",
1680
+ userMessage: "Network error \u2014 RPC node may be down. Will retry next cycle."
1681
+ };
1682
+ }
1683
+ return {
1684
+ category: "unknown",
1685
+ userMessage: message.slice(0, 300)
1686
+ };
1687
+ }
1688
+
1689
+ // src/trading/risk.ts
1690
+ var RiskManager = class {
1691
+ config;
1692
+ dailyPnL = 0;
1693
+ dailyFees = 0;
1694
+ lastResetDate = "";
1695
+ /** Minimum trade value in USD — trades below this are rejected as dust */
1696
+ minTradeValueUSD;
1697
+ constructor(config) {
1698
+ this.config = config;
1699
+ this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
1700
+ }
1215
1701
  /**
1216
- * Check if cached prices are still fresh
1702
+ * Filter signals through risk checks
1703
+ * Returns only signals that pass all guardrails
1217
1704
  */
1218
- get pricesAreFresh() {
1219
- return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
1705
+ filterSignals(signals, marketData) {
1706
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1707
+ if (today !== this.lastResetDate) {
1708
+ this.dailyPnL = 0;
1709
+ this.dailyFees = 0;
1710
+ this.lastResetDate = today;
1711
+ }
1712
+ if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
1713
+ console.warn("Daily loss limit reached - no new trades");
1714
+ return [];
1715
+ }
1716
+ return signals.filter((signal) => this.validateSignal(signal, marketData));
1220
1717
  }
1221
1718
  /**
1222
- * Fetch token prices from CoinGecko free API
1223
- * Returns cached prices if still fresh (<60s old)
1719
+ * Validate individual signal against risk limits.
1720
+ * Sell signals are exempt from position size and minimum value checks —
1721
+ * those guardrails prevent oversized/dust buys, but blocking exits traps capital.
1224
1722
  */
1225
- async fetchPrices(tokenAddresses) {
1226
- if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
1227
- const prices2 = { ...this.cachedPrices };
1228
- for (const addr of tokenAddresses) {
1229
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1230
- if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
1231
- prices2[addr.toLowerCase()] = 1;
1232
- }
1233
- }
1234
- return prices2;
1723
+ validateSignal(signal, marketData) {
1724
+ if (signal.action === "hold") {
1725
+ return true;
1235
1726
  }
1236
- const prices = {};
1237
- const idsToFetch = /* @__PURE__ */ new Set();
1238
- for (const addr of tokenAddresses) {
1239
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1240
- if (cgId && !STABLECOIN_IDS.has(cgId)) {
1241
- idsToFetch.add(cgId);
1242
- }
1727
+ if (signal.confidence < 0.5) {
1728
+ console.warn(`Signal confidence too low: ${signal.confidence}`);
1729
+ return false;
1243
1730
  }
1244
- idsToFetch.add("ethereum");
1245
- if (idsToFetch.size > 0) {
1246
- try {
1247
- const ids = Array.from(idsToFetch).join(",");
1248
- const response = await fetch(
1249
- `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
1250
- { signal: AbortSignal.timeout(5e3) }
1251
- );
1252
- if (response.ok) {
1253
- const data = await response.json();
1254
- for (const [cgId, priceData] of Object.entries(data)) {
1255
- for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
1256
- if (id === cgId) {
1257
- const key = addr.toLowerCase();
1258
- prices[key] = priceData.usd;
1259
- if (priceData.usd_24h_vol !== void 0) {
1260
- this.cachedVolume24h[key] = priceData.usd_24h_vol;
1261
- }
1262
- if (priceData.usd_24h_change !== void 0) {
1263
- this.cachedPriceChange24h[key] = priceData.usd_24h_change;
1264
- }
1265
- }
1266
- }
1267
- }
1268
- this.lastPriceFetchAt = Date.now();
1269
- } else {
1270
- console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
1271
- }
1272
- } catch (error) {
1273
- console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
1274
- }
1731
+ if (signal.action === "sell") {
1732
+ return true;
1275
1733
  }
1276
- for (const addr of tokenAddresses) {
1277
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1278
- if (cgId && STABLECOIN_IDS.has(cgId)) {
1279
- prices[addr.toLowerCase()] = 1;
1280
- }
1734
+ const signalValue = this.estimateSignalValue(signal, marketData);
1735
+ const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
1736
+ if (signalValue > maxPositionValue) {
1737
+ console.warn(
1738
+ `Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
1739
+ );
1740
+ return false;
1281
1741
  }
1282
- const missingAddrs = tokenAddresses.filter(
1283
- (addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
1284
- );
1285
- if (missingAddrs.length > 0) {
1286
- try {
1287
- const coins = missingAddrs.map((a) => `base:${a}`).join(",");
1288
- const llamaResponse = await fetch(
1289
- `https://coins.llama.fi/prices/current/${coins}`,
1290
- { signal: AbortSignal.timeout(5e3) }
1742
+ if (this.config.maxConcurrentPositions) {
1743
+ const activePositions = this.countActivePositions(marketData);
1744
+ if (activePositions >= this.config.maxConcurrentPositions) {
1745
+ console.warn(
1746
+ `Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
1291
1747
  );
1292
- if (llamaResponse.ok) {
1293
- const llamaData = await llamaResponse.json();
1294
- for (const [key, data] of Object.entries(llamaData.coins)) {
1295
- const addr = key.replace("base:", "").toLowerCase();
1296
- if (data.price && data.confidence > 0.5) {
1297
- prices[addr] = data.price;
1298
- }
1299
- }
1300
- if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
1301
- }
1302
- } catch {
1303
- }
1304
- }
1305
- if (Object.keys(prices).length > 0) {
1306
- this.cachedPrices = prices;
1307
- }
1308
- if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
1309
- console.warn("Using cached prices (last successful fetch was stale)");
1310
- return { ...this.cachedPrices };
1311
- }
1312
- for (const addr of tokenAddresses) {
1313
- if (!prices[addr.toLowerCase()]) {
1314
- console.warn(`No price available for ${addr}, using 0`);
1315
- prices[addr.toLowerCase()] = 0;
1316
- }
1317
- }
1318
- return prices;
1319
- }
1320
- /**
1321
- * Fetch real on-chain balances: native ETH + ERC-20 tokens
1322
- */
1323
- async fetchBalances(walletAddress, tokenAddresses) {
1324
- const balances = {};
1325
- const wallet = walletAddress;
1326
- try {
1327
- const nativeBalance = await this.client.getBalance({ address: wallet });
1328
- balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
1329
- const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
1330
- try {
1331
- const balance = await this.client.readContract({
1332
- address: tokenAddress,
1333
- abi: import_viem.erc20Abi,
1334
- functionName: "balanceOf",
1335
- args: [wallet]
1336
- });
1337
- return { address: tokenAddress.toLowerCase(), balance };
1338
- } catch (error) {
1339
- return { address: tokenAddress.toLowerCase(), balance: 0n };
1340
- }
1341
- });
1342
- const results = await Promise.all(erc20Promises);
1343
- for (const { address, balance } of results) {
1344
- balances[address] = balance;
1345
- }
1346
- } catch (error) {
1347
- console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
1348
- balances[NATIVE_ETH.toLowerCase()] = 0n;
1349
- for (const address of tokenAddresses) {
1350
- balances[address.toLowerCase()] = 0n;
1351
- }
1352
- }
1353
- return balances;
1354
- }
1355
- /**
1356
- * Calculate total portfolio value in USD
1357
- */
1358
- calculatePortfolioValue(balances, prices) {
1359
- let total = 0;
1360
- for (const [address, balance] of Object.entries(balances)) {
1361
- const price = prices[address.toLowerCase()] || 0;
1362
- const decimals = getTokenDecimals(address);
1363
- const amount = Number(balance) / Math.pow(10, decimals);
1364
- total += amount * price;
1365
- }
1366
- return total;
1367
- }
1368
- };
1369
-
1370
- // src/trading/risk.ts
1371
- var RiskManager = class {
1372
- config;
1373
- dailyPnL = 0;
1374
- dailyFees = 0;
1375
- lastResetDate = "";
1376
- /** Minimum trade value in USD — trades below this are rejected as dust */
1377
- minTradeValueUSD;
1378
- constructor(config) {
1379
- this.config = config;
1380
- this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
1381
- }
1382
- /**
1383
- * Filter signals through risk checks
1384
- * Returns only signals that pass all guardrails
1385
- */
1386
- filterSignals(signals, marketData) {
1387
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1388
- if (today !== this.lastResetDate) {
1389
- this.dailyPnL = 0;
1390
- this.dailyFees = 0;
1391
- this.lastResetDate = today;
1392
- }
1393
- if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
1394
- console.warn("Daily loss limit reached - no new trades");
1395
- return [];
1396
- }
1397
- return signals.filter((signal) => this.validateSignal(signal, marketData));
1398
- }
1399
- /**
1400
- * Validate individual signal against risk limits
1401
- */
1402
- validateSignal(signal, marketData) {
1403
- if (signal.action === "hold") {
1404
- return true;
1405
- }
1406
- const signalValue = this.estimateSignalValue(signal, marketData);
1407
- const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
1408
- if (signalValue > maxPositionValue) {
1409
- console.warn(
1410
- `Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
1411
- );
1412
- return false;
1413
- }
1414
- if (signal.confidence < 0.5) {
1415
- console.warn(`Signal confidence too low: ${signal.confidence}`);
1416
- return false;
1417
- }
1418
- if (signal.action === "buy" && this.config.maxConcurrentPositions) {
1419
- const activePositions = this.countActivePositions(marketData);
1420
- if (activePositions >= this.config.maxConcurrentPositions) {
1421
- console.warn(
1422
- `Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
1423
- );
1424
- return false;
1748
+ return false;
1425
1749
  }
1426
1750
  }
1427
1751
  if (signalValue < this.minTradeValueUSD) {
@@ -1472,6 +1796,31 @@ var RiskManager = class {
1472
1796
  updateFees(fees) {
1473
1797
  this.dailyFees += fees;
1474
1798
  }
1799
+ /**
1800
+ * Export current risk state for persistence across restarts.
1801
+ */
1802
+ exportState() {
1803
+ return {
1804
+ dailyPnL: this.dailyPnL,
1805
+ dailyFees: this.dailyFees,
1806
+ lastResetDate: this.lastResetDate
1807
+ };
1808
+ }
1809
+ /**
1810
+ * Restore risk state from persistence (called on startup).
1811
+ * Only restores if the saved state is from today — expired state is ignored.
1812
+ */
1813
+ restoreState(state) {
1814
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1815
+ if (state.lastResetDate === today) {
1816
+ this.dailyPnL = state.dailyPnL;
1817
+ this.dailyFees = state.dailyFees;
1818
+ this.lastResetDate = state.lastResetDate;
1819
+ console.log(`Risk state restored: PnL=$${this.dailyPnL.toFixed(2)}, Fees=$${this.dailyFees.toFixed(2)}`);
1820
+ } else {
1821
+ console.log("Risk state expired (different day), starting fresh");
1822
+ }
1823
+ }
1475
1824
  /**
1476
1825
  * Get current risk status
1477
1826
  * @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
@@ -1585,6 +1934,55 @@ var RiskManager = class {
1585
1934
  }
1586
1935
  };
1587
1936
 
1937
+ // src/store.ts
1938
+ var import_fs3 = require("fs");
1939
+ var import_path3 = require("path");
1940
+ var FileStore = class {
1941
+ data = {};
1942
+ filePath;
1943
+ dirty = false;
1944
+ constructor(dataDir) {
1945
+ const dir = dataDir || (0, import_path3.join)(process.cwd(), "data");
1946
+ this.filePath = (0, import_path3.join)(dir, "strategy-store.json");
1947
+ if (!(0, import_fs3.existsSync)(dir)) {
1948
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
1949
+ }
1950
+ if ((0, import_fs3.existsSync)(this.filePath)) {
1951
+ try {
1952
+ const raw = (0, import_fs3.readFileSync)(this.filePath, "utf-8");
1953
+ this.data = JSON.parse(raw);
1954
+ } catch {
1955
+ this.data = {};
1956
+ }
1957
+ }
1958
+ }
1959
+ get(key) {
1960
+ return this.data[key];
1961
+ }
1962
+ set(key, value) {
1963
+ this.data[key] = value;
1964
+ this.dirty = true;
1965
+ this.flush();
1966
+ }
1967
+ delete(key) {
1968
+ delete this.data[key];
1969
+ this.dirty = true;
1970
+ this.flush();
1971
+ }
1972
+ keys() {
1973
+ return Object.keys(this.data);
1974
+ }
1975
+ flush() {
1976
+ if (!this.dirty) return;
1977
+ try {
1978
+ (0, import_fs3.writeFileSync)(this.filePath, JSON.stringify(this.data, null, 2), "utf-8");
1979
+ this.dirty = false;
1980
+ } catch (error) {
1981
+ console.warn("Failed to persist strategy store:", error instanceof Error ? error.message : error);
1982
+ }
1983
+ }
1984
+ };
1985
+
1588
1986
  // src/vault/manager.ts
1589
1987
  var import_viem2 = require("viem");
1590
1988
  var import_accounts = require("viem/accounts");
@@ -1884,292 +2282,29 @@ var VaultManager = class {
1884
2282
  };
1885
2283
 
1886
2284
  // src/relay.ts
1887
- var import_ws = __toESM(require("ws"));
1888
- var import_accounts2 = require("viem/accounts");
1889
- var import_sdk = require("@exagent/sdk");
1890
- var RelayClient = class {
1891
- config;
1892
- ws = null;
1893
- authenticated = false;
1894
- authRejected = false;
1895
- reconnectAttempts = 0;
1896
- maxReconnectAttempts = 50;
1897
- reconnectTimer = null;
1898
- heartbeatTimer = null;
1899
- stopped = false;
2285
+ var import_ws2 = __toESM(require("ws"));
2286
+ var import_accounts4 = require("viem/accounts");
2287
+
2288
+ // src/perp/client.ts
2289
+ var HyperliquidClient = class {
2290
+ apiUrl;
2291
+ meta = null;
2292
+ assetIndexCache = /* @__PURE__ */ new Map();
1900
2293
  constructor(config) {
1901
- this.config = config;
2294
+ this.apiUrl = config.apiUrl;
1902
2295
  }
1903
- /**
1904
- * Connect to the relay server
1905
- */
1906
- async connect() {
1907
- if (this.stopped) return;
1908
- const wsUrl = this.config.relay.apiUrl.replace(/^https?:\/\//, (m) => m.includes("https") ? "wss://" : "ws://").replace(/\/$/, "") + "/ws/agent";
1909
- return new Promise((resolve, reject) => {
1910
- try {
1911
- this.ws = new import_ws.default(wsUrl);
1912
- } catch (error) {
1913
- console.error("Relay: Failed to create WebSocket:", error);
1914
- this.scheduleReconnect();
1915
- reject(error);
1916
- return;
1917
- }
1918
- const connectTimeout = setTimeout(() => {
1919
- if (!this.authenticated) {
1920
- console.error("Relay: Connection timeout");
1921
- this.ws?.close();
1922
- this.scheduleReconnect();
1923
- reject(new Error("Connection timeout"));
1924
- }
1925
- }, 15e3);
1926
- this.ws.on("open", async () => {
1927
- this.authRejected = false;
1928
- console.log("Relay: Connected, authenticating...");
1929
- try {
1930
- await this.authenticate();
1931
- } catch (error) {
1932
- console.error("Relay: Authentication failed:", error);
1933
- this.ws?.close();
1934
- clearTimeout(connectTimeout);
1935
- reject(error);
1936
- }
1937
- });
1938
- this.ws.on("message", (raw) => {
1939
- try {
1940
- const data = JSON.parse(raw.toString());
1941
- this.handleMessage(data);
1942
- if (data.type === "auth_success") {
1943
- clearTimeout(connectTimeout);
1944
- this.authenticated = true;
1945
- this.reconnectAttempts = 0;
1946
- this.startHeartbeat();
1947
- console.log("Relay: Authenticated successfully");
1948
- resolve();
1949
- } else if (data.type === "auth_error") {
1950
- clearTimeout(connectTimeout);
1951
- this.authRejected = true;
1952
- console.error(`Relay: Auth rejected: ${data.message}`);
1953
- reject(new Error(data.message));
1954
- }
1955
- } catch {
1956
- }
1957
- });
1958
- this.ws.on("close", (code, reason) => {
1959
- clearTimeout(connectTimeout);
1960
- this.authenticated = false;
1961
- this.stopHeartbeat();
1962
- if (!this.stopped) {
1963
- if (!this.authRejected) {
1964
- console.log(`Relay: Disconnected (${code}: ${reason.toString() || "unknown"})`);
1965
- }
1966
- this.scheduleReconnect();
1967
- }
1968
- });
1969
- this.ws.on("error", (error) => {
1970
- if (!this.stopped) {
1971
- console.error("Relay: WebSocket error:", error.message);
1972
- }
1973
- });
2296
+ // ============================================================
2297
+ // INFO API (read-only)
2298
+ // ============================================================
2299
+ /** Fetch perpetuals metadata (asset specs, names, indices) */
2300
+ async getMeta() {
2301
+ if (this.meta) return this.meta;
2302
+ const resp = await this.infoRequest({ type: "meta" });
2303
+ this.meta = resp.universe;
2304
+ this.meta.forEach((asset, idx) => {
2305
+ this.assetIndexCache.set(asset.name, idx);
1974
2306
  });
1975
- }
1976
- /**
1977
- * Authenticate with the relay server using wallet signature
1978
- */
1979
- async authenticate() {
1980
- const account = (0, import_accounts2.privateKeyToAccount)(this.config.privateKey);
1981
- const timestamp = Math.floor(Date.now() / 1e3);
1982
- const message = `ExagentRelay:${this.config.agentId}:${timestamp}`;
1983
- const signature = await (0, import_accounts2.signMessage)({
1984
- message,
1985
- privateKey: this.config.privateKey
1986
- });
1987
- this.send({
1988
- type: "auth",
1989
- agentId: this.config.agentId,
1990
- wallet: account.address,
1991
- timestamp,
1992
- signature,
1993
- sdkVersion: import_sdk.SDK_VERSION
1994
- });
1995
- }
1996
- /**
1997
- * Handle incoming messages from the relay server
1998
- */
1999
- handleMessage(data) {
2000
- switch (data.type) {
2001
- case "command":
2002
- if (data.command && this.config.onCommand) {
2003
- this.config.onCommand(data.command);
2004
- }
2005
- break;
2006
- case "auth_success":
2007
- case "auth_error":
2008
- break;
2009
- case "error":
2010
- console.error(`Relay: Server error: ${data.message}`);
2011
- break;
2012
- }
2013
- }
2014
- /**
2015
- * Send a status heartbeat
2016
- */
2017
- sendHeartbeat(status) {
2018
- if (!this.authenticated) return;
2019
- this.send({
2020
- type: "heartbeat",
2021
- agentId: this.config.agentId,
2022
- status
2023
- });
2024
- }
2025
- /**
2026
- * Send a status update (outside of regular heartbeat)
2027
- */
2028
- sendStatusUpdate(status) {
2029
- if (!this.authenticated) return;
2030
- this.send({
2031
- type: "status_update",
2032
- agentId: this.config.agentId,
2033
- status
2034
- });
2035
- }
2036
- /**
2037
- * Send a message to the command center
2038
- */
2039
- sendMessage(messageType, level, title, body, data) {
2040
- if (!this.authenticated) return;
2041
- this.send({
2042
- type: "message",
2043
- agentId: this.config.agentId,
2044
- messageType,
2045
- level,
2046
- title,
2047
- body,
2048
- data
2049
- });
2050
- }
2051
- /**
2052
- * Send a command execution result
2053
- */
2054
- sendCommandResult(commandId, success, result) {
2055
- if (!this.authenticated) return;
2056
- this.send({
2057
- type: "command_result",
2058
- agentId: this.config.agentId,
2059
- commandId,
2060
- success,
2061
- result
2062
- });
2063
- }
2064
- /**
2065
- * Start the heartbeat timer
2066
- */
2067
- startHeartbeat() {
2068
- this.stopHeartbeat();
2069
- const interval = this.config.relay.heartbeatIntervalMs || 3e4;
2070
- this.heartbeatTimer = setInterval(() => {
2071
- if (this.ws?.readyState === import_ws.default.OPEN) {
2072
- this.ws.ping();
2073
- }
2074
- }, interval);
2075
- }
2076
- /**
2077
- * Stop the heartbeat timer
2078
- */
2079
- stopHeartbeat() {
2080
- if (this.heartbeatTimer) {
2081
- clearInterval(this.heartbeatTimer);
2082
- this.heartbeatTimer = null;
2083
- }
2084
- }
2085
- /**
2086
- * Schedule a reconnection with exponential backoff
2087
- */
2088
- scheduleReconnect() {
2089
- if (this.stopped) return;
2090
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
2091
- console.error("Relay: Max reconnection attempts reached. Giving up.");
2092
- return;
2093
- }
2094
- const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
2095
- this.reconnectAttempts++;
2096
- console.log(
2097
- `Relay: Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
2098
- );
2099
- this.reconnectTimer = setTimeout(() => {
2100
- this.connect().catch(() => {
2101
- });
2102
- }, delay);
2103
- }
2104
- /**
2105
- * Send a JSON message to the WebSocket
2106
- */
2107
- send(data) {
2108
- if (this.ws?.readyState === import_ws.default.OPEN) {
2109
- this.ws.send(JSON.stringify(data));
2110
- }
2111
- }
2112
- /**
2113
- * Check if connected and authenticated
2114
- */
2115
- get isConnected() {
2116
- return this.authenticated && this.ws?.readyState === import_ws.default.OPEN;
2117
- }
2118
- /**
2119
- * Disconnect and stop reconnecting
2120
- */
2121
- disconnect() {
2122
- this.stopped = true;
2123
- this.stopHeartbeat();
2124
- if (this.reconnectTimer) {
2125
- clearTimeout(this.reconnectTimer);
2126
- this.reconnectTimer = null;
2127
- }
2128
- if (this.ws) {
2129
- this.ws.close(1e3, "Agent shutting down");
2130
- this.ws = null;
2131
- }
2132
- this.authenticated = false;
2133
- console.log("Relay: Disconnected");
2134
- }
2135
- };
2136
-
2137
- // src/browser-open.ts
2138
- var import_child_process2 = require("child_process");
2139
- function openBrowser(url) {
2140
- const platform = process.platform;
2141
- try {
2142
- if (platform === "darwin") {
2143
- (0, import_child_process2.exec)(`open "${url}"`);
2144
- } else if (platform === "win32") {
2145
- (0, import_child_process2.exec)(`start "" "${url}"`);
2146
- } else {
2147
- (0, import_child_process2.exec)(`xdg-open "${url}"`);
2148
- }
2149
- } catch {
2150
- }
2151
- }
2152
-
2153
- // src/perp/client.ts
2154
- var HyperliquidClient = class {
2155
- apiUrl;
2156
- meta = null;
2157
- assetIndexCache = /* @__PURE__ */ new Map();
2158
- constructor(config) {
2159
- this.apiUrl = config.apiUrl;
2160
- }
2161
- // ============================================================
2162
- // INFO API (read-only)
2163
- // ============================================================
2164
- /** Fetch perpetuals metadata (asset specs, names, indices) */
2165
- async getMeta() {
2166
- if (this.meta) return this.meta;
2167
- const resp = await this.infoRequest({ type: "meta" });
2168
- this.meta = resp.universe;
2169
- this.meta.forEach((asset, idx) => {
2170
- this.assetIndexCache.set(asset.name, idx);
2171
- });
2172
- return this.meta;
2307
+ return this.meta;
2173
2308
  }
2174
2309
  /** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
2175
2310
  async getAssetIndex(coin) {
@@ -2754,7 +2889,7 @@ var PositionManager = class {
2754
2889
  };
2755
2890
 
2756
2891
  // src/perp/websocket.ts
2757
- var import_ws2 = __toESM(require("ws"));
2892
+ var import_ws = __toESM(require("ws"));
2758
2893
  var HyperliquidWebSocket = class {
2759
2894
  wsUrl;
2760
2895
  userAddress;
@@ -2786,14 +2921,14 @@ var HyperliquidWebSocket = class {
2786
2921
  * Connect to Hyperliquid WebSocket and subscribe to user events.
2787
2922
  */
2788
2923
  async connect() {
2789
- if (this.ws?.readyState === import_ws2.default.OPEN || this.isConnecting) {
2924
+ if (this.ws?.readyState === import_ws.default.OPEN || this.isConnecting) {
2790
2925
  return;
2791
2926
  }
2792
2927
  this.isConnecting = true;
2793
2928
  this.shouldReconnect = true;
2794
2929
  return new Promise((resolve, reject) => {
2795
2930
  try {
2796
- this.ws = new import_ws2.default(this.wsUrl);
2931
+ this.ws = new import_ws.default(this.wsUrl);
2797
2932
  this.ws.on("open", () => {
2798
2933
  this.isConnecting = false;
2799
2934
  this.reconnectAttempts = 0;
@@ -2846,7 +2981,7 @@ var HyperliquidWebSocket = class {
2846
2981
  this.stopPing();
2847
2982
  if (this.ws) {
2848
2983
  this.ws.removeAllListeners();
2849
- if (this.ws.readyState === import_ws2.default.OPEN) {
2984
+ if (this.ws.readyState === import_ws.default.OPEN) {
2850
2985
  this.ws.close(1e3, "Client disconnect");
2851
2986
  }
2852
2987
  this.ws = null;
@@ -2857,7 +2992,7 @@ var HyperliquidWebSocket = class {
2857
2992
  * Check if WebSocket is connected.
2858
2993
  */
2859
2994
  get isConnected() {
2860
- return this.ws?.readyState === import_ws2.default.OPEN;
2995
+ return this.ws?.readyState === import_ws.default.OPEN;
2861
2996
  }
2862
2997
  // ============================================================
2863
2998
  // EVENT HANDLERS
@@ -2984,7 +3119,7 @@ var HyperliquidWebSocket = class {
2984
3119
  startPing() {
2985
3120
  this.stopPing();
2986
3121
  this.pingTimer = setInterval(() => {
2987
- if (this.ws?.readyState === import_ws2.default.OPEN) {
3122
+ if (this.ws?.readyState === import_ws.default.OPEN) {
2988
3123
  this.ws.send(JSON.stringify({ method: "ping" }));
2989
3124
  }
2990
3125
  }, 25e3);
@@ -2999,7 +3134,7 @@ var HyperliquidWebSocket = class {
2999
3134
  // HELPERS
3000
3135
  // ============================================================
3001
3136
  subscribe(msg) {
3002
- if (this.ws?.readyState === import_ws2.default.OPEN) {
3137
+ if (this.ws?.readyState === import_ws.default.OPEN) {
3003
3138
  this.ws.send(JSON.stringify(msg));
3004
3139
  }
3005
3140
  }
@@ -3008,7 +3143,7 @@ var HyperliquidWebSocket = class {
3008
3143
  // src/perp/recorder.ts
3009
3144
  var import_viem4 = require("viem");
3010
3145
  var import_chains2 = require("viem/chains");
3011
- var import_accounts3 = require("viem/accounts");
3146
+ var import_accounts2 = require("viem/accounts");
3012
3147
  var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
3013
3148
  var ROUTER_ABI = [
3014
3149
  {
@@ -3047,7 +3182,7 @@ var PerpTradeRecorder = class {
3047
3182
  constructor(opts) {
3048
3183
  this.agentId = opts.agentId;
3049
3184
  this.configHash = opts.configHash;
3050
- this.account = (0, import_accounts3.privateKeyToAccount)(opts.privateKey);
3185
+ this.account = (0, import_accounts2.privateKeyToAccount)(opts.privateKey);
3051
3186
  const rpcUrl = opts.rpcUrl || "https://mainnet.base.org";
3052
3187
  const transport = (0, import_viem4.http)(rpcUrl);
3053
3188
  this.publicClient = (0, import_viem4.createPublicClient)({
@@ -3155,232 +3290,683 @@ var PerpTradeRecorder = class {
3155
3290
  }
3156
3291
  }
3157
3292
  /**
3158
- * Process the retry queue — attempt to re-submit failed recordings.
3293
+ * Process the retry queue — attempt to re-submit failed recordings.
3294
+ */
3295
+ async processRetryQueue() {
3296
+ if (this.retryQueue.length === 0) return;
3297
+ const now = Date.now();
3298
+ const toRetry = this.retryQueue.filter(
3299
+ (item) => now - item.lastAttempt >= RETRY_DELAY_MS
3300
+ );
3301
+ for (const item of toRetry) {
3302
+ item.retries++;
3303
+ item.lastAttempt = now;
3304
+ if (item.retries > MAX_RETRIES) {
3305
+ console.error(
3306
+ `Perp trade recording permanently failed after ${MAX_RETRIES} retries: ${item.params.instrument} ${item.params.fillId}`
3307
+ );
3308
+ const idx = this.retryQueue.indexOf(item);
3309
+ if (idx >= 0) this.retryQueue.splice(idx, 1);
3310
+ continue;
3311
+ }
3312
+ console.log(
3313
+ `Retrying perp trade recording (attempt ${item.retries}/${MAX_RETRIES}): ${item.params.instrument}`
3314
+ );
3315
+ const result = await this.submitRecord(item.params);
3316
+ if (result.success) {
3317
+ const idx = this.retryQueue.indexOf(item);
3318
+ if (idx >= 0) this.retryQueue.splice(idx, 1);
3319
+ }
3320
+ }
3321
+ }
3322
+ // ============================================================
3323
+ // CONVERSION HELPERS
3324
+ // ============================================================
3325
+ /**
3326
+ * Calculate notional USD from a fill (6-decimal).
3327
+ * notionalUSD = px * sz * 1e6
3328
+ */
3329
+ calculateNotionalUSD(fill) {
3330
+ const px = parseFloat(fill.px);
3331
+ const sz = parseFloat(fill.sz);
3332
+ return BigInt(Math.round(px * sz * 1e6));
3333
+ }
3334
+ /**
3335
+ * Calculate fee USD from a fill (6-decimal).
3336
+ * feeUSD = fee * 1e6 (fee is already in USD on Hyperliquid)
3337
+ */
3338
+ calculateFeeUSD(fill) {
3339
+ const fee = parseFloat(fill.fee);
3340
+ const builderFee = fill.builderFee ? parseFloat(fill.builderFee) : 0;
3341
+ return BigInt(Math.round((fee + builderFee) * 1e6));
3342
+ }
3343
+ };
3344
+
3345
+ // src/perp/onboarding.ts
3346
+ var PerpOnboarding = class {
3347
+ client;
3348
+ signer;
3349
+ config;
3350
+ constructor(client, signer, config) {
3351
+ this.client = client;
3352
+ this.signer = signer;
3353
+ this.config = config;
3354
+ }
3355
+ // ============================================================
3356
+ // BUILDER FEE
3357
+ // ============================================================
3358
+ /**
3359
+ * Check if the user has approved the builder fee.
3360
+ * Builder fee must be approved before orders can include builder fees.
3361
+ */
3362
+ async isBuilderFeeApproved() {
3363
+ try {
3364
+ const maxFee = await this.client.getMaxBuilderFee(
3365
+ this.signer.getAddress(),
3366
+ this.config.builderAddress
3367
+ );
3368
+ return maxFee >= this.config.builderFeeTenthsBps;
3369
+ } catch {
3370
+ return false;
3371
+ }
3372
+ }
3373
+ /**
3374
+ * Approve the builder fee on Hyperliquid.
3375
+ * This is a one-time approval per builder address.
3376
+ */
3377
+ async approveBuilderFee() {
3378
+ try {
3379
+ const action = {
3380
+ type: "approveBuilderFee",
3381
+ hyperliquidChain: "Mainnet",
3382
+ maxFeeRate: `${this.config.builderFeeTenthsBps / 1e4}%`,
3383
+ builder: this.config.builderAddress,
3384
+ nonce: Number(getNextNonce())
3385
+ };
3386
+ const { signature } = await this.signer.signApproval(action);
3387
+ const resp = await fetch(`${this.config.apiUrl}/exchange`, {
3388
+ method: "POST",
3389
+ headers: { "Content-Type": "application/json" },
3390
+ body: JSON.stringify({
3391
+ action,
3392
+ signature: {
3393
+ r: signature.slice(0, 66),
3394
+ s: `0x${signature.slice(66, 130)}`,
3395
+ v: parseInt(signature.slice(130, 132), 16)
3396
+ },
3397
+ nonce: action.nonce,
3398
+ vaultAddress: null
3399
+ })
3400
+ });
3401
+ if (!resp.ok) {
3402
+ const text = await resp.text();
3403
+ console.error(`Builder fee approval failed: ${resp.status} ${text}`);
3404
+ return false;
3405
+ }
3406
+ console.log(`Builder fee approved: ${this.config.builderFeeTenthsBps / 10} bps for ${this.config.builderAddress}`);
3407
+ return true;
3408
+ } catch (error) {
3409
+ const message = error instanceof Error ? error.message : String(error);
3410
+ console.error(`Builder fee approval failed: ${message}`);
3411
+ return false;
3412
+ }
3413
+ }
3414
+ // ============================================================
3415
+ // BALANCE & REQUIREMENTS
3416
+ // ============================================================
3417
+ /**
3418
+ * Check if the user has sufficient USDC balance on Hyperliquid.
3419
+ * Returns the account equity in USD.
3420
+ */
3421
+ async checkBalance() {
3422
+ try {
3423
+ const account = await this.client.getAccountSummary(this.signer.getAddress());
3424
+ return {
3425
+ hasBalance: account.totalEquity > 0,
3426
+ equity: account.totalEquity
3427
+ };
3428
+ } catch {
3429
+ return { hasBalance: false, equity: 0 };
3430
+ }
3431
+ }
3432
+ /**
3433
+ * Verify that the agent's risk universe allows perp trading.
3434
+ * Perps require risk universe >= 2 (Derivatives or higher).
3435
+ */
3436
+ verifyRiskUniverse(riskUniverse) {
3437
+ if (riskUniverse >= 2) {
3438
+ return {
3439
+ allowed: true,
3440
+ message: `Risk universe ${riskUniverse} allows perp trading`
3441
+ };
3442
+ }
3443
+ return {
3444
+ allowed: false,
3445
+ message: `Risk universe ${riskUniverse} does not allow perp trading. Perps require Derivatives (2) or higher.`
3446
+ };
3447
+ }
3448
+ // ============================================================
3449
+ // FULL ONBOARDING CHECK
3450
+ // ============================================================
3451
+ /**
3452
+ * Run all onboarding checks and return status.
3453
+ * Does NOT auto-approve — caller must explicitly approve after review.
3454
+ */
3455
+ async checkOnboardingStatus(riskUniverse) {
3456
+ const riskCheck = this.verifyRiskUniverse(riskUniverse);
3457
+ const balanceCheck = await this.checkBalance();
3458
+ const builderFeeApproved = await this.isBuilderFeeApproved();
3459
+ const ready = riskCheck.allowed && balanceCheck.hasBalance && builderFeeApproved;
3460
+ return {
3461
+ ready,
3462
+ riskUniverseOk: riskCheck.allowed,
3463
+ riskUniverseMessage: riskCheck.message,
3464
+ hasBalance: balanceCheck.hasBalance,
3465
+ equity: balanceCheck.equity,
3466
+ builderFeeApproved,
3467
+ builderAddress: this.config.builderAddress,
3468
+ builderFeeBps: this.config.builderFeeTenthsBps / 10
3469
+ };
3470
+ }
3471
+ /**
3472
+ * Run full onboarding: check status and auto-approve builder fee if needed.
3473
+ * Returns the final status after all actions.
3474
+ */
3475
+ async onboard(riskUniverse) {
3476
+ let status = await this.checkOnboardingStatus(riskUniverse);
3477
+ if (!status.riskUniverseOk) {
3478
+ console.error(`Perp onboarding blocked: ${status.riskUniverseMessage}`);
3479
+ return status;
3480
+ }
3481
+ if (!status.hasBalance) {
3482
+ console.warn("No USDC balance on Hyperliquid \u2014 deposit required before trading");
3483
+ return status;
3484
+ }
3485
+ if (!status.builderFeeApproved) {
3486
+ console.log("Approving builder fee...");
3487
+ const approved = await this.approveBuilderFee();
3488
+ if (approved) {
3489
+ status = { ...status, builderFeeApproved: true, ready: true };
3490
+ }
3491
+ }
3492
+ if (status.ready) {
3493
+ console.log(`Perp onboarding complete \u2014 equity: $${status.equity.toFixed(2)}`);
3494
+ }
3495
+ return status;
3496
+ }
3497
+ };
3498
+
3499
+ // src/perp/funding.ts
3500
+ var import_viem5 = require("viem");
3501
+ var import_chains3 = require("viem/chains");
3502
+ var import_accounts3 = require("viem/accounts");
3503
+ var ERC20_ABI = (0, import_viem5.parseAbi)([
3504
+ "function approve(address spender, uint256 amount) external returns (bool)",
3505
+ "function balanceOf(address account) external view returns (uint256)",
3506
+ "function allowance(address owner, address spender) external view returns (uint256)"
3507
+ ]);
3508
+ var TOKEN_MESSENGER_V2_ABI = (0, import_viem5.parseAbi)([
3509
+ "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken) external returns (uint64 nonce)",
3510
+ "event DepositForBurn(uint64 indexed nonce, address indexed burnToken, uint256 amount, address indexed depositor, bytes32 mintRecipient, uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller)"
3511
+ ]);
3512
+ var MESSAGE_TRANSMITTER_V2_ABI = (0, import_viem5.parseAbi)([
3513
+ "function receiveMessage(bytes message, bytes attestation) external returns (bool success)",
3514
+ "event MessageSent(bytes message)"
3515
+ ]);
3516
+ var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
3517
+ "function deposit(uint256 amount, uint32 destinationDex) external"
3518
+ ]);
3519
+
3520
+ // src/secure-env.ts
3521
+ var crypto = __toESM(require("crypto"));
3522
+ var fs = __toESM(require("fs"));
3523
+ var path = __toESM(require("path"));
3524
+ var ALGORITHM = "aes-256-gcm";
3525
+ var PBKDF2_ITERATIONS = 1e5;
3526
+ var SALT_LENGTH = 32;
3527
+ var IV_LENGTH = 16;
3528
+ var KEY_LENGTH = 32;
3529
+ var SENSITIVE_PATTERNS = [
3530
+ /PRIVATE_KEY$/i,
3531
+ /_API_KEY$/i,
3532
+ /API_KEY$/i,
3533
+ /_SECRET$/i,
3534
+ /^OPENAI_API_KEY$/i,
3535
+ /^ANTHROPIC_API_KEY$/i,
3536
+ /^GOOGLE_AI_API_KEY$/i,
3537
+ /^DEEPSEEK_API_KEY$/i,
3538
+ /^MISTRAL_API_KEY$/i,
3539
+ /^GROQ_API_KEY$/i,
3540
+ /^TOGETHER_API_KEY$/i
3541
+ ];
3542
+ function isSensitiveKey(key) {
3543
+ return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
3544
+ }
3545
+ function deriveKey(passphrase, salt) {
3546
+ return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
3547
+ }
3548
+ function encryptValue(value, key) {
3549
+ const iv = crypto.randomBytes(IV_LENGTH);
3550
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
3551
+ let encrypted = cipher.update(value, "utf8", "hex");
3552
+ encrypted += cipher.final("hex");
3553
+ const tag = cipher.getAuthTag();
3554
+ return {
3555
+ iv: iv.toString("hex"),
3556
+ encrypted,
3557
+ tag: tag.toString("hex")
3558
+ };
3559
+ }
3560
+ function decryptValue(encrypted, key, iv, tag) {
3561
+ const decipher = crypto.createDecipheriv(
3562
+ ALGORITHM,
3563
+ key,
3564
+ Buffer.from(iv, "hex")
3565
+ );
3566
+ decipher.setAuthTag(Buffer.from(tag, "hex"));
3567
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
3568
+ decrypted += decipher.final("utf8");
3569
+ return decrypted;
3570
+ }
3571
+ function parseEnvFile(content) {
3572
+ const entries = [];
3573
+ for (const line of content.split("\n")) {
3574
+ const trimmed = line.trim();
3575
+ if (!trimmed || trimmed.startsWith("#")) continue;
3576
+ const eqIndex = trimmed.indexOf("=");
3577
+ if (eqIndex === -1) continue;
3578
+ const key = trimmed.slice(0, eqIndex).trim();
3579
+ let value = trimmed.slice(eqIndex + 1).trim();
3580
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
3581
+ value = value.slice(1, -1);
3582
+ }
3583
+ if (key && value) {
3584
+ entries.push({ key, value });
3585
+ }
3586
+ }
3587
+ return entries;
3588
+ }
3589
+ function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
3590
+ if (!fs.existsSync(envPath)) {
3591
+ throw new Error(`File not found: ${envPath}`);
3592
+ }
3593
+ const content = fs.readFileSync(envPath, "utf-8");
3594
+ const entries = parseEnvFile(content);
3595
+ if (entries.length === 0) {
3596
+ throw new Error("No environment variables found in file");
3597
+ }
3598
+ const salt = crypto.randomBytes(SALT_LENGTH);
3599
+ const key = deriveKey(passphrase, salt);
3600
+ const encryptedEntries = entries.map(({ key: envKey, value }) => {
3601
+ if (isSensitiveKey(envKey)) {
3602
+ const { iv, encrypted, tag } = encryptValue(value, key);
3603
+ return {
3604
+ key: envKey,
3605
+ value: encrypted,
3606
+ encrypted: true,
3607
+ iv,
3608
+ tag
3609
+ };
3610
+ }
3611
+ return {
3612
+ key: envKey,
3613
+ value,
3614
+ encrypted: false
3615
+ };
3616
+ });
3617
+ const encryptedEnv = {
3618
+ version: 1,
3619
+ salt: salt.toString("hex"),
3620
+ entries: encryptedEntries
3621
+ };
3622
+ const encPath = envPath + ".enc";
3623
+ fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
3624
+ if (deleteOriginal) {
3625
+ fs.unlinkSync(envPath);
3626
+ }
3627
+ const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
3628
+ const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
3629
+ console.log(
3630
+ `Encrypted ${sensitiveCount} sensitive values (${plainCount} non-sensitive kept as plaintext)`
3631
+ );
3632
+ return encPath;
3633
+ }
3634
+ function decryptEnvFile(encPath, passphrase) {
3635
+ if (!fs.existsSync(encPath)) {
3636
+ throw new Error(`Encrypted env file not found: ${encPath}`);
3637
+ }
3638
+ const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
3639
+ if (content.version !== 1) {
3640
+ throw new Error(`Unsupported encrypted env version: ${content.version}`);
3641
+ }
3642
+ const salt = Buffer.from(content.salt, "hex");
3643
+ const key = deriveKey(passphrase, salt);
3644
+ const result = {};
3645
+ for (const entry of content.entries) {
3646
+ if (entry.encrypted) {
3647
+ if (!entry.iv || !entry.tag) {
3648
+ throw new Error(`Missing encryption metadata for ${entry.key}`);
3649
+ }
3650
+ try {
3651
+ result[entry.key] = decryptValue(entry.value, key, entry.iv, entry.tag);
3652
+ } catch {
3653
+ throw new Error(
3654
+ `Failed to decrypt ${entry.key}. Wrong passphrase or corrupted data.`
3655
+ );
3656
+ }
3657
+ } else {
3658
+ result[entry.key] = entry.value;
3659
+ }
3660
+ }
3661
+ return result;
3662
+ }
3663
+ function loadSecureEnv(basePath, passphrase) {
3664
+ const encPath = path.join(basePath, ".env.enc");
3665
+ const envPath = path.join(basePath, ".env");
3666
+ if (fs.existsSync(encPath)) {
3667
+ if (!passphrase) {
3668
+ passphrase = process.env.EXAGENT_PASSPHRASE;
3669
+ }
3670
+ if (!passphrase) {
3671
+ console.warn("");
3672
+ console.warn("WARNING: Found .env.enc but no passphrase provided.");
3673
+ console.warn(" Set EXAGENT_PASSPHRASE environment variable or");
3674
+ console.warn(" pass --passphrase when running the agent.");
3675
+ console.warn(" Falling back to plaintext .env file.");
3676
+ console.warn("");
3677
+ } else {
3678
+ const vars = decryptEnvFile(encPath, passphrase);
3679
+ for (const [key, value] of Object.entries(vars)) {
3680
+ process.env[key] = value;
3681
+ }
3682
+ return true;
3683
+ }
3684
+ }
3685
+ if (fs.existsSync(envPath)) {
3686
+ const content = fs.readFileSync(envPath, "utf-8");
3687
+ const entries = parseEnvFile(content);
3688
+ const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
3689
+ if (sensitiveKeys.length > 0) {
3690
+ console.warn("");
3691
+ console.warn("WARNING: Sensitive values stored in plaintext .env file:");
3692
+ for (const key of sensitiveKeys) {
3693
+ console.warn(` - ${key}`);
3694
+ }
3695
+ console.warn("");
3696
+ console.warn(' Run "npx @exagent/agent encrypt" to secure your keys.');
3697
+ console.warn("");
3698
+ }
3699
+ return false;
3700
+ }
3701
+ return false;
3702
+ }
3703
+
3704
+ // src/index.ts
3705
+ var AGENT_VERSION = "0.1.21";
3706
+
3707
+ // src/relay.ts
3708
+ var RelayClient = class {
3709
+ config;
3710
+ ws = null;
3711
+ authenticated = false;
3712
+ authRejected = false;
3713
+ reconnectAttempts = 0;
3714
+ maxReconnectAttempts = 50;
3715
+ reconnectTimer = null;
3716
+ heartbeatTimer = null;
3717
+ stopped = false;
3718
+ constructor(config) {
3719
+ this.config = config;
3720
+ }
3721
+ /**
3722
+ * Connect to the relay server
3723
+ */
3724
+ async connect() {
3725
+ if (this.stopped) return;
3726
+ const wsUrl = this.config.relay.apiUrl.replace(/^https?:\/\//, (m) => m.includes("https") ? "wss://" : "ws://").replace(/\/$/, "") + "/ws/agent";
3727
+ return new Promise((resolve, reject) => {
3728
+ try {
3729
+ this.ws = new import_ws2.default(wsUrl);
3730
+ } catch (error) {
3731
+ console.error("Relay: Failed to create WebSocket:", error);
3732
+ this.scheduleReconnect();
3733
+ reject(error);
3734
+ return;
3735
+ }
3736
+ const connectTimeout = setTimeout(() => {
3737
+ if (!this.authenticated) {
3738
+ console.error("Relay: Connection timeout");
3739
+ this.ws?.close();
3740
+ this.scheduleReconnect();
3741
+ reject(new Error("Connection timeout"));
3742
+ }
3743
+ }, 15e3);
3744
+ this.ws.on("open", async () => {
3745
+ this.authRejected = false;
3746
+ console.log("Relay: Connected, authenticating...");
3747
+ try {
3748
+ await this.authenticate();
3749
+ } catch (error) {
3750
+ console.error("Relay: Authentication failed:", error);
3751
+ this.ws?.close();
3752
+ clearTimeout(connectTimeout);
3753
+ reject(error);
3754
+ }
3755
+ });
3756
+ this.ws.on("message", (raw) => {
3757
+ try {
3758
+ const data = JSON.parse(raw.toString());
3759
+ this.handleMessage(data);
3760
+ if (data.type === "auth_success") {
3761
+ clearTimeout(connectTimeout);
3762
+ this.authenticated = true;
3763
+ this.reconnectAttempts = 0;
3764
+ this.startHeartbeat();
3765
+ console.log("Relay: Authenticated successfully");
3766
+ resolve();
3767
+ } else if (data.type === "auth_error") {
3768
+ clearTimeout(connectTimeout);
3769
+ this.authRejected = true;
3770
+ console.error(`Relay: Auth rejected: ${data.message}`);
3771
+ reject(new Error(data.message));
3772
+ }
3773
+ } catch {
3774
+ }
3775
+ });
3776
+ this.ws.on("close", (code, reason) => {
3777
+ clearTimeout(connectTimeout);
3778
+ this.authenticated = false;
3779
+ this.stopHeartbeat();
3780
+ if (!this.stopped) {
3781
+ if (!this.authRejected) {
3782
+ console.log(`Relay: Disconnected (${code}: ${reason.toString() || "unknown"})`);
3783
+ }
3784
+ this.scheduleReconnect();
3785
+ }
3786
+ });
3787
+ this.ws.on("error", (error) => {
3788
+ if (!this.stopped) {
3789
+ console.error("Relay: WebSocket error:", error.message);
3790
+ }
3791
+ });
3792
+ });
3793
+ }
3794
+ /**
3795
+ * Authenticate with the relay server using wallet signature
3796
+ */
3797
+ async authenticate() {
3798
+ const account = (0, import_accounts4.privateKeyToAccount)(this.config.privateKey);
3799
+ const timestamp = Math.floor(Date.now() / 1e3);
3800
+ const message = `ExagentRelay:${this.config.agentId}:${timestamp}`;
3801
+ const signature = await (0, import_accounts4.signMessage)({
3802
+ message,
3803
+ privateKey: this.config.privateKey
3804
+ });
3805
+ this.send({
3806
+ type: "auth",
3807
+ agentId: this.config.agentId,
3808
+ wallet: account.address,
3809
+ timestamp,
3810
+ signature,
3811
+ sdkVersion: AGENT_VERSION
3812
+ });
3813
+ }
3814
+ /**
3815
+ * Handle incoming messages from the relay server
3159
3816
  */
3160
- async processRetryQueue() {
3161
- if (this.retryQueue.length === 0) return;
3162
- const now = Date.now();
3163
- const toRetry = this.retryQueue.filter(
3164
- (item) => now - item.lastAttempt >= RETRY_DELAY_MS
3165
- );
3166
- for (const item of toRetry) {
3167
- item.retries++;
3168
- item.lastAttempt = now;
3169
- if (item.retries > MAX_RETRIES) {
3170
- console.error(
3171
- `Perp trade recording permanently failed after ${MAX_RETRIES} retries: ${item.params.instrument} ${item.params.fillId}`
3172
- );
3173
- const idx = this.retryQueue.indexOf(item);
3174
- if (idx >= 0) this.retryQueue.splice(idx, 1);
3175
- continue;
3176
- }
3177
- console.log(
3178
- `Retrying perp trade recording (attempt ${item.retries}/${MAX_RETRIES}): ${item.params.instrument}`
3179
- );
3180
- const result = await this.submitRecord(item.params);
3181
- if (result.success) {
3182
- const idx = this.retryQueue.indexOf(item);
3183
- if (idx >= 0) this.retryQueue.splice(idx, 1);
3184
- }
3817
+ handleMessage(data) {
3818
+ switch (data.type) {
3819
+ case "command":
3820
+ if (data.command && this.config.onCommand) {
3821
+ this.config.onCommand(data.command);
3822
+ }
3823
+ break;
3824
+ case "auth_success":
3825
+ case "auth_error":
3826
+ break;
3827
+ case "error":
3828
+ console.error(`Relay: Server error: ${data.message}`);
3829
+ break;
3185
3830
  }
3186
3831
  }
3187
- // ============================================================
3188
- // CONVERSION HELPERS
3189
- // ============================================================
3190
3832
  /**
3191
- * Calculate notional USD from a fill (6-decimal).
3192
- * notionalUSD = px * sz * 1e6
3833
+ * Send a status heartbeat
3193
3834
  */
3194
- calculateNotionalUSD(fill) {
3195
- const px = parseFloat(fill.px);
3196
- const sz = parseFloat(fill.sz);
3197
- return BigInt(Math.round(px * sz * 1e6));
3835
+ sendHeartbeat(status) {
3836
+ if (!this.authenticated) return;
3837
+ this.send({
3838
+ type: "heartbeat",
3839
+ agentId: this.config.agentId,
3840
+ status
3841
+ });
3198
3842
  }
3199
3843
  /**
3200
- * Calculate fee USD from a fill (6-decimal).
3201
- * feeUSD = fee * 1e6 (fee is already in USD on Hyperliquid)
3844
+ * Send a status update (outside of regular heartbeat)
3202
3845
  */
3203
- calculateFeeUSD(fill) {
3204
- const fee = parseFloat(fill.fee);
3205
- const builderFee = fill.builderFee ? parseFloat(fill.builderFee) : 0;
3206
- return BigInt(Math.round((fee + builderFee) * 1e6));
3207
- }
3208
- };
3209
-
3210
- // src/perp/onboarding.ts
3211
- var PerpOnboarding = class {
3212
- client;
3213
- signer;
3214
- config;
3215
- constructor(client, signer, config) {
3216
- this.client = client;
3217
- this.signer = signer;
3218
- this.config = config;
3846
+ sendStatusUpdate(status) {
3847
+ if (!this.authenticated) return;
3848
+ this.send({
3849
+ type: "status_update",
3850
+ agentId: this.config.agentId,
3851
+ status
3852
+ });
3219
3853
  }
3220
- // ============================================================
3221
- // BUILDER FEE
3222
- // ============================================================
3223
3854
  /**
3224
- * Check if the user has approved the builder fee.
3225
- * Builder fee must be approved before orders can include builder fees.
3855
+ * Send a message to the command center
3226
3856
  */
3227
- async isBuilderFeeApproved() {
3228
- try {
3229
- const maxFee = await this.client.getMaxBuilderFee(
3230
- this.signer.getAddress(),
3231
- this.config.builderAddress
3232
- );
3233
- return maxFee >= this.config.builderFeeTenthsBps;
3234
- } catch {
3235
- return false;
3236
- }
3857
+ sendMessage(messageType, level, title, body, data) {
3858
+ if (!this.authenticated) return;
3859
+ this.send({
3860
+ type: "message",
3861
+ agentId: this.config.agentId,
3862
+ messageType,
3863
+ level,
3864
+ title,
3865
+ body,
3866
+ data
3867
+ });
3237
3868
  }
3238
3869
  /**
3239
- * Approve the builder fee on Hyperliquid.
3240
- * This is a one-time approval per builder address.
3870
+ * Send a command execution result
3241
3871
  */
3242
- async approveBuilderFee() {
3243
- try {
3244
- const action = {
3245
- type: "approveBuilderFee",
3246
- hyperliquidChain: "Mainnet",
3247
- maxFeeRate: `${this.config.builderFeeTenthsBps / 1e4}%`,
3248
- builder: this.config.builderAddress,
3249
- nonce: Number(getNextNonce())
3250
- };
3251
- const { signature } = await this.signer.signApproval(action);
3252
- const resp = await fetch(`${this.config.apiUrl}/exchange`, {
3253
- method: "POST",
3254
- headers: { "Content-Type": "application/json" },
3255
- body: JSON.stringify({
3256
- action,
3257
- signature: {
3258
- r: signature.slice(0, 66),
3259
- s: `0x${signature.slice(66, 130)}`,
3260
- v: parseInt(signature.slice(130, 132), 16)
3261
- },
3262
- nonce: action.nonce,
3263
- vaultAddress: null
3264
- })
3265
- });
3266
- if (!resp.ok) {
3267
- const text = await resp.text();
3268
- console.error(`Builder fee approval failed: ${resp.status} ${text}`);
3269
- return false;
3270
- }
3271
- console.log(`Builder fee approved: ${this.config.builderFeeTenthsBps / 10} bps for ${this.config.builderAddress}`);
3272
- return true;
3273
- } catch (error) {
3274
- const message = error instanceof Error ? error.message : String(error);
3275
- console.error(`Builder fee approval failed: ${message}`);
3276
- return false;
3277
- }
3872
+ sendCommandResult(commandId, success, result) {
3873
+ if (!this.authenticated) return;
3874
+ this.send({
3875
+ type: "command_result",
3876
+ agentId: this.config.agentId,
3877
+ commandId,
3878
+ success,
3879
+ result
3880
+ });
3278
3881
  }
3279
- // ============================================================
3280
- // BALANCE & REQUIREMENTS
3281
- // ============================================================
3282
3882
  /**
3283
- * Check if the user has sufficient USDC balance on Hyperliquid.
3284
- * Returns the account equity in USD.
3883
+ * Start the heartbeat timer
3285
3884
  */
3286
- async checkBalance() {
3287
- try {
3288
- const account = await this.client.getAccountSummary(this.signer.getAddress());
3289
- return {
3290
- hasBalance: account.totalEquity > 0,
3291
- equity: account.totalEquity
3292
- };
3293
- } catch {
3294
- return { hasBalance: false, equity: 0 };
3295
- }
3885
+ startHeartbeat() {
3886
+ this.stopHeartbeat();
3887
+ const interval = this.config.relay.heartbeatIntervalMs || 3e4;
3888
+ this.heartbeatTimer = setInterval(() => {
3889
+ if (this.ws?.readyState === import_ws2.default.OPEN) {
3890
+ this.ws.ping();
3891
+ }
3892
+ }, interval);
3296
3893
  }
3297
3894
  /**
3298
- * Verify that the agent's risk universe allows perp trading.
3299
- * Perps require risk universe >= 2 (Derivatives or higher).
3895
+ * Stop the heartbeat timer
3300
3896
  */
3301
- verifyRiskUniverse(riskUniverse) {
3302
- if (riskUniverse >= 2) {
3303
- return {
3304
- allowed: true,
3305
- message: `Risk universe ${riskUniverse} allows perp trading`
3306
- };
3897
+ stopHeartbeat() {
3898
+ if (this.heartbeatTimer) {
3899
+ clearInterval(this.heartbeatTimer);
3900
+ this.heartbeatTimer = null;
3307
3901
  }
3308
- return {
3309
- allowed: false,
3310
- message: `Risk universe ${riskUniverse} does not allow perp trading. Perps require Derivatives (2) or higher.`
3311
- };
3312
3902
  }
3313
- // ============================================================
3314
- // FULL ONBOARDING CHECK
3315
- // ============================================================
3316
3903
  /**
3317
- * Run all onboarding checks and return status.
3318
- * Does NOT auto-approve — caller must explicitly approve after review.
3904
+ * Schedule a reconnection with exponential backoff
3319
3905
  */
3320
- async checkOnboardingStatus(riskUniverse) {
3321
- const riskCheck = this.verifyRiskUniverse(riskUniverse);
3322
- const balanceCheck = await this.checkBalance();
3323
- const builderFeeApproved = await this.isBuilderFeeApproved();
3324
- const ready = riskCheck.allowed && balanceCheck.hasBalance && builderFeeApproved;
3325
- return {
3326
- ready,
3327
- riskUniverseOk: riskCheck.allowed,
3328
- riskUniverseMessage: riskCheck.message,
3329
- hasBalance: balanceCheck.hasBalance,
3330
- equity: balanceCheck.equity,
3331
- builderFeeApproved,
3332
- builderAddress: this.config.builderAddress,
3333
- builderFeeBps: this.config.builderFeeTenthsBps / 10
3334
- };
3906
+ scheduleReconnect() {
3907
+ if (this.stopped) return;
3908
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
3909
+ console.error("Relay: Max reconnection attempts reached. Giving up.");
3910
+ return;
3911
+ }
3912
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
3913
+ this.reconnectAttempts++;
3914
+ console.log(
3915
+ `Relay: Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
3916
+ );
3917
+ this.reconnectTimer = setTimeout(() => {
3918
+ this.connect().catch(() => {
3919
+ });
3920
+ }, delay);
3335
3921
  }
3336
3922
  /**
3337
- * Run full onboarding: check status and auto-approve builder fee if needed.
3338
- * Returns the final status after all actions.
3923
+ * Send a JSON message to the WebSocket
3339
3924
  */
3340
- async onboard(riskUniverse) {
3341
- let status = await this.checkOnboardingStatus(riskUniverse);
3342
- if (!status.riskUniverseOk) {
3343
- console.error(`Perp onboarding blocked: ${status.riskUniverseMessage}`);
3344
- return status;
3345
- }
3346
- if (!status.hasBalance) {
3347
- console.warn("No USDC balance on Hyperliquid \u2014 deposit required before trading");
3348
- return status;
3349
- }
3350
- if (!status.builderFeeApproved) {
3351
- console.log("Approving builder fee...");
3352
- const approved = await this.approveBuilderFee();
3353
- if (approved) {
3354
- status = { ...status, builderFeeApproved: true, ready: true };
3355
- }
3925
+ send(data) {
3926
+ if (this.ws?.readyState === import_ws2.default.OPEN) {
3927
+ this.ws.send(JSON.stringify(data));
3356
3928
  }
3357
- if (status.ready) {
3358
- console.log(`Perp onboarding complete \u2014 equity: $${status.equity.toFixed(2)}`);
3929
+ }
3930
+ /**
3931
+ * Check if connected and authenticated
3932
+ */
3933
+ get isConnected() {
3934
+ return this.authenticated && this.ws?.readyState === import_ws2.default.OPEN;
3935
+ }
3936
+ /**
3937
+ * Disconnect and stop reconnecting
3938
+ */
3939
+ disconnect() {
3940
+ this.stopped = true;
3941
+ this.stopHeartbeat();
3942
+ if (this.reconnectTimer) {
3943
+ clearTimeout(this.reconnectTimer);
3944
+ this.reconnectTimer = null;
3359
3945
  }
3360
- return status;
3946
+ if (this.ws) {
3947
+ this.ws.close(1e3, "Agent shutting down");
3948
+ this.ws = null;
3949
+ }
3950
+ this.authenticated = false;
3951
+ console.log("Relay: Disconnected");
3361
3952
  }
3362
3953
  };
3363
3954
 
3364
- // src/perp/funding.ts
3365
- var import_viem5 = require("viem");
3366
- var import_chains3 = require("viem/chains");
3367
- var import_accounts4 = require("viem/accounts");
3368
- var ERC20_ABI = (0, import_viem5.parseAbi)([
3369
- "function approve(address spender, uint256 amount) external returns (bool)",
3370
- "function balanceOf(address account) external view returns (uint256)",
3371
- "function allowance(address owner, address spender) external view returns (uint256)"
3372
- ]);
3373
- var TOKEN_MESSENGER_V2_ABI = (0, import_viem5.parseAbi)([
3374
- "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken) external returns (uint64 nonce)",
3375
- "event DepositForBurn(uint64 indexed nonce, address indexed burnToken, uint256 amount, address indexed depositor, bytes32 mintRecipient, uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller)"
3376
- ]);
3377
- var MESSAGE_TRANSMITTER_V2_ABI = (0, import_viem5.parseAbi)([
3378
- "function receiveMessage(bytes message, bytes attestation) external returns (bool success)",
3379
- "event MessageSent(bytes message)"
3380
- ]);
3381
- var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
3382
- "function deposit(uint256 amount, uint32 destinationDex) external"
3383
- ]);
3955
+ // src/browser-open.ts
3956
+ var import_child_process2 = require("child_process");
3957
+ function openBrowser(url) {
3958
+ const platform = process.platform;
3959
+ try {
3960
+ if (platform === "darwin") {
3961
+ (0, import_child_process2.exec)(`open "${url}"`);
3962
+ } else if (platform === "win32") {
3963
+ (0, import_child_process2.exec)(`start "" "${url}"`);
3964
+ } else {
3965
+ (0, import_child_process2.exec)(`xdg-open "${url}"`);
3966
+ }
3967
+ } catch {
3968
+ }
3969
+ }
3384
3970
 
3385
3971
  // src/runtime.ts
3386
3972
  var FUNDS_LOW_THRESHOLD = 5e-3;
@@ -3402,9 +3988,12 @@ var AgentRuntime = class {
3402
3988
  lastCycleAt = 0;
3403
3989
  lastPortfolioValue = 0;
3404
3990
  lastEthBalance = "0";
3991
+ lastPrices = {};
3405
3992
  processAlive = true;
3406
3993
  riskUniverse = 0;
3407
3994
  allowedTokens = /* @__PURE__ */ new Set();
3995
+ strategyContext;
3996
+ positionTracker;
3408
3997
  // Perp trading components (null if perp not enabled)
3409
3998
  perpClient = null;
3410
3999
  perpSigner = null;
@@ -3429,7 +4018,7 @@ var AgentRuntime = class {
3429
4018
  */
3430
4019
  async initialize() {
3431
4020
  console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
3432
- this.client = new import_sdk2.ExagentClient({
4021
+ this.client = new import_sdk.ExagentClient({
3433
4022
  privateKey: this.config.privateKey,
3434
4023
  network: this.config.network
3435
4024
  });
@@ -3447,9 +4036,20 @@ var AgentRuntime = class {
3447
4036
  console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
3448
4037
  await this.syncConfigHash();
3449
4038
  this.strategy = await loadStrategy();
4039
+ const store = new FileStore();
4040
+ this.strategyContext = {
4041
+ store,
4042
+ agentId: Number(this.config.agentId),
4043
+ walletAddress: this.client.address
4044
+ };
4045
+ this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
3450
4046
  this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
3451
4047
  this.riskManager = new RiskManager(this.config.trading);
3452
4048
  this.marketData = new MarketDataService(this.getRpcUrl());
4049
+ const savedRisk = this.positionTracker.getRiskState();
4050
+ if (savedRisk.lastResetDate) {
4051
+ this.riskManager.restoreState(savedRisk);
4052
+ }
3453
4053
  await this.initializeVaultManager();
3454
4054
  await this.initializePerp();
3455
4055
  await this.initializeRelay();
@@ -3635,7 +4235,7 @@ var AgentRuntime = class {
3635
4235
  if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
3636
4236
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
3637
4237
  const nonce = await this.client.registry.getNonce(address);
3638
- const linkMessage = import_sdk2.ExagentRegistry.generateLinkMessage(
4238
+ const linkMessage = import_sdk.ExagentRegistry.generateLinkMessage(
3639
4239
  address,
3640
4240
  agentId,
3641
4241
  nonce
@@ -3726,18 +4326,17 @@ var AgentRuntime = class {
3726
4326
  async syncConfigHash() {
3727
4327
  const agentId = BigInt(this.config.agentId);
3728
4328
  const llmMeta = this.llm.getMetadata();
3729
- this.configHash = import_sdk2.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
4329
+ this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
3730
4330
  console.log(`Config hash: ${this.configHash}`);
3731
4331
  const onChainHash = await this.client.registry.getConfigHash(agentId);
3732
4332
  if (onChainHash !== this.configHash) {
3733
4333
  console.log("Config changed, updating on-chain...");
3734
4334
  try {
3735
4335
  await this.client.registry.updateConfig(agentId, this.configHash);
3736
- const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
3737
- console.log(`Config updated, new epoch started: ${newEpoch}`);
4336
+ console.log(`Config updated on-chain`);
3738
4337
  } catch (error) {
3739
4338
  const message = error instanceof Error ? error.message : String(error);
3740
- if (message.includes("insufficient funds") || message.includes("gas") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
4339
+ if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
3741
4340
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
3742
4341
  const chain = import_chains4.base;
3743
4342
  const publicClientInstance = (0, import_viem6.createPublicClient)({
@@ -3765,19 +4364,18 @@ var AgentRuntime = class {
3765
4364
  console.log(" ETH detected! Retrying config update...");
3766
4365
  console.log("");
3767
4366
  await this.client.registry.updateConfig(agentId, this.configHash);
3768
- const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
3769
- console.log(`Config updated, new epoch started: ${newEpoch}`);
4367
+ console.log(`Config updated on-chain`);
3770
4368
  return;
3771
4369
  }
3772
4370
  process.stdout.write(".");
3773
4371
  }
3774
4372
  } else {
3775
- throw error;
4373
+ console.warn(`Config update skipped (continuing with on-chain config): ${message}`);
4374
+ this.configHash = onChainHash;
3776
4375
  }
3777
4376
  }
3778
4377
  } else {
3779
- const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
3780
- console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
4378
+ console.log("Config hash matches on-chain");
3781
4379
  }
3782
4380
  }
3783
4381
  /**
@@ -3911,6 +4509,10 @@ var AgentRuntime = class {
3911
4509
  }
3912
4510
  if (updated) {
3913
4511
  this.riskManager = new RiskManager(this.config.trading);
4512
+ const savedRiskState = this.positionTracker.getRiskState();
4513
+ if (savedRiskState.lastResetDate) {
4514
+ this.riskManager.restoreState(savedRiskState);
4515
+ }
3914
4516
  console.log("Risk params updated via command center");
3915
4517
  this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
3916
4518
  this.relay?.sendMessage(
@@ -4114,6 +4716,7 @@ var AgentRuntime = class {
4114
4716
  mode: this.mode,
4115
4717
  agentId: String(this.config.agentId),
4116
4718
  wallet: this.client?.address,
4719
+ sdkVersion: AGENT_VERSION,
4117
4720
  cycleCount: this.cycleCount,
4118
4721
  lastCycleAt: this.lastCycleAt,
4119
4722
  tradingIntervalMs: this.config.trading.tradingIntervalMs,
@@ -4142,7 +4745,8 @@ var AgentRuntime = class {
4142
4745
  openPositions: 0,
4143
4746
  effectiveLeverage: 0,
4144
4747
  pendingRecords: this.perpRecorder?.pendingRetries ?? 0
4145
- } : void 0
4748
+ } : void 0,
4749
+ positions: this.positionTracker ? this.positionTracker.getPositionSummary(this.lastPrices) : void 0
4146
4750
  };
4147
4751
  if (this.perpConnected && this.perpPositions && status.perp) {
4148
4752
  this.perpPositions.getAccountSummary().then((account) => {
@@ -4173,17 +4777,22 @@ var AgentRuntime = class {
4173
4777
  const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
4174
4778
  console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
4175
4779
  this.lastPortfolioValue = marketData.portfolioValue;
4780
+ this.lastPrices = marketData.prices;
4176
4781
  const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4177
4782
  this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
4783
+ this.positionTracker.syncBalances(marketData.balances, marketData.prices);
4178
4784
  const fundsOk = this.checkFundsLow(marketData);
4179
4785
  if (!fundsOk) {
4180
4786
  console.warn("Skipping trading cycle \u2014 ETH balance critically low");
4181
4787
  this.sendRelayStatus();
4182
4788
  return;
4183
4789
  }
4790
+ this.strategyContext.positions = this.positionTracker.getPositions();
4791
+ this.strategyContext.tradeHistory = this.positionTracker.getTradeHistory(20);
4792
+ this.strategyContext.positionTracker = this.positionTracker;
4184
4793
  let signals;
4185
4794
  try {
4186
- signals = await this.strategy(marketData, this.llm, this.config);
4795
+ signals = await this.strategy(marketData, this.llm, this.config, this.strategyContext);
4187
4796
  } catch (error) {
4188
4797
  const message = error instanceof Error ? error.message : String(error);
4189
4798
  console.error("LLM/strategy error:", message);
@@ -4246,13 +4855,30 @@ var AgentRuntime = class {
4246
4855
  );
4247
4856
  }
4248
4857
  }
4858
+ for (const result of results) {
4859
+ const tokenIn = result.signal.tokenIn.toLowerCase();
4860
+ const tokenOut = result.signal.tokenOut.toLowerCase();
4861
+ this.positionTracker.recordTrade({
4862
+ action: result.signal.action,
4863
+ tokenIn,
4864
+ tokenOut,
4865
+ amountIn: result.signal.amountIn,
4866
+ priceIn: marketData.prices[tokenIn] || 0,
4867
+ priceOut: marketData.prices[tokenOut] || 0,
4868
+ txHash: result.txHash,
4869
+ reasoning: result.signal.reasoning,
4870
+ success: result.success
4871
+ });
4872
+ }
4249
4873
  const postTokens = this.config.allowedTokens || this.getDefaultTokens();
4250
4874
  const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
4251
4875
  const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
4252
4876
  this.riskManager.updatePnL(marketPnL);
4877
+ this.positionTracker.saveRiskState(this.riskManager.exportState());
4253
4878
  if (marketPnL !== 0) {
4254
4879
  console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
4255
4880
  }
4881
+ this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
4256
4882
  this.lastPortfolioValue = postTradeData.portfolioValue;
4257
4883
  const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4258
4884
  this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
@@ -4527,192 +5153,6 @@ var AgentRuntime = class {
4527
5153
 
4528
5154
  // src/cli.ts
4529
5155
  var import_accounts6 = require("viem/accounts");
4530
-
4531
- // src/secure-env.ts
4532
- var crypto = __toESM(require("crypto"));
4533
- var fs = __toESM(require("fs"));
4534
- var path = __toESM(require("path"));
4535
- var ALGORITHM = "aes-256-gcm";
4536
- var PBKDF2_ITERATIONS = 1e5;
4537
- var SALT_LENGTH = 32;
4538
- var IV_LENGTH = 16;
4539
- var KEY_LENGTH = 32;
4540
- var SENSITIVE_PATTERNS = [
4541
- /PRIVATE_KEY$/i,
4542
- /_API_KEY$/i,
4543
- /API_KEY$/i,
4544
- /_SECRET$/i,
4545
- /^OPENAI_API_KEY$/i,
4546
- /^ANTHROPIC_API_KEY$/i,
4547
- /^GOOGLE_AI_API_KEY$/i,
4548
- /^DEEPSEEK_API_KEY$/i,
4549
- /^MISTRAL_API_KEY$/i,
4550
- /^GROQ_API_KEY$/i,
4551
- /^TOGETHER_API_KEY$/i
4552
- ];
4553
- function isSensitiveKey(key) {
4554
- return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
4555
- }
4556
- function deriveKey(passphrase, salt) {
4557
- return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
4558
- }
4559
- function encryptValue(value, key) {
4560
- const iv = crypto.randomBytes(IV_LENGTH);
4561
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
4562
- let encrypted = cipher.update(value, "utf8", "hex");
4563
- encrypted += cipher.final("hex");
4564
- const tag = cipher.getAuthTag();
4565
- return {
4566
- iv: iv.toString("hex"),
4567
- encrypted,
4568
- tag: tag.toString("hex")
4569
- };
4570
- }
4571
- function decryptValue(encrypted, key, iv, tag) {
4572
- const decipher = crypto.createDecipheriv(
4573
- ALGORITHM,
4574
- key,
4575
- Buffer.from(iv, "hex")
4576
- );
4577
- decipher.setAuthTag(Buffer.from(tag, "hex"));
4578
- let decrypted = decipher.update(encrypted, "hex", "utf8");
4579
- decrypted += decipher.final("utf8");
4580
- return decrypted;
4581
- }
4582
- function parseEnvFile(content) {
4583
- const entries = [];
4584
- for (const line of content.split("\n")) {
4585
- const trimmed = line.trim();
4586
- if (!trimmed || trimmed.startsWith("#")) continue;
4587
- const eqIndex = trimmed.indexOf("=");
4588
- if (eqIndex === -1) continue;
4589
- const key = trimmed.slice(0, eqIndex).trim();
4590
- let value = trimmed.slice(eqIndex + 1).trim();
4591
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4592
- value = value.slice(1, -1);
4593
- }
4594
- if (key && value) {
4595
- entries.push({ key, value });
4596
- }
4597
- }
4598
- return entries;
4599
- }
4600
- function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
4601
- if (!fs.existsSync(envPath)) {
4602
- throw new Error(`File not found: ${envPath}`);
4603
- }
4604
- const content = fs.readFileSync(envPath, "utf-8");
4605
- const entries = parseEnvFile(content);
4606
- if (entries.length === 0) {
4607
- throw new Error("No environment variables found in file");
4608
- }
4609
- const salt = crypto.randomBytes(SALT_LENGTH);
4610
- const key = deriveKey(passphrase, salt);
4611
- const encryptedEntries = entries.map(({ key: envKey, value }) => {
4612
- if (isSensitiveKey(envKey)) {
4613
- const { iv, encrypted, tag } = encryptValue(value, key);
4614
- return {
4615
- key: envKey,
4616
- value: encrypted,
4617
- encrypted: true,
4618
- iv,
4619
- tag
4620
- };
4621
- }
4622
- return {
4623
- key: envKey,
4624
- value,
4625
- encrypted: false
4626
- };
4627
- });
4628
- const encryptedEnv = {
4629
- version: 1,
4630
- salt: salt.toString("hex"),
4631
- entries: encryptedEntries
4632
- };
4633
- const encPath = envPath + ".enc";
4634
- fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
4635
- if (deleteOriginal) {
4636
- fs.unlinkSync(envPath);
4637
- }
4638
- const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
4639
- const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
4640
- console.log(
4641
- `Encrypted ${sensitiveCount} sensitive values (${plainCount} non-sensitive kept as plaintext)`
4642
- );
4643
- return encPath;
4644
- }
4645
- function decryptEnvFile(encPath, passphrase) {
4646
- if (!fs.existsSync(encPath)) {
4647
- throw new Error(`Encrypted env file not found: ${encPath}`);
4648
- }
4649
- const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
4650
- if (content.version !== 1) {
4651
- throw new Error(`Unsupported encrypted env version: ${content.version}`);
4652
- }
4653
- const salt = Buffer.from(content.salt, "hex");
4654
- const key = deriveKey(passphrase, salt);
4655
- const result = {};
4656
- for (const entry of content.entries) {
4657
- if (entry.encrypted) {
4658
- if (!entry.iv || !entry.tag) {
4659
- throw new Error(`Missing encryption metadata for ${entry.key}`);
4660
- }
4661
- try {
4662
- result[entry.key] = decryptValue(entry.value, key, entry.iv, entry.tag);
4663
- } catch {
4664
- throw new Error(
4665
- `Failed to decrypt ${entry.key}. Wrong passphrase or corrupted data.`
4666
- );
4667
- }
4668
- } else {
4669
- result[entry.key] = entry.value;
4670
- }
4671
- }
4672
- return result;
4673
- }
4674
- function loadSecureEnv(basePath, passphrase) {
4675
- const encPath = path.join(basePath, ".env.enc");
4676
- const envPath = path.join(basePath, ".env");
4677
- if (fs.existsSync(encPath)) {
4678
- if (!passphrase) {
4679
- passphrase = process.env.EXAGENT_PASSPHRASE;
4680
- }
4681
- if (!passphrase) {
4682
- console.warn("");
4683
- console.warn("WARNING: Found .env.enc but no passphrase provided.");
4684
- console.warn(" Set EXAGENT_PASSPHRASE environment variable or");
4685
- console.warn(" pass --passphrase when running the agent.");
4686
- console.warn(" Falling back to plaintext .env file.");
4687
- console.warn("");
4688
- } else {
4689
- const vars = decryptEnvFile(encPath, passphrase);
4690
- for (const [key, value] of Object.entries(vars)) {
4691
- process.env[key] = value;
4692
- }
4693
- return true;
4694
- }
4695
- }
4696
- if (fs.existsSync(envPath)) {
4697
- const content = fs.readFileSync(envPath, "utf-8");
4698
- const entries = parseEnvFile(content);
4699
- const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
4700
- if (sensitiveKeys.length > 0) {
4701
- console.warn("");
4702
- console.warn("WARNING: Sensitive values stored in plaintext .env file:");
4703
- for (const key of sensitiveKeys) {
4704
- console.warn(` - ${key}`);
4705
- }
4706
- console.warn("");
4707
- console.warn(' Run "npx @exagent/agent encrypt" to secure your keys.');
4708
- console.warn("");
4709
- }
4710
- return false;
4711
- }
4712
- return false;
4713
- }
4714
-
4715
- // src/cli.ts
4716
5156
  (0, import_dotenv2.config)();
4717
5157
  var program = new import_commander.Command();
4718
5158
  program.name("exagent").description("Exagent autonomous trading agent").version("0.1.0");