@exagent/agent 0.1.20 → 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
  };
@@ -1150,349 +1686,76 @@ function classifyTradeError(message) {
1150
1686
  };
1151
1687
  }
1152
1688
 
1153
- // src/trading/market.ts
1154
- var import_viem = require("viem");
1155
- var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
1156
- var TOKEN_DECIMALS = {
1157
- // Base Mainnet — Core tokens
1158
- "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
1159
- // USDC
1160
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
1161
- // USDbC
1162
- "0x4200000000000000000000000000000000000006": 18,
1163
- // WETH
1164
- "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
1165
- // DAI
1166
- "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
1167
- // cbETH
1168
- [NATIVE_ETH.toLowerCase()]: 18,
1169
- // Native ETH
1170
- // Base Mainnet — Established tokens
1171
- "0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
1172
- // AERO (Aerodrome)
1173
- "0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
1174
- // BRETT
1175
- "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
1176
- // DEGEN
1177
- "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
1178
- // VIRTUAL
1179
- "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
1180
- // TOSHI
1181
- "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
1182
- // cbBTC
1183
- "0x2416092f143378750bb29b79ed961ab195cceea5": 18,
1184
- // ezETH (Renzo)
1185
- "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
1186
- // wstETH (Lido)
1187
- };
1188
- function getTokenDecimals(address) {
1189
- const decimals = TOKEN_DECIMALS[address.toLowerCase()];
1190
- if (decimals === void 0) {
1191
- console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
1192
- return 18;
1193
- }
1194
- return decimals;
1195
- }
1196
- var TOKEN_TO_COINGECKO = {
1197
- // Core
1198
- "0x4200000000000000000000000000000000000006": "ethereum",
1199
- // WETH
1200
- [NATIVE_ETH.toLowerCase()]: "ethereum",
1201
- "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
1202
- // USDC
1203
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
1204
- // USDbC
1205
- "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
1206
- // cbETH
1207
- "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
1208
- // DAI
1209
- // Established
1210
- "0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
1211
- // AERO
1212
- "0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
1213
- // BRETT
1214
- "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
1215
- // DEGEN
1216
- "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
1217
- // VIRTUAL
1218
- "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
1219
- // TOSHI
1220
- "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
1221
- // cbBTC
1222
- "0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
1223
- // ezETH
1224
- "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
1225
- // wstETH
1226
- };
1227
- var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
1228
- var PRICE_STALENESS_MS = 6e4;
1229
- var MarketDataService = class {
1230
- rpcUrl;
1231
- client;
1232
- /** Cached prices from last fetch */
1233
- cachedPrices = {};
1234
- /** Timestamp of last successful price fetch */
1235
- lastPriceFetchAt = 0;
1236
- constructor(rpcUrl) {
1237
- this.rpcUrl = rpcUrl;
1238
- this.client = (0, import_viem.createPublicClient)({
1239
- transport: (0, import_viem.http)(rpcUrl)
1240
- });
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;
1241
1700
  }
1242
- /** Cached volume data */
1243
- cachedVolume24h = {};
1244
- /** Cached price change data */
1245
- cachedPriceChange24h = {};
1246
1701
  /**
1247
- * Fetch current market data for the agent
1702
+ * Filter signals through risk checks
1703
+ * Returns only signals that pass all guardrails
1248
1704
  */
1249
- async fetchMarketData(walletAddress, tokenAddresses) {
1250
- const prices = await this.fetchPrices(tokenAddresses);
1251
- const balances = await this.fetchBalances(walletAddress, tokenAddresses);
1252
- const portfolioValue = this.calculatePortfolioValue(balances, prices);
1253
- let gasPrice;
1254
- try {
1255
- gasPrice = await this.client.getGasPrice();
1256
- } catch {
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;
1257
1711
  }
1258
- return {
1259
- timestamp: Date.now(),
1260
- prices,
1261
- balances,
1262
- portfolioValue,
1263
- volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
1264
- priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
1265
- gasPrice,
1266
- network: {
1267
- chainId: this.client.chain?.id ?? 8453
1268
- }
1269
- };
1270
- }
1271
- /**
1272
- * Check if cached prices are still fresh
1273
- */
1274
- get pricesAreFresh() {
1275
- return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
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));
1276
1717
  }
1277
1718
  /**
1278
- * Fetch token prices from CoinGecko free API
1279
- * 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.
1280
1722
  */
1281
- async fetchPrices(tokenAddresses) {
1282
- if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
1283
- const prices2 = { ...this.cachedPrices };
1284
- for (const addr of tokenAddresses) {
1285
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1286
- if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
1287
- prices2[addr.toLowerCase()] = 1;
1288
- }
1289
- }
1290
- return prices2;
1723
+ validateSignal(signal, marketData) {
1724
+ if (signal.action === "hold") {
1725
+ return true;
1291
1726
  }
1292
- const prices = {};
1293
- const idsToFetch = /* @__PURE__ */ new Set();
1294
- for (const addr of tokenAddresses) {
1295
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1296
- if (cgId && !STABLECOIN_IDS.has(cgId)) {
1297
- idsToFetch.add(cgId);
1298
- }
1727
+ if (signal.confidence < 0.5) {
1728
+ console.warn(`Signal confidence too low: ${signal.confidence}`);
1729
+ return false;
1299
1730
  }
1300
- idsToFetch.add("ethereum");
1301
- if (idsToFetch.size > 0) {
1302
- try {
1303
- const ids = Array.from(idsToFetch).join(",");
1304
- const response = await fetch(
1305
- `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
1306
- { signal: AbortSignal.timeout(5e3) }
1307
- );
1308
- if (response.ok) {
1309
- const data = await response.json();
1310
- for (const [cgId, priceData] of Object.entries(data)) {
1311
- for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
1312
- if (id === cgId) {
1313
- const key = addr.toLowerCase();
1314
- prices[key] = priceData.usd;
1315
- if (priceData.usd_24h_vol !== void 0) {
1316
- this.cachedVolume24h[key] = priceData.usd_24h_vol;
1317
- }
1318
- if (priceData.usd_24h_change !== void 0) {
1319
- this.cachedPriceChange24h[key] = priceData.usd_24h_change;
1320
- }
1321
- }
1322
- }
1323
- }
1324
- this.lastPriceFetchAt = Date.now();
1325
- } else {
1326
- console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
1327
- }
1328
- } catch (error) {
1329
- console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
1330
- }
1731
+ if (signal.action === "sell") {
1732
+ return true;
1331
1733
  }
1332
- for (const addr of tokenAddresses) {
1333
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1334
- if (cgId && STABLECOIN_IDS.has(cgId)) {
1335
- prices[addr.toLowerCase()] = 1;
1336
- }
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;
1337
1741
  }
1338
- const missingAddrs = tokenAddresses.filter(
1339
- (addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
1340
- );
1341
- if (missingAddrs.length > 0) {
1342
- try {
1343
- const coins = missingAddrs.map((a) => `base:${a}`).join(",");
1344
- const llamaResponse = await fetch(
1345
- `https://coins.llama.fi/prices/current/${coins}`,
1346
- { 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`
1347
1747
  );
1348
- if (llamaResponse.ok) {
1349
- const llamaData = await llamaResponse.json();
1350
- for (const [key, data] of Object.entries(llamaData.coins)) {
1351
- const addr = key.replace("base:", "").toLowerCase();
1352
- if (data.price && data.confidence > 0.5) {
1353
- prices[addr] = data.price;
1354
- }
1355
- }
1356
- if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
1357
- }
1358
- } catch {
1748
+ return false;
1359
1749
  }
1360
1750
  }
1361
- if (Object.keys(prices).length > 0) {
1362
- this.cachedPrices = prices;
1363
- }
1364
- if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
1365
- console.warn("Using cached prices (last successful fetch was stale)");
1366
- return { ...this.cachedPrices };
1367
- }
1368
- for (const addr of tokenAddresses) {
1369
- if (!prices[addr.toLowerCase()]) {
1370
- console.warn(`No price available for ${addr}, using 0`);
1371
- prices[addr.toLowerCase()] = 0;
1372
- }
1751
+ if (signalValue < this.minTradeValueUSD) {
1752
+ console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
1753
+ return false;
1373
1754
  }
1374
- return prices;
1755
+ return true;
1375
1756
  }
1376
1757
  /**
1377
- * Fetch real on-chain balances: native ETH + ERC-20 tokens
1378
- */
1379
- async fetchBalances(walletAddress, tokenAddresses) {
1380
- const balances = {};
1381
- const wallet = walletAddress;
1382
- try {
1383
- const nativeBalance = await this.client.getBalance({ address: wallet });
1384
- balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
1385
- const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
1386
- try {
1387
- const balance = await this.client.readContract({
1388
- address: tokenAddress,
1389
- abi: import_viem.erc20Abi,
1390
- functionName: "balanceOf",
1391
- args: [wallet]
1392
- });
1393
- return { address: tokenAddress.toLowerCase(), balance };
1394
- } catch (error) {
1395
- return { address: tokenAddress.toLowerCase(), balance: 0n };
1396
- }
1397
- });
1398
- const results = await Promise.all(erc20Promises);
1399
- for (const { address, balance } of results) {
1400
- balances[address] = balance;
1401
- }
1402
- } catch (error) {
1403
- console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
1404
- balances[NATIVE_ETH.toLowerCase()] = 0n;
1405
- for (const address of tokenAddresses) {
1406
- balances[address.toLowerCase()] = 0n;
1407
- }
1408
- }
1409
- return balances;
1410
- }
1411
- /**
1412
- * Calculate total portfolio value in USD
1413
- */
1414
- calculatePortfolioValue(balances, prices) {
1415
- let total = 0;
1416
- for (const [address, balance] of Object.entries(balances)) {
1417
- const price = prices[address.toLowerCase()] || 0;
1418
- const decimals = getTokenDecimals(address);
1419
- const amount = Number(balance) / Math.pow(10, decimals);
1420
- total += amount * price;
1421
- }
1422
- return total;
1423
- }
1424
- };
1425
-
1426
- // src/trading/risk.ts
1427
- var RiskManager = class {
1428
- config;
1429
- dailyPnL = 0;
1430
- dailyFees = 0;
1431
- lastResetDate = "";
1432
- /** Minimum trade value in USD — trades below this are rejected as dust */
1433
- minTradeValueUSD;
1434
- constructor(config) {
1435
- this.config = config;
1436
- this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
1437
- }
1438
- /**
1439
- * Filter signals through risk checks
1440
- * Returns only signals that pass all guardrails
1441
- */
1442
- filterSignals(signals, marketData) {
1443
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1444
- if (today !== this.lastResetDate) {
1445
- this.dailyPnL = 0;
1446
- this.dailyFees = 0;
1447
- this.lastResetDate = today;
1448
- }
1449
- if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
1450
- console.warn("Daily loss limit reached - no new trades");
1451
- return [];
1452
- }
1453
- return signals.filter((signal) => this.validateSignal(signal, marketData));
1454
- }
1455
- /**
1456
- * Validate individual signal against risk limits.
1457
- * Sell signals are exempt from position size and minimum value checks —
1458
- * those guardrails prevent oversized/dust buys, but blocking exits traps capital.
1459
- */
1460
- validateSignal(signal, marketData) {
1461
- if (signal.action === "hold") {
1462
- return true;
1463
- }
1464
- if (signal.confidence < 0.5) {
1465
- console.warn(`Signal confidence too low: ${signal.confidence}`);
1466
- return false;
1467
- }
1468
- if (signal.action === "sell") {
1469
- return true;
1470
- }
1471
- const signalValue = this.estimateSignalValue(signal, marketData);
1472
- const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
1473
- if (signalValue > maxPositionValue) {
1474
- console.warn(
1475
- `Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
1476
- );
1477
- return false;
1478
- }
1479
- if (this.config.maxConcurrentPositions) {
1480
- const activePositions = this.countActivePositions(marketData);
1481
- if (activePositions >= this.config.maxConcurrentPositions) {
1482
- console.warn(
1483
- `Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
1484
- );
1485
- return false;
1486
- }
1487
- }
1488
- if (signalValue < this.minTradeValueUSD) {
1489
- console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
1490
- return false;
1491
- }
1492
- return true;
1493
- }
1494
- /**
1495
- * Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
1758
+ * Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
1496
1759
  */
1497
1760
  countActivePositions(marketData) {
1498
1761
  const NATIVE_ETH_KEY = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
@@ -1533,6 +1796,31 @@ var RiskManager = class {
1533
1796
  updateFees(fees) {
1534
1797
  this.dailyFees += fees;
1535
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
+ }
1536
1824
  /**
1537
1825
  * Get current risk status
1538
1826
  * @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
@@ -1994,300 +2282,37 @@ var VaultManager = class {
1994
2282
  };
1995
2283
 
1996
2284
  // src/relay.ts
1997
- var import_ws = __toESM(require("ws"));
1998
- var import_accounts2 = require("viem/accounts");
1999
- var import_sdk = require("@exagent/sdk");
2000
- var RelayClient = class {
2001
- config;
2002
- ws = null;
2003
- authenticated = false;
2004
- authRejected = false;
2005
- reconnectAttempts = 0;
2006
- maxReconnectAttempts = 50;
2007
- reconnectTimer = null;
2008
- heartbeatTimer = null;
2009
- 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();
2010
2293
  constructor(config) {
2011
- this.config = config;
2294
+ this.apiUrl = config.apiUrl;
2012
2295
  }
2013
- /**
2014
- * Connect to the relay server
2015
- */
2016
- async connect() {
2017
- if (this.stopped) return;
2018
- const wsUrl = this.config.relay.apiUrl.replace(/^https?:\/\//, (m) => m.includes("https") ? "wss://" : "ws://").replace(/\/$/, "") + "/ws/agent";
2019
- return new Promise((resolve, reject) => {
2020
- try {
2021
- this.ws = new import_ws.default(wsUrl);
2022
- } catch (error) {
2023
- console.error("Relay: Failed to create WebSocket:", error);
2024
- this.scheduleReconnect();
2025
- reject(error);
2026
- return;
2027
- }
2028
- const connectTimeout = setTimeout(() => {
2029
- if (!this.authenticated) {
2030
- console.error("Relay: Connection timeout");
2031
- this.ws?.close();
2032
- this.scheduleReconnect();
2033
- reject(new Error("Connection timeout"));
2034
- }
2035
- }, 15e3);
2036
- this.ws.on("open", async () => {
2037
- this.authRejected = false;
2038
- console.log("Relay: Connected, authenticating...");
2039
- try {
2040
- await this.authenticate();
2041
- } catch (error) {
2042
- console.error("Relay: Authentication failed:", error);
2043
- this.ws?.close();
2044
- clearTimeout(connectTimeout);
2045
- reject(error);
2046
- }
2047
- });
2048
- this.ws.on("message", (raw) => {
2049
- try {
2050
- const data = JSON.parse(raw.toString());
2051
- this.handleMessage(data);
2052
- if (data.type === "auth_success") {
2053
- clearTimeout(connectTimeout);
2054
- this.authenticated = true;
2055
- this.reconnectAttempts = 0;
2056
- this.startHeartbeat();
2057
- console.log("Relay: Authenticated successfully");
2058
- resolve();
2059
- } else if (data.type === "auth_error") {
2060
- clearTimeout(connectTimeout);
2061
- this.authRejected = true;
2062
- console.error(`Relay: Auth rejected: ${data.message}`);
2063
- reject(new Error(data.message));
2064
- }
2065
- } catch {
2066
- }
2067
- });
2068
- this.ws.on("close", (code, reason) => {
2069
- clearTimeout(connectTimeout);
2070
- this.authenticated = false;
2071
- this.stopHeartbeat();
2072
- if (!this.stopped) {
2073
- if (!this.authRejected) {
2074
- console.log(`Relay: Disconnected (${code}: ${reason.toString() || "unknown"})`);
2075
- }
2076
- this.scheduleReconnect();
2077
- }
2078
- });
2079
- this.ws.on("error", (error) => {
2080
- if (!this.stopped) {
2081
- console.error("Relay: WebSocket error:", error.message);
2082
- }
2083
- });
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);
2084
2306
  });
2307
+ return this.meta;
2085
2308
  }
2086
- /**
2087
- * Authenticate with the relay server using wallet signature
2088
- */
2089
- async authenticate() {
2090
- const account = (0, import_accounts2.privateKeyToAccount)(this.config.privateKey);
2091
- const timestamp = Math.floor(Date.now() / 1e3);
2092
- const message = `ExagentRelay:${this.config.agentId}:${timestamp}`;
2093
- const signature = await (0, import_accounts2.signMessage)({
2094
- message,
2095
- privateKey: this.config.privateKey
2096
- });
2097
- this.send({
2098
- type: "auth",
2099
- agentId: this.config.agentId,
2100
- wallet: account.address,
2101
- timestamp,
2102
- signature,
2103
- sdkVersion: import_sdk.SDK_VERSION
2104
- });
2105
- }
2106
- /**
2107
- * Handle incoming messages from the relay server
2108
- */
2109
- handleMessage(data) {
2110
- switch (data.type) {
2111
- case "command":
2112
- if (data.command && this.config.onCommand) {
2113
- this.config.onCommand(data.command);
2114
- }
2115
- break;
2116
- case "auth_success":
2117
- case "auth_error":
2118
- break;
2119
- case "error":
2120
- console.error(`Relay: Server error: ${data.message}`);
2121
- break;
2122
- }
2123
- }
2124
- /**
2125
- * Send a status heartbeat
2126
- */
2127
- sendHeartbeat(status) {
2128
- if (!this.authenticated) return;
2129
- this.send({
2130
- type: "heartbeat",
2131
- agentId: this.config.agentId,
2132
- status
2133
- });
2134
- }
2135
- /**
2136
- * Send a status update (outside of regular heartbeat)
2137
- */
2138
- sendStatusUpdate(status) {
2139
- if (!this.authenticated) return;
2140
- this.send({
2141
- type: "status_update",
2142
- agentId: this.config.agentId,
2143
- status
2144
- });
2145
- }
2146
- /**
2147
- * Send a message to the command center
2148
- */
2149
- sendMessage(messageType, level, title, body, data) {
2150
- if (!this.authenticated) return;
2151
- this.send({
2152
- type: "message",
2153
- agentId: this.config.agentId,
2154
- messageType,
2155
- level,
2156
- title,
2157
- body,
2158
- data
2159
- });
2160
- }
2161
- /**
2162
- * Send a command execution result
2163
- */
2164
- sendCommandResult(commandId, success, result) {
2165
- if (!this.authenticated) return;
2166
- this.send({
2167
- type: "command_result",
2168
- agentId: this.config.agentId,
2169
- commandId,
2170
- success,
2171
- result
2172
- });
2173
- }
2174
- /**
2175
- * Start the heartbeat timer
2176
- */
2177
- startHeartbeat() {
2178
- this.stopHeartbeat();
2179
- const interval = this.config.relay.heartbeatIntervalMs || 3e4;
2180
- this.heartbeatTimer = setInterval(() => {
2181
- if (this.ws?.readyState === import_ws.default.OPEN) {
2182
- this.ws.ping();
2183
- }
2184
- }, interval);
2185
- }
2186
- /**
2187
- * Stop the heartbeat timer
2188
- */
2189
- stopHeartbeat() {
2190
- if (this.heartbeatTimer) {
2191
- clearInterval(this.heartbeatTimer);
2192
- this.heartbeatTimer = null;
2193
- }
2194
- }
2195
- /**
2196
- * Schedule a reconnection with exponential backoff
2197
- */
2198
- scheduleReconnect() {
2199
- if (this.stopped) return;
2200
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
2201
- console.error("Relay: Max reconnection attempts reached. Giving up.");
2202
- return;
2203
- }
2204
- const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
2205
- this.reconnectAttempts++;
2206
- console.log(
2207
- `Relay: Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
2208
- );
2209
- this.reconnectTimer = setTimeout(() => {
2210
- this.connect().catch(() => {
2211
- });
2212
- }, delay);
2213
- }
2214
- /**
2215
- * Send a JSON message to the WebSocket
2216
- */
2217
- send(data) {
2218
- if (this.ws?.readyState === import_ws.default.OPEN) {
2219
- this.ws.send(JSON.stringify(data));
2220
- }
2221
- }
2222
- /**
2223
- * Check if connected and authenticated
2224
- */
2225
- get isConnected() {
2226
- return this.authenticated && this.ws?.readyState === import_ws.default.OPEN;
2227
- }
2228
- /**
2229
- * Disconnect and stop reconnecting
2230
- */
2231
- disconnect() {
2232
- this.stopped = true;
2233
- this.stopHeartbeat();
2234
- if (this.reconnectTimer) {
2235
- clearTimeout(this.reconnectTimer);
2236
- this.reconnectTimer = null;
2237
- }
2238
- if (this.ws) {
2239
- this.ws.close(1e3, "Agent shutting down");
2240
- this.ws = null;
2241
- }
2242
- this.authenticated = false;
2243
- console.log("Relay: Disconnected");
2244
- }
2245
- };
2246
-
2247
- // src/browser-open.ts
2248
- var import_child_process2 = require("child_process");
2249
- function openBrowser(url) {
2250
- const platform = process.platform;
2251
- try {
2252
- if (platform === "darwin") {
2253
- (0, import_child_process2.exec)(`open "${url}"`);
2254
- } else if (platform === "win32") {
2255
- (0, import_child_process2.exec)(`start "" "${url}"`);
2256
- } else {
2257
- (0, import_child_process2.exec)(`xdg-open "${url}"`);
2258
- }
2259
- } catch {
2260
- }
2261
- }
2262
-
2263
- // src/perp/client.ts
2264
- var HyperliquidClient = class {
2265
- apiUrl;
2266
- meta = null;
2267
- assetIndexCache = /* @__PURE__ */ new Map();
2268
- constructor(config) {
2269
- this.apiUrl = config.apiUrl;
2270
- }
2271
- // ============================================================
2272
- // INFO API (read-only)
2273
- // ============================================================
2274
- /** Fetch perpetuals metadata (asset specs, names, indices) */
2275
- async getMeta() {
2276
- if (this.meta) return this.meta;
2277
- const resp = await this.infoRequest({ type: "meta" });
2278
- this.meta = resp.universe;
2279
- this.meta.forEach((asset, idx) => {
2280
- this.assetIndexCache.set(asset.name, idx);
2281
- });
2282
- return this.meta;
2283
- }
2284
- /** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
2285
- async getAssetIndex(coin) {
2286
- if (this.assetIndexCache.has(coin)) return this.assetIndexCache.get(coin);
2287
- await this.getMeta();
2288
- const idx = this.assetIndexCache.get(coin);
2289
- if (idx === void 0) throw new Error(`Unknown instrument: ${coin}`);
2290
- return idx;
2309
+ /** Get asset index from symbol (e.g. "ETH" -> 1). Caches after first getMeta() call. */
2310
+ async getAssetIndex(coin) {
2311
+ if (this.assetIndexCache.has(coin)) return this.assetIndexCache.get(coin);
2312
+ await this.getMeta();
2313
+ const idx = this.assetIndexCache.get(coin);
2314
+ if (idx === void 0) throw new Error(`Unknown instrument: ${coin}`);
2315
+ return idx;
2291
2316
  }
2292
2317
  /** Get mid-market prices for all perpetuals */
2293
2318
  async getAllMids() {
@@ -2864,7 +2889,7 @@ var PositionManager = class {
2864
2889
  };
2865
2890
 
2866
2891
  // src/perp/websocket.ts
2867
- var import_ws2 = __toESM(require("ws"));
2892
+ var import_ws = __toESM(require("ws"));
2868
2893
  var HyperliquidWebSocket = class {
2869
2894
  wsUrl;
2870
2895
  userAddress;
@@ -2896,14 +2921,14 @@ var HyperliquidWebSocket = class {
2896
2921
  * Connect to Hyperliquid WebSocket and subscribe to user events.
2897
2922
  */
2898
2923
  async connect() {
2899
- if (this.ws?.readyState === import_ws2.default.OPEN || this.isConnecting) {
2924
+ if (this.ws?.readyState === import_ws.default.OPEN || this.isConnecting) {
2900
2925
  return;
2901
2926
  }
2902
2927
  this.isConnecting = true;
2903
2928
  this.shouldReconnect = true;
2904
2929
  return new Promise((resolve, reject) => {
2905
2930
  try {
2906
- this.ws = new import_ws2.default(this.wsUrl);
2931
+ this.ws = new import_ws.default(this.wsUrl);
2907
2932
  this.ws.on("open", () => {
2908
2933
  this.isConnecting = false;
2909
2934
  this.reconnectAttempts = 0;
@@ -2956,7 +2981,7 @@ var HyperliquidWebSocket = class {
2956
2981
  this.stopPing();
2957
2982
  if (this.ws) {
2958
2983
  this.ws.removeAllListeners();
2959
- if (this.ws.readyState === import_ws2.default.OPEN) {
2984
+ if (this.ws.readyState === import_ws.default.OPEN) {
2960
2985
  this.ws.close(1e3, "Client disconnect");
2961
2986
  }
2962
2987
  this.ws = null;
@@ -2967,7 +2992,7 @@ var HyperliquidWebSocket = class {
2967
2992
  * Check if WebSocket is connected.
2968
2993
  */
2969
2994
  get isConnected() {
2970
- return this.ws?.readyState === import_ws2.default.OPEN;
2995
+ return this.ws?.readyState === import_ws.default.OPEN;
2971
2996
  }
2972
2997
  // ============================================================
2973
2998
  // EVENT HANDLERS
@@ -3094,7 +3119,7 @@ var HyperliquidWebSocket = class {
3094
3119
  startPing() {
3095
3120
  this.stopPing();
3096
3121
  this.pingTimer = setInterval(() => {
3097
- if (this.ws?.readyState === import_ws2.default.OPEN) {
3122
+ if (this.ws?.readyState === import_ws.default.OPEN) {
3098
3123
  this.ws.send(JSON.stringify({ method: "ping" }));
3099
3124
  }
3100
3125
  }, 25e3);
@@ -3109,7 +3134,7 @@ var HyperliquidWebSocket = class {
3109
3134
  // HELPERS
3110
3135
  // ============================================================
3111
3136
  subscribe(msg) {
3112
- if (this.ws?.readyState === import_ws2.default.OPEN) {
3137
+ if (this.ws?.readyState === import_ws.default.OPEN) {
3113
3138
  this.ws.send(JSON.stringify(msg));
3114
3139
  }
3115
3140
  }
@@ -3118,7 +3143,7 @@ var HyperliquidWebSocket = class {
3118
3143
  // src/perp/recorder.ts
3119
3144
  var import_viem4 = require("viem");
3120
3145
  var import_chains2 = require("viem/chains");
3121
- var import_accounts3 = require("viem/accounts");
3146
+ var import_accounts2 = require("viem/accounts");
3122
3147
  var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
3123
3148
  var ROUTER_ABI = [
3124
3149
  {
@@ -3157,7 +3182,7 @@ var PerpTradeRecorder = class {
3157
3182
  constructor(opts) {
3158
3183
  this.agentId = opts.agentId;
3159
3184
  this.configHash = opts.configHash;
3160
- this.account = (0, import_accounts3.privateKeyToAccount)(opts.privateKey);
3185
+ this.account = (0, import_accounts2.privateKeyToAccount)(opts.privateKey);
3161
3186
  const rpcUrl = opts.rpcUrl || "https://mainnet.base.org";
3162
3187
  const transport = (0, import_viem4.http)(rpcUrl);
3163
3188
  this.publicClient = (0, import_viem4.createPublicClient)({
@@ -3265,232 +3290,683 @@ var PerpTradeRecorder = class {
3265
3290
  }
3266
3291
  }
3267
3292
  /**
3268
- * 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
3269
3816
  */
3270
- async processRetryQueue() {
3271
- if (this.retryQueue.length === 0) return;
3272
- const now = Date.now();
3273
- const toRetry = this.retryQueue.filter(
3274
- (item) => now - item.lastAttempt >= RETRY_DELAY_MS
3275
- );
3276
- for (const item of toRetry) {
3277
- item.retries++;
3278
- item.lastAttempt = now;
3279
- if (item.retries > MAX_RETRIES) {
3280
- console.error(
3281
- `Perp trade recording permanently failed after ${MAX_RETRIES} retries: ${item.params.instrument} ${item.params.fillId}`
3282
- );
3283
- const idx = this.retryQueue.indexOf(item);
3284
- if (idx >= 0) this.retryQueue.splice(idx, 1);
3285
- continue;
3286
- }
3287
- console.log(
3288
- `Retrying perp trade recording (attempt ${item.retries}/${MAX_RETRIES}): ${item.params.instrument}`
3289
- );
3290
- const result = await this.submitRecord(item.params);
3291
- if (result.success) {
3292
- const idx = this.retryQueue.indexOf(item);
3293
- if (idx >= 0) this.retryQueue.splice(idx, 1);
3294
- }
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;
3295
3830
  }
3296
3831
  }
3297
- // ============================================================
3298
- // CONVERSION HELPERS
3299
- // ============================================================
3300
3832
  /**
3301
- * Calculate notional USD from a fill (6-decimal).
3302
- * notionalUSD = px * sz * 1e6
3833
+ * Send a status heartbeat
3303
3834
  */
3304
- calculateNotionalUSD(fill) {
3305
- const px = parseFloat(fill.px);
3306
- const sz = parseFloat(fill.sz);
3307
- 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
+ });
3308
3842
  }
3309
3843
  /**
3310
- * Calculate fee USD from a fill (6-decimal).
3311
- * feeUSD = fee * 1e6 (fee is already in USD on Hyperliquid)
3844
+ * Send a status update (outside of regular heartbeat)
3312
3845
  */
3313
- calculateFeeUSD(fill) {
3314
- const fee = parseFloat(fill.fee);
3315
- const builderFee = fill.builderFee ? parseFloat(fill.builderFee) : 0;
3316
- return BigInt(Math.round((fee + builderFee) * 1e6));
3317
- }
3318
- };
3319
-
3320
- // src/perp/onboarding.ts
3321
- var PerpOnboarding = class {
3322
- client;
3323
- signer;
3324
- config;
3325
- constructor(client, signer, config) {
3326
- this.client = client;
3327
- this.signer = signer;
3328
- 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
+ });
3329
3853
  }
3330
- // ============================================================
3331
- // BUILDER FEE
3332
- // ============================================================
3333
3854
  /**
3334
- * Check if the user has approved the builder fee.
3335
- * Builder fee must be approved before orders can include builder fees.
3855
+ * Send a message to the command center
3336
3856
  */
3337
- async isBuilderFeeApproved() {
3338
- try {
3339
- const maxFee = await this.client.getMaxBuilderFee(
3340
- this.signer.getAddress(),
3341
- this.config.builderAddress
3342
- );
3343
- return maxFee >= this.config.builderFeeTenthsBps;
3344
- } catch {
3345
- return false;
3346
- }
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
+ });
3347
3868
  }
3348
3869
  /**
3349
- * Approve the builder fee on Hyperliquid.
3350
- * This is a one-time approval per builder address.
3870
+ * Send a command execution result
3351
3871
  */
3352
- async approveBuilderFee() {
3353
- try {
3354
- const action = {
3355
- type: "approveBuilderFee",
3356
- hyperliquidChain: "Mainnet",
3357
- maxFeeRate: `${this.config.builderFeeTenthsBps / 1e4}%`,
3358
- builder: this.config.builderAddress,
3359
- nonce: Number(getNextNonce())
3360
- };
3361
- const { signature } = await this.signer.signApproval(action);
3362
- const resp = await fetch(`${this.config.apiUrl}/exchange`, {
3363
- method: "POST",
3364
- headers: { "Content-Type": "application/json" },
3365
- body: JSON.stringify({
3366
- action,
3367
- signature: {
3368
- r: signature.slice(0, 66),
3369
- s: `0x${signature.slice(66, 130)}`,
3370
- v: parseInt(signature.slice(130, 132), 16)
3371
- },
3372
- nonce: action.nonce,
3373
- vaultAddress: null
3374
- })
3375
- });
3376
- if (!resp.ok) {
3377
- const text = await resp.text();
3378
- console.error(`Builder fee approval failed: ${resp.status} ${text}`);
3379
- return false;
3380
- }
3381
- console.log(`Builder fee approved: ${this.config.builderFeeTenthsBps / 10} bps for ${this.config.builderAddress}`);
3382
- return true;
3383
- } catch (error) {
3384
- const message = error instanceof Error ? error.message : String(error);
3385
- console.error(`Builder fee approval failed: ${message}`);
3386
- return false;
3387
- }
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
+ });
3388
3881
  }
3389
- // ============================================================
3390
- // BALANCE & REQUIREMENTS
3391
- // ============================================================
3392
3882
  /**
3393
- * Check if the user has sufficient USDC balance on Hyperliquid.
3394
- * Returns the account equity in USD.
3883
+ * Start the heartbeat timer
3395
3884
  */
3396
- async checkBalance() {
3397
- try {
3398
- const account = await this.client.getAccountSummary(this.signer.getAddress());
3399
- return {
3400
- hasBalance: account.totalEquity > 0,
3401
- equity: account.totalEquity
3402
- };
3403
- } catch {
3404
- return { hasBalance: false, equity: 0 };
3405
- }
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);
3406
3893
  }
3407
3894
  /**
3408
- * Verify that the agent's risk universe allows perp trading.
3409
- * Perps require risk universe >= 2 (Derivatives or higher).
3895
+ * Stop the heartbeat timer
3410
3896
  */
3411
- verifyRiskUniverse(riskUniverse) {
3412
- if (riskUniverse >= 2) {
3413
- return {
3414
- allowed: true,
3415
- message: `Risk universe ${riskUniverse} allows perp trading`
3416
- };
3897
+ stopHeartbeat() {
3898
+ if (this.heartbeatTimer) {
3899
+ clearInterval(this.heartbeatTimer);
3900
+ this.heartbeatTimer = null;
3417
3901
  }
3418
- return {
3419
- allowed: false,
3420
- message: `Risk universe ${riskUniverse} does not allow perp trading. Perps require Derivatives (2) or higher.`
3421
- };
3422
3902
  }
3423
- // ============================================================
3424
- // FULL ONBOARDING CHECK
3425
- // ============================================================
3426
3903
  /**
3427
- * Run all onboarding checks and return status.
3428
- * Does NOT auto-approve — caller must explicitly approve after review.
3904
+ * Schedule a reconnection with exponential backoff
3429
3905
  */
3430
- async checkOnboardingStatus(riskUniverse) {
3431
- const riskCheck = this.verifyRiskUniverse(riskUniverse);
3432
- const balanceCheck = await this.checkBalance();
3433
- const builderFeeApproved = await this.isBuilderFeeApproved();
3434
- const ready = riskCheck.allowed && balanceCheck.hasBalance && builderFeeApproved;
3435
- return {
3436
- ready,
3437
- riskUniverseOk: riskCheck.allowed,
3438
- riskUniverseMessage: riskCheck.message,
3439
- hasBalance: balanceCheck.hasBalance,
3440
- equity: balanceCheck.equity,
3441
- builderFeeApproved,
3442
- builderAddress: this.config.builderAddress,
3443
- builderFeeBps: this.config.builderFeeTenthsBps / 10
3444
- };
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);
3445
3921
  }
3446
3922
  /**
3447
- * Run full onboarding: check status and auto-approve builder fee if needed.
3448
- * Returns the final status after all actions.
3923
+ * Send a JSON message to the WebSocket
3449
3924
  */
3450
- async onboard(riskUniverse) {
3451
- let status = await this.checkOnboardingStatus(riskUniverse);
3452
- if (!status.riskUniverseOk) {
3453
- console.error(`Perp onboarding blocked: ${status.riskUniverseMessage}`);
3454
- return status;
3455
- }
3456
- if (!status.hasBalance) {
3457
- console.warn("No USDC balance on Hyperliquid \u2014 deposit required before trading");
3458
- return status;
3925
+ send(data) {
3926
+ if (this.ws?.readyState === import_ws2.default.OPEN) {
3927
+ this.ws.send(JSON.stringify(data));
3459
3928
  }
3460
- if (!status.builderFeeApproved) {
3461
- console.log("Approving builder fee...");
3462
- const approved = await this.approveBuilderFee();
3463
- if (approved) {
3464
- status = { ...status, builderFeeApproved: true, ready: true };
3465
- }
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;
3466
3945
  }
3467
- if (status.ready) {
3468
- console.log(`Perp onboarding complete \u2014 equity: $${status.equity.toFixed(2)}`);
3946
+ if (this.ws) {
3947
+ this.ws.close(1e3, "Agent shutting down");
3948
+ this.ws = null;
3469
3949
  }
3470
- return status;
3950
+ this.authenticated = false;
3951
+ console.log("Relay: Disconnected");
3471
3952
  }
3472
3953
  };
3473
3954
 
3474
- // src/perp/funding.ts
3475
- var import_viem5 = require("viem");
3476
- var import_chains3 = require("viem/chains");
3477
- var import_accounts4 = require("viem/accounts");
3478
- var ERC20_ABI = (0, import_viem5.parseAbi)([
3479
- "function approve(address spender, uint256 amount) external returns (bool)",
3480
- "function balanceOf(address account) external view returns (uint256)",
3481
- "function allowance(address owner, address spender) external view returns (uint256)"
3482
- ]);
3483
- var TOKEN_MESSENGER_V2_ABI = (0, import_viem5.parseAbi)([
3484
- "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken) external returns (uint64 nonce)",
3485
- "event DepositForBurn(uint64 indexed nonce, address indexed burnToken, uint256 amount, address indexed depositor, bytes32 mintRecipient, uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller)"
3486
- ]);
3487
- var MESSAGE_TRANSMITTER_V2_ABI = (0, import_viem5.parseAbi)([
3488
- "function receiveMessage(bytes message, bytes attestation) external returns (bool success)",
3489
- "event MessageSent(bytes message)"
3490
- ]);
3491
- var CORE_DEPOSIT_WALLET_ABI = (0, import_viem5.parseAbi)([
3492
- "function deposit(uint256 amount, uint32 destinationDex) external"
3493
- ]);
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
+ }
3494
3970
 
3495
3971
  // src/runtime.ts
3496
3972
  var FUNDS_LOW_THRESHOLD = 5e-3;
@@ -3512,10 +3988,12 @@ var AgentRuntime = class {
3512
3988
  lastCycleAt = 0;
3513
3989
  lastPortfolioValue = 0;
3514
3990
  lastEthBalance = "0";
3991
+ lastPrices = {};
3515
3992
  processAlive = true;
3516
3993
  riskUniverse = 0;
3517
3994
  allowedTokens = /* @__PURE__ */ new Set();
3518
3995
  strategyContext;
3996
+ positionTracker;
3519
3997
  // Perp trading components (null if perp not enabled)
3520
3998
  perpClient = null;
3521
3999
  perpSigner = null;
@@ -3540,7 +4018,7 @@ var AgentRuntime = class {
3540
4018
  */
3541
4019
  async initialize() {
3542
4020
  console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
3543
- this.client = new import_sdk2.ExagentClient({
4021
+ this.client = new import_sdk.ExagentClient({
3544
4022
  privateKey: this.config.privateKey,
3545
4023
  network: this.config.network
3546
4024
  });
@@ -3558,14 +4036,20 @@ var AgentRuntime = class {
3558
4036
  console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
3559
4037
  await this.syncConfigHash();
3560
4038
  this.strategy = await loadStrategy();
4039
+ const store = new FileStore();
3561
4040
  this.strategyContext = {
3562
- store: new FileStore(),
4041
+ store,
3563
4042
  agentId: Number(this.config.agentId),
3564
4043
  walletAddress: this.client.address
3565
4044
  };
4045
+ this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
3566
4046
  this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
3567
4047
  this.riskManager = new RiskManager(this.config.trading);
3568
4048
  this.marketData = new MarketDataService(this.getRpcUrl());
4049
+ const savedRisk = this.positionTracker.getRiskState();
4050
+ if (savedRisk.lastResetDate) {
4051
+ this.riskManager.restoreState(savedRisk);
4052
+ }
3569
4053
  await this.initializeVaultManager();
3570
4054
  await this.initializePerp();
3571
4055
  await this.initializeRelay();
@@ -3751,7 +4235,7 @@ var AgentRuntime = class {
3751
4235
  if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
3752
4236
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
3753
4237
  const nonce = await this.client.registry.getNonce(address);
3754
- const linkMessage = import_sdk2.ExagentRegistry.generateLinkMessage(
4238
+ const linkMessage = import_sdk.ExagentRegistry.generateLinkMessage(
3755
4239
  address,
3756
4240
  agentId,
3757
4241
  nonce
@@ -3842,15 +4326,14 @@ var AgentRuntime = class {
3842
4326
  async syncConfigHash() {
3843
4327
  const agentId = BigInt(this.config.agentId);
3844
4328
  const llmMeta = this.llm.getMetadata();
3845
- this.configHash = import_sdk2.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
4329
+ this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
3846
4330
  console.log(`Config hash: ${this.configHash}`);
3847
4331
  const onChainHash = await this.client.registry.getConfigHash(agentId);
3848
4332
  if (onChainHash !== this.configHash) {
3849
4333
  console.log("Config changed, updating on-chain...");
3850
4334
  try {
3851
4335
  await this.client.registry.updateConfig(agentId, this.configHash);
3852
- const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
3853
- console.log(`Config updated, new epoch started: ${newEpoch}`);
4336
+ console.log(`Config updated on-chain`);
3854
4337
  } catch (error) {
3855
4338
  const message = error instanceof Error ? error.message : String(error);
3856
4339
  if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
@@ -3881,8 +4364,7 @@ var AgentRuntime = class {
3881
4364
  console.log(" ETH detected! Retrying config update...");
3882
4365
  console.log("");
3883
4366
  await this.client.registry.updateConfig(agentId, this.configHash);
3884
- const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
3885
- console.log(`Config updated, new epoch started: ${newEpoch}`);
4367
+ console.log(`Config updated on-chain`);
3886
4368
  return;
3887
4369
  }
3888
4370
  process.stdout.write(".");
@@ -3893,8 +4375,7 @@ var AgentRuntime = class {
3893
4375
  }
3894
4376
  }
3895
4377
  } else {
3896
- const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
3897
- console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
4378
+ console.log("Config hash matches on-chain");
3898
4379
  }
3899
4380
  }
3900
4381
  /**
@@ -4028,6 +4509,10 @@ var AgentRuntime = class {
4028
4509
  }
4029
4510
  if (updated) {
4030
4511
  this.riskManager = new RiskManager(this.config.trading);
4512
+ const savedRiskState = this.positionTracker.getRiskState();
4513
+ if (savedRiskState.lastResetDate) {
4514
+ this.riskManager.restoreState(savedRiskState);
4515
+ }
4031
4516
  console.log("Risk params updated via command center");
4032
4517
  this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
4033
4518
  this.relay?.sendMessage(
@@ -4231,6 +4716,7 @@ var AgentRuntime = class {
4231
4716
  mode: this.mode,
4232
4717
  agentId: String(this.config.agentId),
4233
4718
  wallet: this.client?.address,
4719
+ sdkVersion: AGENT_VERSION,
4234
4720
  cycleCount: this.cycleCount,
4235
4721
  lastCycleAt: this.lastCycleAt,
4236
4722
  tradingIntervalMs: this.config.trading.tradingIntervalMs,
@@ -4259,7 +4745,8 @@ var AgentRuntime = class {
4259
4745
  openPositions: 0,
4260
4746
  effectiveLeverage: 0,
4261
4747
  pendingRecords: this.perpRecorder?.pendingRetries ?? 0
4262
- } : void 0
4748
+ } : void 0,
4749
+ positions: this.positionTracker ? this.positionTracker.getPositionSummary(this.lastPrices) : void 0
4263
4750
  };
4264
4751
  if (this.perpConnected && this.perpPositions && status.perp) {
4265
4752
  this.perpPositions.getAccountSummary().then((account) => {
@@ -4290,14 +4777,19 @@ var AgentRuntime = class {
4290
4777
  const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
4291
4778
  console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
4292
4779
  this.lastPortfolioValue = marketData.portfolioValue;
4780
+ this.lastPrices = marketData.prices;
4293
4781
  const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4294
4782
  this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
4783
+ this.positionTracker.syncBalances(marketData.balances, marketData.prices);
4295
4784
  const fundsOk = this.checkFundsLow(marketData);
4296
4785
  if (!fundsOk) {
4297
4786
  console.warn("Skipping trading cycle \u2014 ETH balance critically low");
4298
4787
  this.sendRelayStatus();
4299
4788
  return;
4300
4789
  }
4790
+ this.strategyContext.positions = this.positionTracker.getPositions();
4791
+ this.strategyContext.tradeHistory = this.positionTracker.getTradeHistory(20);
4792
+ this.strategyContext.positionTracker = this.positionTracker;
4301
4793
  let signals;
4302
4794
  try {
4303
4795
  signals = await this.strategy(marketData, this.llm, this.config, this.strategyContext);
@@ -4363,13 +4855,30 @@ var AgentRuntime = class {
4363
4855
  );
4364
4856
  }
4365
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
+ }
4366
4873
  const postTokens = this.config.allowedTokens || this.getDefaultTokens();
4367
4874
  const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
4368
4875
  const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
4369
4876
  this.riskManager.updatePnL(marketPnL);
4877
+ this.positionTracker.saveRiskState(this.riskManager.exportState());
4370
4878
  if (marketPnL !== 0) {
4371
4879
  console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
4372
4880
  }
4881
+ this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
4373
4882
  this.lastPortfolioValue = postTradeData.portfolioValue;
4374
4883
  const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4375
4884
  this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
@@ -4644,192 +5153,6 @@ var AgentRuntime = class {
4644
5153
 
4645
5154
  // src/cli.ts
4646
5155
  var import_accounts6 = require("viem/accounts");
4647
-
4648
- // src/secure-env.ts
4649
- var crypto = __toESM(require("crypto"));
4650
- var fs = __toESM(require("fs"));
4651
- var path = __toESM(require("path"));
4652
- var ALGORITHM = "aes-256-gcm";
4653
- var PBKDF2_ITERATIONS = 1e5;
4654
- var SALT_LENGTH = 32;
4655
- var IV_LENGTH = 16;
4656
- var KEY_LENGTH = 32;
4657
- var SENSITIVE_PATTERNS = [
4658
- /PRIVATE_KEY$/i,
4659
- /_API_KEY$/i,
4660
- /API_KEY$/i,
4661
- /_SECRET$/i,
4662
- /^OPENAI_API_KEY$/i,
4663
- /^ANTHROPIC_API_KEY$/i,
4664
- /^GOOGLE_AI_API_KEY$/i,
4665
- /^DEEPSEEK_API_KEY$/i,
4666
- /^MISTRAL_API_KEY$/i,
4667
- /^GROQ_API_KEY$/i,
4668
- /^TOGETHER_API_KEY$/i
4669
- ];
4670
- function isSensitiveKey(key) {
4671
- return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
4672
- }
4673
- function deriveKey(passphrase, salt) {
4674
- return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
4675
- }
4676
- function encryptValue(value, key) {
4677
- const iv = crypto.randomBytes(IV_LENGTH);
4678
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
4679
- let encrypted = cipher.update(value, "utf8", "hex");
4680
- encrypted += cipher.final("hex");
4681
- const tag = cipher.getAuthTag();
4682
- return {
4683
- iv: iv.toString("hex"),
4684
- encrypted,
4685
- tag: tag.toString("hex")
4686
- };
4687
- }
4688
- function decryptValue(encrypted, key, iv, tag) {
4689
- const decipher = crypto.createDecipheriv(
4690
- ALGORITHM,
4691
- key,
4692
- Buffer.from(iv, "hex")
4693
- );
4694
- decipher.setAuthTag(Buffer.from(tag, "hex"));
4695
- let decrypted = decipher.update(encrypted, "hex", "utf8");
4696
- decrypted += decipher.final("utf8");
4697
- return decrypted;
4698
- }
4699
- function parseEnvFile(content) {
4700
- const entries = [];
4701
- for (const line of content.split("\n")) {
4702
- const trimmed = line.trim();
4703
- if (!trimmed || trimmed.startsWith("#")) continue;
4704
- const eqIndex = trimmed.indexOf("=");
4705
- if (eqIndex === -1) continue;
4706
- const key = trimmed.slice(0, eqIndex).trim();
4707
- let value = trimmed.slice(eqIndex + 1).trim();
4708
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4709
- value = value.slice(1, -1);
4710
- }
4711
- if (key && value) {
4712
- entries.push({ key, value });
4713
- }
4714
- }
4715
- return entries;
4716
- }
4717
- function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
4718
- if (!fs.existsSync(envPath)) {
4719
- throw new Error(`File not found: ${envPath}`);
4720
- }
4721
- const content = fs.readFileSync(envPath, "utf-8");
4722
- const entries = parseEnvFile(content);
4723
- if (entries.length === 0) {
4724
- throw new Error("No environment variables found in file");
4725
- }
4726
- const salt = crypto.randomBytes(SALT_LENGTH);
4727
- const key = deriveKey(passphrase, salt);
4728
- const encryptedEntries = entries.map(({ key: envKey, value }) => {
4729
- if (isSensitiveKey(envKey)) {
4730
- const { iv, encrypted, tag } = encryptValue(value, key);
4731
- return {
4732
- key: envKey,
4733
- value: encrypted,
4734
- encrypted: true,
4735
- iv,
4736
- tag
4737
- };
4738
- }
4739
- return {
4740
- key: envKey,
4741
- value,
4742
- encrypted: false
4743
- };
4744
- });
4745
- const encryptedEnv = {
4746
- version: 1,
4747
- salt: salt.toString("hex"),
4748
- entries: encryptedEntries
4749
- };
4750
- const encPath = envPath + ".enc";
4751
- fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
4752
- if (deleteOriginal) {
4753
- fs.unlinkSync(envPath);
4754
- }
4755
- const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
4756
- const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
4757
- console.log(
4758
- `Encrypted ${sensitiveCount} sensitive values (${plainCount} non-sensitive kept as plaintext)`
4759
- );
4760
- return encPath;
4761
- }
4762
- function decryptEnvFile(encPath, passphrase) {
4763
- if (!fs.existsSync(encPath)) {
4764
- throw new Error(`Encrypted env file not found: ${encPath}`);
4765
- }
4766
- const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
4767
- if (content.version !== 1) {
4768
- throw new Error(`Unsupported encrypted env version: ${content.version}`);
4769
- }
4770
- const salt = Buffer.from(content.salt, "hex");
4771
- const key = deriveKey(passphrase, salt);
4772
- const result = {};
4773
- for (const entry of content.entries) {
4774
- if (entry.encrypted) {
4775
- if (!entry.iv || !entry.tag) {
4776
- throw new Error(`Missing encryption metadata for ${entry.key}`);
4777
- }
4778
- try {
4779
- result[entry.key] = decryptValue(entry.value, key, entry.iv, entry.tag);
4780
- } catch {
4781
- throw new Error(
4782
- `Failed to decrypt ${entry.key}. Wrong passphrase or corrupted data.`
4783
- );
4784
- }
4785
- } else {
4786
- result[entry.key] = entry.value;
4787
- }
4788
- }
4789
- return result;
4790
- }
4791
- function loadSecureEnv(basePath, passphrase) {
4792
- const encPath = path.join(basePath, ".env.enc");
4793
- const envPath = path.join(basePath, ".env");
4794
- if (fs.existsSync(encPath)) {
4795
- if (!passphrase) {
4796
- passphrase = process.env.EXAGENT_PASSPHRASE;
4797
- }
4798
- if (!passphrase) {
4799
- console.warn("");
4800
- console.warn("WARNING: Found .env.enc but no passphrase provided.");
4801
- console.warn(" Set EXAGENT_PASSPHRASE environment variable or");
4802
- console.warn(" pass --passphrase when running the agent.");
4803
- console.warn(" Falling back to plaintext .env file.");
4804
- console.warn("");
4805
- } else {
4806
- const vars = decryptEnvFile(encPath, passphrase);
4807
- for (const [key, value] of Object.entries(vars)) {
4808
- process.env[key] = value;
4809
- }
4810
- return true;
4811
- }
4812
- }
4813
- if (fs.existsSync(envPath)) {
4814
- const content = fs.readFileSync(envPath, "utf-8");
4815
- const entries = parseEnvFile(content);
4816
- const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
4817
- if (sensitiveKeys.length > 0) {
4818
- console.warn("");
4819
- console.warn("WARNING: Sensitive values stored in plaintext .env file:");
4820
- for (const key of sensitiveKeys) {
4821
- console.warn(` - ${key}`);
4822
- }
4823
- console.warn("");
4824
- console.warn(' Run "npx @exagent/agent encrypt" to secure your keys.');
4825
- console.warn("");
4826
- }
4827
- return false;
4828
- }
4829
- return false;
4830
- }
4831
-
4832
- // src/cli.ts
4833
5156
  (0, import_dotenv2.config)();
4834
5157
  var program = new import_commander.Command();
4835
5158
  program.name("exagent").description("Exagent autonomous trading agent").version("0.1.0");