@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/index.js CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AGENT_VERSION: () => AGENT_VERSION,
33
34
  AgentConfigSchema: () => AgentConfigSchema,
34
35
  AgentRuntime: () => AgentRuntime,
35
36
  AnthropicAdapter: () => AnthropicAdapter,
@@ -53,6 +54,7 @@ __export(index_exports, {
53
54
  PerpOnboarding: () => PerpOnboarding,
54
55
  PerpTradeRecorder: () => PerpTradeRecorder,
55
56
  PositionManager: () => PositionManager,
57
+ PositionTracker: () => PositionTracker,
56
58
  RelayClient: () => RelayClient,
57
59
  RelayConfigSchema: () => RelayConfigSchema,
58
60
  RiskManager: () => RiskManager,
@@ -82,11 +84,547 @@ __export(index_exports, {
82
84
  module.exports = __toCommonJS(index_exports);
83
85
 
84
86
  // src/runtime.ts
85
- var import_sdk2 = require("@exagent/sdk");
87
+ var import_sdk = require("@exagent/sdk");
86
88
  var import_viem6 = require("viem");
87
89
  var import_chains4 = require("viem/chains");
88
90
  var import_accounts5 = require("viem/accounts");
89
91
 
92
+ // src/trading/market.ts
93
+ var import_viem = require("viem");
94
+ var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
95
+ var TOKEN_DECIMALS = {
96
+ // Base Mainnet — Core tokens
97
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
98
+ // USDC
99
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
100
+ // USDbC
101
+ "0x4200000000000000000000000000000000000006": 18,
102
+ // WETH
103
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
104
+ // DAI
105
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
106
+ // cbETH
107
+ [NATIVE_ETH.toLowerCase()]: 18,
108
+ // Native ETH
109
+ // Base Mainnet — Established tokens
110
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
111
+ // AERO (Aerodrome)
112
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
113
+ // BRETT
114
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
115
+ // DEGEN
116
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
117
+ // VIRTUAL
118
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
119
+ // TOSHI
120
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
121
+ // cbBTC
122
+ "0x2416092f143378750bb29b79ed961ab195cceea5": 18,
123
+ // ezETH (Renzo)
124
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
125
+ // wstETH (Lido)
126
+ };
127
+ function getTokenDecimals(address) {
128
+ const decimals = TOKEN_DECIMALS[address.toLowerCase()];
129
+ if (decimals === void 0) {
130
+ console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
131
+ return 18;
132
+ }
133
+ return decimals;
134
+ }
135
+ var TOKEN_TO_COINGECKO = {
136
+ // Core
137
+ "0x4200000000000000000000000000000000000006": "ethereum",
138
+ // WETH
139
+ [NATIVE_ETH.toLowerCase()]: "ethereum",
140
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
141
+ // USDC
142
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
143
+ // USDbC
144
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
145
+ // cbETH
146
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
147
+ // DAI
148
+ // Established
149
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
150
+ // AERO
151
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
152
+ // BRETT
153
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
154
+ // DEGEN
155
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
156
+ // VIRTUAL
157
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
158
+ // TOSHI
159
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
160
+ // cbBTC
161
+ "0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
162
+ // ezETH
163
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
164
+ // wstETH
165
+ };
166
+ var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
167
+ var PRICE_STALENESS_MS = 6e4;
168
+ var MarketDataService = class {
169
+ rpcUrl;
170
+ client;
171
+ /** Cached prices from last fetch */
172
+ cachedPrices = {};
173
+ /** Timestamp of last successful price fetch */
174
+ lastPriceFetchAt = 0;
175
+ constructor(rpcUrl) {
176
+ this.rpcUrl = rpcUrl;
177
+ this.client = (0, import_viem.createPublicClient)({
178
+ transport: (0, import_viem.http)(rpcUrl)
179
+ });
180
+ }
181
+ /** Cached volume data */
182
+ cachedVolume24h = {};
183
+ /** Cached price change data */
184
+ cachedPriceChange24h = {};
185
+ /**
186
+ * Fetch current market data for the agent
187
+ */
188
+ async fetchMarketData(walletAddress, tokenAddresses) {
189
+ const prices = await this.fetchPrices(tokenAddresses);
190
+ const balances = await this.fetchBalances(walletAddress, tokenAddresses);
191
+ const portfolioValue = this.calculatePortfolioValue(balances, prices);
192
+ let gasPrice;
193
+ try {
194
+ gasPrice = await this.client.getGasPrice();
195
+ } catch {
196
+ }
197
+ return {
198
+ timestamp: Date.now(),
199
+ prices,
200
+ balances,
201
+ portfolioValue,
202
+ volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
203
+ priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
204
+ gasPrice,
205
+ network: {
206
+ chainId: this.client.chain?.id ?? 8453
207
+ }
208
+ };
209
+ }
210
+ /**
211
+ * Check if cached prices are still fresh
212
+ */
213
+ get pricesAreFresh() {
214
+ return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
215
+ }
216
+ /**
217
+ * Fetch token prices from CoinGecko free API
218
+ * Returns cached prices if still fresh (<60s old)
219
+ */
220
+ async fetchPrices(tokenAddresses) {
221
+ if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
222
+ const prices2 = { ...this.cachedPrices };
223
+ for (const addr of tokenAddresses) {
224
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
225
+ if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
226
+ prices2[addr.toLowerCase()] = 1;
227
+ }
228
+ }
229
+ return prices2;
230
+ }
231
+ const prices = {};
232
+ const idsToFetch = /* @__PURE__ */ new Set();
233
+ for (const addr of tokenAddresses) {
234
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
235
+ if (cgId && !STABLECOIN_IDS.has(cgId)) {
236
+ idsToFetch.add(cgId);
237
+ }
238
+ }
239
+ idsToFetch.add("ethereum");
240
+ if (idsToFetch.size > 0) {
241
+ try {
242
+ const ids = Array.from(idsToFetch).join(",");
243
+ const response = await fetch(
244
+ `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
245
+ { signal: AbortSignal.timeout(5e3) }
246
+ );
247
+ if (response.ok) {
248
+ const data = await response.json();
249
+ for (const [cgId, priceData] of Object.entries(data)) {
250
+ for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
251
+ if (id === cgId) {
252
+ const key = addr.toLowerCase();
253
+ prices[key] = priceData.usd;
254
+ if (priceData.usd_24h_vol !== void 0) {
255
+ this.cachedVolume24h[key] = priceData.usd_24h_vol;
256
+ }
257
+ if (priceData.usd_24h_change !== void 0) {
258
+ this.cachedPriceChange24h[key] = priceData.usd_24h_change;
259
+ }
260
+ }
261
+ }
262
+ }
263
+ this.lastPriceFetchAt = Date.now();
264
+ } else {
265
+ console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
266
+ }
267
+ } catch (error) {
268
+ console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
269
+ }
270
+ }
271
+ for (const addr of tokenAddresses) {
272
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
273
+ if (cgId && STABLECOIN_IDS.has(cgId)) {
274
+ prices[addr.toLowerCase()] = 1;
275
+ }
276
+ }
277
+ const missingAddrs = tokenAddresses.filter(
278
+ (addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
279
+ );
280
+ if (missingAddrs.length > 0) {
281
+ try {
282
+ const coins = missingAddrs.map((a) => `base:${a}`).join(",");
283
+ const llamaResponse = await fetch(
284
+ `https://coins.llama.fi/prices/current/${coins}`,
285
+ { signal: AbortSignal.timeout(5e3) }
286
+ );
287
+ if (llamaResponse.ok) {
288
+ const llamaData = await llamaResponse.json();
289
+ for (const [key, data] of Object.entries(llamaData.coins)) {
290
+ const addr = key.replace("base:", "").toLowerCase();
291
+ if (data.price && data.confidence > 0.5) {
292
+ prices[addr] = data.price;
293
+ }
294
+ }
295
+ if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
296
+ }
297
+ } catch {
298
+ }
299
+ }
300
+ if (Object.keys(prices).length > 0) {
301
+ this.cachedPrices = prices;
302
+ }
303
+ if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
304
+ console.warn("Using cached prices (last successful fetch was stale)");
305
+ return { ...this.cachedPrices };
306
+ }
307
+ for (const addr of tokenAddresses) {
308
+ if (!prices[addr.toLowerCase()]) {
309
+ console.warn(`No price available for ${addr}, using 0`);
310
+ prices[addr.toLowerCase()] = 0;
311
+ }
312
+ }
313
+ return prices;
314
+ }
315
+ /**
316
+ * Fetch real on-chain balances: native ETH + ERC-20 tokens
317
+ */
318
+ async fetchBalances(walletAddress, tokenAddresses) {
319
+ const balances = {};
320
+ const wallet = walletAddress;
321
+ try {
322
+ const nativeBalance = await this.client.getBalance({ address: wallet });
323
+ balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
324
+ const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
325
+ try {
326
+ const balance = await this.client.readContract({
327
+ address: tokenAddress,
328
+ abi: import_viem.erc20Abi,
329
+ functionName: "balanceOf",
330
+ args: [wallet]
331
+ });
332
+ return { address: tokenAddress.toLowerCase(), balance };
333
+ } catch (error) {
334
+ return { address: tokenAddress.toLowerCase(), balance: 0n };
335
+ }
336
+ });
337
+ const results = await Promise.all(erc20Promises);
338
+ for (const { address, balance } of results) {
339
+ balances[address] = balance;
340
+ }
341
+ } catch (error) {
342
+ console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
343
+ balances[NATIVE_ETH.toLowerCase()] = 0n;
344
+ for (const address of tokenAddresses) {
345
+ balances[address.toLowerCase()] = 0n;
346
+ }
347
+ }
348
+ return balances;
349
+ }
350
+ /**
351
+ * Calculate total portfolio value in USD
352
+ */
353
+ calculatePortfolioValue(balances, prices) {
354
+ let total = 0;
355
+ for (const [address, balance] of Object.entries(balances)) {
356
+ const price = prices[address.toLowerCase()] || 0;
357
+ const decimals = getTokenDecimals(address);
358
+ const amount = Number(balance) / Math.pow(10, decimals);
359
+ total += amount * price;
360
+ }
361
+ return total;
362
+ }
363
+ };
364
+
365
+ // src/position-tracker.ts
366
+ var BASE_ASSETS = /* @__PURE__ */ new Set([
367
+ NATIVE_ETH.toLowerCase(),
368
+ // Native ETH
369
+ "0x4200000000000000000000000000000000000006",
370
+ // WETH
371
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
372
+ // USDC
373
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca",
374
+ // USDbC
375
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb",
376
+ // DAI
377
+ "0xfde4c96c8593536e31f229ea8f37b2ada2699bb2",
378
+ // USDT
379
+ "0x60a3e35cc302bfa44cb36dc100b2587cd09b9c83"
380
+ // EURC
381
+ ]);
382
+ var TOKEN_SYMBOLS = {
383
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": "AERO",
384
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": "BRETT",
385
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "DEGEN",
386
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "VIRTUAL",
387
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "TOSHI",
388
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "cbBTC",
389
+ "0x2416092f143378750bb29b79ed961ab195cceea5": "ezETH",
390
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wstETH",
391
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "cbETH",
392
+ "0x13403fb738c97cf7564f279288468c140aaed05c": "EXA"
393
+ };
394
+ var KEY_POSITIONS = "__positions";
395
+ var KEY_TRADE_HISTORY = "__trade_history";
396
+ var KEY_RISK_STATE = "__risk_state";
397
+ var PositionTracker = class {
398
+ store;
399
+ positions;
400
+ tradeHistory;
401
+ maxTradeHistory;
402
+ constructor(store, options) {
403
+ this.store = store;
404
+ this.maxTradeHistory = options?.maxTradeHistory ?? 50;
405
+ this.positions = store.get(KEY_POSITIONS) || {};
406
+ this.tradeHistory = store.get(KEY_TRADE_HISTORY) || [];
407
+ const posCount = Object.keys(this.positions).length;
408
+ if (posCount > 0 || this.tradeHistory.length > 0) {
409
+ console.log(`Position tracker loaded: ${posCount} positions, ${this.tradeHistory.length} trade records`);
410
+ }
411
+ }
412
+ // ============================================================
413
+ // TRADE RECORDING (called by runtime after execution)
414
+ // ============================================================
415
+ /**
416
+ * Record a trade result. On buy: creates/updates position with cost-basis
417
+ * weighted average. On sell: calculates realized PnL and removes if fully sold.
418
+ */
419
+ recordTrade(params) {
420
+ const { action, tokenIn, tokenOut, amountIn, priceIn, priceOut, txHash, reasoning, success } = params;
421
+ const decimalsIn = getTokenDecimals(tokenIn);
422
+ const amountInUnits = Number(amountIn) / Math.pow(10, decimalsIn);
423
+ const tradeValueUSD = amountInUnits * priceIn;
424
+ const record = {
425
+ timestamp: Date.now(),
426
+ action,
427
+ tokenIn: tokenIn.toLowerCase(),
428
+ tokenOut: tokenOut.toLowerCase(),
429
+ amountIn: amountIn.toString(),
430
+ priceUSD: tradeValueUSD,
431
+ txHash,
432
+ reasoning,
433
+ success
434
+ };
435
+ if (success) {
436
+ if (action === "buy") {
437
+ this.handleBuy(tokenOut.toLowerCase(), tradeValueUSD, priceOut, txHash);
438
+ } else if (action === "sell") {
439
+ const realizedPnL = this.handleSell(tokenIn.toLowerCase(), tradeValueUSD);
440
+ record.realizedPnL = realizedPnL;
441
+ }
442
+ }
443
+ this.tradeHistory.unshift(record);
444
+ if (this.tradeHistory.length > this.maxTradeHistory) {
445
+ this.tradeHistory = this.tradeHistory.slice(0, this.maxTradeHistory);
446
+ }
447
+ this.persist();
448
+ }
449
+ /**
450
+ * Handle a buy: create or update position with cost-basis weighted average.
451
+ */
452
+ handleBuy(token, costUSD, priceUSD, txHash) {
453
+ if (BASE_ASSETS.has(token) || priceUSD <= 0 || costUSD <= 0) return;
454
+ const existing = this.positions[token];
455
+ const acquiredAmount = costUSD / priceUSD;
456
+ if (existing) {
457
+ const newTotalCost = existing.totalCostBasis + costUSD;
458
+ const newTotalAmount = existing.totalAmountAcquired + acquiredAmount;
459
+ existing.averageEntryPrice = newTotalAmount > 0 ? newTotalCost / newTotalAmount : priceUSD;
460
+ existing.totalCostBasis = newTotalCost;
461
+ existing.totalAmountAcquired = newTotalAmount;
462
+ existing.lastUpdateTimestamp = Date.now();
463
+ if (txHash) {
464
+ existing.txHashes.push(txHash);
465
+ if (existing.txHashes.length > 10) existing.txHashes.shift();
466
+ }
467
+ } else {
468
+ this.positions[token] = {
469
+ token,
470
+ symbol: TOKEN_SYMBOLS[token],
471
+ entryPrice: priceUSD,
472
+ averageEntryPrice: priceUSD,
473
+ totalCostBasis: costUSD,
474
+ totalAmountAcquired: acquiredAmount,
475
+ currentAmount: acquiredAmount,
476
+ entryTimestamp: Date.now(),
477
+ lastUpdateTimestamp: Date.now(),
478
+ txHashes: txHash ? [txHash] : []
479
+ };
480
+ }
481
+ }
482
+ /**
483
+ * Handle a sell: calculate realized PnL and remove position if fully sold.
484
+ * Returns the realized PnL in USD.
485
+ */
486
+ handleSell(token, saleValueUSD) {
487
+ if (BASE_ASSETS.has(token)) return 0;
488
+ const position = this.positions[token];
489
+ if (!position || position.averageEntryPrice <= 0) return 0;
490
+ const estimatedUnitsSold = position.currentAmount > 0 ? Math.min(position.currentAmount, saleValueUSD / position.averageEntryPrice) : saleValueUSD / position.averageEntryPrice;
491
+ const costBasisOfSold = estimatedUnitsSold * position.averageEntryPrice;
492
+ const realizedPnL = saleValueUSD - costBasisOfSold;
493
+ position.totalAmountAcquired = Math.max(0, position.totalAmountAcquired - estimatedUnitsSold);
494
+ position.totalCostBasis = Math.max(0, position.totalCostBasis - costBasisOfSold);
495
+ position.lastUpdateTimestamp = Date.now();
496
+ return realizedPnL;
497
+ }
498
+ // ============================================================
499
+ // BALANCE SYNC (called by runtime each cycle)
500
+ // ============================================================
501
+ /**
502
+ * Sync tracked positions with on-chain balances.
503
+ * Updates currentAmount, detects new tokens (airdrops), removes zeroed positions.
504
+ */
505
+ syncBalances(balances, prices) {
506
+ let changed = false;
507
+ for (const [address, balance] of Object.entries(balances)) {
508
+ const token = address.toLowerCase();
509
+ if (BASE_ASSETS.has(token)) continue;
510
+ const decimals = getTokenDecimals(token);
511
+ const amount = Number(balance) / Math.pow(10, decimals);
512
+ if (amount > 0) {
513
+ if (this.positions[token]) {
514
+ if (this.positions[token].currentAmount !== amount) {
515
+ this.positions[token].currentAmount = amount;
516
+ this.positions[token].lastUpdateTimestamp = Date.now();
517
+ changed = true;
518
+ }
519
+ } else {
520
+ const price = prices[token] || 0;
521
+ this.positions[token] = {
522
+ token,
523
+ symbol: TOKEN_SYMBOLS[token],
524
+ entryPrice: price,
525
+ averageEntryPrice: price,
526
+ totalCostBasis: amount * price,
527
+ totalAmountAcquired: amount,
528
+ currentAmount: amount,
529
+ entryTimestamp: Date.now(),
530
+ lastUpdateTimestamp: Date.now(),
531
+ txHashes: []
532
+ };
533
+ if (price > 0) {
534
+ console.log(`Position tracker: detected new holding ${TOKEN_SYMBOLS[token] || token.slice(0, 10)} (${amount.toFixed(4)} units @ $${price.toFixed(4)})`);
535
+ }
536
+ changed = true;
537
+ }
538
+ } else if (this.positions[token]) {
539
+ delete this.positions[token];
540
+ changed = true;
541
+ }
542
+ }
543
+ if (changed) {
544
+ this.persist();
545
+ }
546
+ }
547
+ // ============================================================
548
+ // QUERY METHODS (for strategies)
549
+ // ============================================================
550
+ /** Get all tracked positions */
551
+ getPositions() {
552
+ return Object.values(this.positions);
553
+ }
554
+ /** Get a single position by token address */
555
+ getPosition(token) {
556
+ return this.positions[token.toLowerCase()];
557
+ }
558
+ /** Get trade history (newest first) */
559
+ getTradeHistory(limit) {
560
+ return limit ? this.tradeHistory.slice(0, limit) : [...this.tradeHistory];
561
+ }
562
+ /** Get unrealized PnL per position given current prices */
563
+ getUnrealizedPnL(prices) {
564
+ const pnl = {};
565
+ for (const pos of Object.values(this.positions)) {
566
+ const currentPrice = prices[pos.token] || 0;
567
+ if (currentPrice > 0 && pos.averageEntryPrice > 0 && pos.currentAmount > 0) {
568
+ const currentValue = pos.currentAmount * currentPrice;
569
+ const costBasis = pos.currentAmount * pos.averageEntryPrice;
570
+ pnl[pos.token] = currentValue - costBasis;
571
+ }
572
+ }
573
+ return pnl;
574
+ }
575
+ /** Get total unrealized PnL across all positions */
576
+ getTotalUnrealizedPnL(prices) {
577
+ const pnl = this.getUnrealizedPnL(prices);
578
+ return Object.values(pnl).reduce((sum, v) => sum + v, 0);
579
+ }
580
+ // ============================================================
581
+ // RISK STATE PERSISTENCE
582
+ // ============================================================
583
+ /** Load persisted risk state */
584
+ getRiskState() {
585
+ return this.store.get(KEY_RISK_STATE) || {
586
+ dailyPnL: 0,
587
+ dailyFees: 0,
588
+ lastResetDate: ""
589
+ };
590
+ }
591
+ /** Save risk state to persistent store */
592
+ saveRiskState(state) {
593
+ this.store.set(KEY_RISK_STATE, state);
594
+ }
595
+ // ============================================================
596
+ // RELAY SUMMARY
597
+ // ============================================================
598
+ /** Get a compact summary for relay heartbeats */
599
+ getPositionSummary(prices) {
600
+ const unrealizedPnL = this.getUnrealizedPnL(prices);
601
+ const now = Date.now();
602
+ const topPositions = Object.values(this.positions).map((pos) => ({
603
+ token: pos.token,
604
+ symbol: pos.symbol,
605
+ unrealizedPnL: unrealizedPnL[pos.token] || 0,
606
+ holdingDuration: now - pos.entryTimestamp
607
+ })).sort((a, b) => Math.abs(b.unrealizedPnL) - Math.abs(a.unrealizedPnL)).slice(0, 5);
608
+ const oneDayAgo = now - 24 * 60 * 60 * 1e3;
609
+ const recentTrades = this.tradeHistory.filter((t) => t.timestamp > oneDayAgo).length;
610
+ const totalRealizedPnL = this.tradeHistory.filter((t) => t.realizedPnL !== void 0).reduce((sum, t) => sum + (t.realizedPnL || 0), 0);
611
+ return {
612
+ openPositions: Object.keys(this.positions).length,
613
+ totalUnrealizedPnL: Object.values(unrealizedPnL).reduce((s, v) => s + v, 0),
614
+ topPositions,
615
+ recentTrades,
616
+ totalRealizedPnL
617
+ };
618
+ }
619
+ // ============================================================
620
+ // INTERNAL
621
+ // ============================================================
622
+ persist() {
623
+ this.store.set(KEY_POSITIONS, this.positions);
624
+ this.store.set(KEY_TRADE_HISTORY, this.tradeHistory);
625
+ }
626
+ };
627
+
90
628
  // src/llm/openai.ts
91
629
  var import_openai = __toESM(require("openai"));
92
630
 
@@ -1012,502 +1550,229 @@ function loadConfig(configPath) {
1012
1550
  if (process.env.OPENAI_API_KEY && config.llm.provider === "openai") {
1013
1551
  llmConfig.apiKey = process.env.OPENAI_API_KEY;
1014
1552
  }
1015
- if (process.env.ANTHROPIC_API_KEY && config.llm.provider === "anthropic") {
1016
- llmConfig.apiKey = process.env.ANTHROPIC_API_KEY;
1017
- }
1018
- if (process.env.GOOGLE_AI_API_KEY && config.llm.provider === "google") {
1019
- llmConfig.apiKey = process.env.GOOGLE_AI_API_KEY;
1020
- }
1021
- if (process.env.DEEPSEEK_API_KEY && config.llm.provider === "deepseek") {
1022
- llmConfig.apiKey = process.env.DEEPSEEK_API_KEY;
1023
- }
1024
- if (process.env.MISTRAL_API_KEY && config.llm.provider === "mistral") {
1025
- llmConfig.apiKey = process.env.MISTRAL_API_KEY;
1026
- }
1027
- if (process.env.GROQ_API_KEY && config.llm.provider === "groq") {
1028
- llmConfig.apiKey = process.env.GROQ_API_KEY;
1029
- }
1030
- if (process.env.TOGETHER_API_KEY && config.llm.provider === "together") {
1031
- llmConfig.apiKey = process.env.TOGETHER_API_KEY;
1032
- }
1033
- if (process.env.EXAGENT_LLM_URL) {
1034
- llmConfig.endpoint = process.env.EXAGENT_LLM_URL;
1035
- }
1036
- if (process.env.EXAGENT_LLM_MODEL) {
1037
- llmConfig.model = process.env.EXAGENT_LLM_MODEL;
1038
- }
1039
- const network = process.env.EXAGENT_NETWORK || config.network;
1040
- return {
1041
- ...config,
1042
- llm: llmConfig,
1043
- network,
1044
- privateKey: privateKey || ""
1045
- };
1046
- }
1047
- function validateConfig(config) {
1048
- if (!config.privateKey) {
1049
- throw new Error("Private key is required");
1050
- }
1051
- if (config.llm.provider !== "ollama" && !config.llm.apiKey) {
1052
- throw new Error(`API key required for ${config.llm.provider} provider`);
1053
- }
1054
- if (config.llm.provider === "ollama" && !config.llm.endpoint) {
1055
- config.llm.endpoint = "http://localhost:11434";
1056
- }
1057
- if (config.llm.provider === "custom" && !config.llm.endpoint) {
1058
- throw new Error("Endpoint required for custom LLM provider");
1059
- }
1060
- if (!config.agentId || Number(config.agentId) <= 0) {
1061
- throw new Error("Valid agent ID required");
1062
- }
1063
- }
1064
- var DEFAULT_RPC_URL = "https://base-rpc.publicnode.com";
1065
- function getRpcUrl() {
1066
- return process.env.BASE_RPC_URL || process.env.EXAGENT_RPC_URL || DEFAULT_RPC_URL;
1067
- }
1068
- function createSampleConfig(agentId, name) {
1069
- return {
1070
- agentId,
1071
- name,
1072
- network: "mainnet",
1073
- llm: {
1074
- provider: "openai",
1075
- model: "gpt-4.1",
1076
- temperature: 0.7,
1077
- maxTokens: 4096
1078
- },
1079
- riskUniverse: "established",
1080
- trading: {
1081
- timeHorizon: "swing",
1082
- maxPositionSizeBps: 1e3,
1083
- maxDailyLossBps: 500,
1084
- maxConcurrentPositions: 5,
1085
- tradingIntervalMs: 6e4,
1086
- maxSlippageBps: 100,
1087
- minTradeValueUSD: 1
1088
- },
1089
- vault: {
1090
- // Default to manual - user must explicitly enable auto-creation
1091
- policy: "manual",
1092
- // Will use agent name for vault name if not set
1093
- preferVaultTrading: true
1094
- }
1095
- };
1096
- }
1097
-
1098
- // src/trading/executor.ts
1099
- var TradeExecutor = class {
1100
- client;
1101
- config;
1102
- allowedTokens;
1103
- configHashFn;
1104
- constructor(client, config, configHashFn) {
1105
- this.client = client;
1106
- this.config = config;
1107
- this.configHashFn = configHashFn;
1108
- this.allowedTokens = new Set(
1109
- (config.allowedTokens || []).map((t) => t.toLowerCase())
1110
- );
1111
- }
1112
- /**
1113
- * Execute a single trade signal
1114
- */
1115
- async execute(signal) {
1116
- if (signal.action === "hold") {
1117
- return { success: true };
1118
- }
1119
- try {
1120
- console.log(`Executing ${signal.action}: ${signal.tokenIn} -> ${signal.tokenOut}`);
1121
- console.log(`Amount: ${signal.amountIn.toString()}, Confidence: ${signal.confidence}`);
1122
- if (!this.validateSignal(signal)) {
1123
- return { success: false, error: "Signal exceeds position limits" };
1124
- }
1125
- const configHash = this.configHashFn?.();
1126
- const result = await this.client.trade({
1127
- tokenIn: signal.tokenIn,
1128
- tokenOut: signal.tokenOut,
1129
- amountIn: signal.amountIn,
1130
- maxSlippageBps: this.config.trading?.maxSlippageBps ?? 100,
1131
- ...configHash && { configHash }
1132
- });
1133
- console.log(`Trade executed: ${result.hash}`);
1134
- return { success: true, txHash: result.hash };
1135
- } catch (error) {
1136
- const message = error instanceof Error ? error.message : "Unknown error";
1137
- const classified = classifyTradeError(message);
1138
- console.error(`Trade failed (${classified.category}): ${classified.userMessage}`);
1139
- return { success: false, error: classified.userMessage };
1140
- }
1141
- }
1142
- /**
1143
- * Execute multiple trade signals
1144
- * Returns results for each signal
1145
- */
1146
- async executeAll(signals) {
1147
- const results = [];
1148
- for (const signal of signals) {
1149
- const result = await this.execute(signal);
1150
- results.push({ signal, ...result });
1151
- if (signals.indexOf(signal) < signals.length - 1) {
1152
- await this.delay(1e3);
1153
- }
1154
- }
1155
- return results;
1156
- }
1157
- /**
1158
- * Validate a signal against config limits and token restrictions
1159
- */
1160
- validateSignal(signal) {
1161
- if (signal.confidence < 0.5) {
1162
- console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
1163
- return false;
1164
- }
1165
- if (this.allowedTokens.size > 0) {
1166
- const tokenInAllowed = this.allowedTokens.has(signal.tokenIn.toLowerCase());
1167
- const tokenOutAllowed = this.allowedTokens.has(signal.tokenOut.toLowerCase());
1168
- if (!tokenInAllowed) {
1169
- console.warn(`Token ${signal.tokenIn} not in allowed list for this agent's risk universe \u2014 skipping`);
1170
- return false;
1171
- }
1172
- if (!tokenOutAllowed) {
1173
- console.warn(`Token ${signal.tokenOut} not in allowed list for this agent's risk universe \u2014 skipping`);
1174
- return false;
1175
- }
1176
- }
1177
- return true;
1178
- }
1179
- delay(ms) {
1180
- return new Promise((resolve) => setTimeout(resolve, ms));
1181
- }
1182
- };
1183
- function classifyTradeError(message) {
1184
- const lower = message.toLowerCase();
1185
- if (lower.includes("configmismatch") || lower.includes("config mismatch")) {
1186
- return {
1187
- category: "config_mismatch",
1188
- userMessage: "Config hash mismatch \u2014 on-chain config does not match. Restart the agent to re-sync."
1189
- };
1553
+ if (process.env.ANTHROPIC_API_KEY && config.llm.provider === "anthropic") {
1554
+ llmConfig.apiKey = process.env.ANTHROPIC_API_KEY;
1190
1555
  }
1191
- if (lower.includes("insufficient funds") || lower.includes("exceeds the balance")) {
1192
- return {
1193
- category: "insufficient_funds",
1194
- userMessage: "Insufficient ETH for gas \u2014 fund your trading wallet."
1195
- };
1556
+ if (process.env.GOOGLE_AI_API_KEY && config.llm.provider === "google") {
1557
+ llmConfig.apiKey = process.env.GOOGLE_AI_API_KEY;
1196
1558
  }
1197
- if (lower.includes("out of gas") || lower.includes("intrinsic gas too low")) {
1198
- return {
1199
- category: "out_of_gas",
1200
- userMessage: "Transaction ran out of gas during execution. This is usually transient \u2014 retrying with more gas."
1201
- };
1559
+ if (process.env.DEEPSEEK_API_KEY && config.llm.provider === "deepseek") {
1560
+ llmConfig.apiKey = process.env.DEEPSEEK_API_KEY;
1202
1561
  }
1203
- if (lower.includes("minamountnotmet") || lower.includes("min amount") || lower.includes("slippage")) {
1204
- return {
1205
- category: "slippage",
1206
- userMessage: "Swap failed due to slippage \u2014 price moved beyond tolerance. Will retry next cycle."
1207
- };
1562
+ if (process.env.MISTRAL_API_KEY && config.llm.provider === "mistral") {
1563
+ llmConfig.apiKey = process.env.MISTRAL_API_KEY;
1208
1564
  }
1209
- if (lower.includes("notauthorizedforagent") || lower.includes("not authorized")) {
1210
- return {
1211
- category: "not_authorized",
1212
- userMessage: "Wallet not authorized for this agent. Ensure your wallet is linked via the registry."
1213
- };
1565
+ if (process.env.GROQ_API_KEY && config.llm.provider === "groq") {
1566
+ llmConfig.apiKey = process.env.GROQ_API_KEY;
1214
1567
  }
1215
- if (lower.includes("aggregatornotwhitelisted")) {
1216
- return {
1217
- category: "aggregator_error",
1218
- userMessage: "DEX aggregator not whitelisted on the router. Contact support."
1219
- };
1568
+ if (process.env.TOGETHER_API_KEY && config.llm.provider === "together") {
1569
+ llmConfig.apiKey = process.env.TOGETHER_API_KEY;
1220
1570
  }
1221
- if (lower.includes("reverted") || lower.includes("execution reverted")) {
1222
- return {
1223
- category: "reverted",
1224
- userMessage: `Transaction reverted: ${message.slice(0, 200)}`
1225
- };
1571
+ if (process.env.EXAGENT_LLM_URL) {
1572
+ llmConfig.endpoint = process.env.EXAGENT_LLM_URL;
1226
1573
  }
1227
- if (lower.includes("timeout") || lower.includes("econnrefused") || lower.includes("network")) {
1228
- return {
1229
- category: "network",
1230
- userMessage: "Network error \u2014 RPC node may be down. Will retry next cycle."
1231
- };
1574
+ if (process.env.EXAGENT_LLM_MODEL) {
1575
+ llmConfig.model = process.env.EXAGENT_LLM_MODEL;
1232
1576
  }
1577
+ const network = process.env.EXAGENT_NETWORK || config.network;
1233
1578
  return {
1234
- category: "unknown",
1235
- userMessage: message.slice(0, 300)
1579
+ ...config,
1580
+ llm: llmConfig,
1581
+ network,
1582
+ privateKey: privateKey || ""
1236
1583
  };
1237
1584
  }
1238
-
1239
- // src/trading/market.ts
1240
- var import_viem = require("viem");
1241
- var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
1242
- var TOKEN_DECIMALS = {
1243
- // Base Mainnet — Core tokens
1244
- "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
1245
- // USDC
1246
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
1247
- // USDbC
1248
- "0x4200000000000000000000000000000000000006": 18,
1249
- // WETH
1250
- "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
1251
- // DAI
1252
- "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
1253
- // cbETH
1254
- [NATIVE_ETH.toLowerCase()]: 18,
1255
- // Native ETH
1256
- // Base Mainnet — Established tokens
1257
- "0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
1258
- // AERO (Aerodrome)
1259
- "0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
1260
- // BRETT
1261
- "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
1262
- // DEGEN
1263
- "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
1264
- // VIRTUAL
1265
- "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
1266
- // TOSHI
1267
- "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
1268
- // cbBTC
1269
- "0x2416092f143378750bb29b79ed961ab195cceea5": 18,
1270
- // ezETH (Renzo)
1271
- "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
1272
- // wstETH (Lido)
1273
- };
1274
- function getTokenDecimals(address) {
1275
- const decimals = TOKEN_DECIMALS[address.toLowerCase()];
1276
- if (decimals === void 0) {
1277
- console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
1278
- return 18;
1585
+ function validateConfig(config) {
1586
+ if (!config.privateKey) {
1587
+ throw new Error("Private key is required");
1279
1588
  }
1280
- return decimals;
1281
- }
1282
- var TOKEN_TO_COINGECKO = {
1283
- // Core
1284
- "0x4200000000000000000000000000000000000006": "ethereum",
1285
- // WETH
1286
- [NATIVE_ETH.toLowerCase()]: "ethereum",
1287
- "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
1288
- // USDC
1289
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
1290
- // USDbC
1291
- "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
1292
- // cbETH
1293
- "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
1294
- // DAI
1295
- // Established
1296
- "0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
1297
- // AERO
1298
- "0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
1299
- // BRETT
1300
- "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
1301
- // DEGEN
1302
- "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
1303
- // VIRTUAL
1304
- "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
1305
- // TOSHI
1306
- "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
1307
- // cbBTC
1308
- "0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
1309
- // ezETH
1310
- "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
1311
- // wstETH
1312
- };
1313
- var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
1314
- var PRICE_STALENESS_MS = 6e4;
1315
- var MarketDataService = class {
1316
- rpcUrl;
1317
- client;
1318
- /** Cached prices from last fetch */
1319
- cachedPrices = {};
1320
- /** Timestamp of last successful price fetch */
1321
- lastPriceFetchAt = 0;
1322
- constructor(rpcUrl) {
1323
- this.rpcUrl = rpcUrl;
1324
- this.client = (0, import_viem.createPublicClient)({
1325
- transport: (0, import_viem.http)(rpcUrl)
1326
- });
1589
+ if (config.llm.provider !== "ollama" && !config.llm.apiKey) {
1590
+ throw new Error(`API key required for ${config.llm.provider} provider`);
1327
1591
  }
1328
- /** Cached volume data */
1329
- cachedVolume24h = {};
1330
- /** Cached price change data */
1331
- cachedPriceChange24h = {};
1332
- /**
1333
- * Fetch current market data for the agent
1334
- */
1335
- async fetchMarketData(walletAddress, tokenAddresses) {
1336
- const prices = await this.fetchPrices(tokenAddresses);
1337
- const balances = await this.fetchBalances(walletAddress, tokenAddresses);
1338
- const portfolioValue = this.calculatePortfolioValue(balances, prices);
1339
- let gasPrice;
1340
- try {
1341
- gasPrice = await this.client.getGasPrice();
1342
- } catch {
1343
- }
1344
- return {
1345
- timestamp: Date.now(),
1346
- prices,
1347
- balances,
1348
- portfolioValue,
1349
- volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
1350
- priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
1351
- gasPrice,
1352
- network: {
1353
- chainId: this.client.chain?.id ?? 8453
1354
- }
1355
- };
1592
+ if (config.llm.provider === "ollama" && !config.llm.endpoint) {
1593
+ config.llm.endpoint = "http://localhost:11434";
1356
1594
  }
1357
- /**
1358
- * Check if cached prices are still fresh
1359
- */
1360
- get pricesAreFresh() {
1361
- return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
1595
+ if (config.llm.provider === "custom" && !config.llm.endpoint) {
1596
+ throw new Error("Endpoint required for custom LLM provider");
1362
1597
  }
1363
- /**
1364
- * Fetch token prices from CoinGecko free API
1365
- * Returns cached prices if still fresh (<60s old)
1366
- */
1367
- async fetchPrices(tokenAddresses) {
1368
- if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
1369
- const prices2 = { ...this.cachedPrices };
1370
- for (const addr of tokenAddresses) {
1371
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1372
- if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
1373
- prices2[addr.toLowerCase()] = 1;
1374
- }
1375
- }
1376
- return prices2;
1377
- }
1378
- const prices = {};
1379
- const idsToFetch = /* @__PURE__ */ new Set();
1380
- for (const addr of tokenAddresses) {
1381
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1382
- if (cgId && !STABLECOIN_IDS.has(cgId)) {
1383
- idsToFetch.add(cgId);
1384
- }
1385
- }
1386
- idsToFetch.add("ethereum");
1387
- if (idsToFetch.size > 0) {
1388
- try {
1389
- const ids = Array.from(idsToFetch).join(",");
1390
- const response = await fetch(
1391
- `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
1392
- { signal: AbortSignal.timeout(5e3) }
1393
- );
1394
- if (response.ok) {
1395
- const data = await response.json();
1396
- for (const [cgId, priceData] of Object.entries(data)) {
1397
- for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
1398
- if (id === cgId) {
1399
- const key = addr.toLowerCase();
1400
- prices[key] = priceData.usd;
1401
- if (priceData.usd_24h_vol !== void 0) {
1402
- this.cachedVolume24h[key] = priceData.usd_24h_vol;
1403
- }
1404
- if (priceData.usd_24h_change !== void 0) {
1405
- this.cachedPriceChange24h[key] = priceData.usd_24h_change;
1406
- }
1407
- }
1408
- }
1409
- }
1410
- this.lastPriceFetchAt = Date.now();
1411
- } else {
1412
- console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
1413
- }
1414
- } catch (error) {
1415
- console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
1416
- }
1417
- }
1418
- for (const addr of tokenAddresses) {
1419
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1420
- if (cgId && STABLECOIN_IDS.has(cgId)) {
1421
- prices[addr.toLowerCase()] = 1;
1422
- }
1598
+ if (!config.agentId || Number(config.agentId) <= 0) {
1599
+ throw new Error("Valid agent ID required");
1600
+ }
1601
+ }
1602
+ var DEFAULT_RPC_URL = "https://base-rpc.publicnode.com";
1603
+ function getRpcUrl() {
1604
+ return process.env.BASE_RPC_URL || process.env.EXAGENT_RPC_URL || DEFAULT_RPC_URL;
1605
+ }
1606
+ function createSampleConfig(agentId, name) {
1607
+ return {
1608
+ agentId,
1609
+ name,
1610
+ network: "mainnet",
1611
+ llm: {
1612
+ provider: "openai",
1613
+ model: "gpt-4.1",
1614
+ temperature: 0.7,
1615
+ maxTokens: 4096
1616
+ },
1617
+ riskUniverse: "established",
1618
+ trading: {
1619
+ timeHorizon: "swing",
1620
+ maxPositionSizeBps: 1e3,
1621
+ maxDailyLossBps: 500,
1622
+ maxConcurrentPositions: 5,
1623
+ tradingIntervalMs: 6e4,
1624
+ maxSlippageBps: 100,
1625
+ minTradeValueUSD: 1
1626
+ },
1627
+ vault: {
1628
+ // Default to manual - user must explicitly enable auto-creation
1629
+ policy: "manual",
1630
+ // Will use agent name for vault name if not set
1631
+ preferVaultTrading: true
1423
1632
  }
1424
- const missingAddrs = tokenAddresses.filter(
1425
- (addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
1633
+ };
1634
+ }
1635
+
1636
+ // src/trading/executor.ts
1637
+ var TradeExecutor = class {
1638
+ client;
1639
+ config;
1640
+ allowedTokens;
1641
+ configHashFn;
1642
+ constructor(client, config, configHashFn) {
1643
+ this.client = client;
1644
+ this.config = config;
1645
+ this.configHashFn = configHashFn;
1646
+ this.allowedTokens = new Set(
1647
+ (config.allowedTokens || []).map((t) => t.toLowerCase())
1426
1648
  );
1427
- if (missingAddrs.length > 0) {
1428
- try {
1429
- const coins = missingAddrs.map((a) => `base:${a}`).join(",");
1430
- const llamaResponse = await fetch(
1431
- `https://coins.llama.fi/prices/current/${coins}`,
1432
- { signal: AbortSignal.timeout(5e3) }
1433
- );
1434
- if (llamaResponse.ok) {
1435
- const llamaData = await llamaResponse.json();
1436
- for (const [key, data] of Object.entries(llamaData.coins)) {
1437
- const addr = key.replace("base:", "").toLowerCase();
1438
- if (data.price && data.confidence > 0.5) {
1439
- prices[addr] = data.price;
1440
- }
1441
- }
1442
- if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
1443
- }
1444
- } catch {
1445
- }
1446
- }
1447
- if (Object.keys(prices).length > 0) {
1448
- this.cachedPrices = prices;
1449
- }
1450
- if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
1451
- console.warn("Using cached prices (last successful fetch was stale)");
1452
- return { ...this.cachedPrices };
1453
- }
1454
- for (const addr of tokenAddresses) {
1455
- if (!prices[addr.toLowerCase()]) {
1456
- console.warn(`No price available for ${addr}, using 0`);
1457
- prices[addr.toLowerCase()] = 0;
1458
- }
1459
- }
1460
- return prices;
1461
1649
  }
1462
1650
  /**
1463
- * Fetch real on-chain balances: native ETH + ERC-20 tokens
1651
+ * Execute a single trade signal
1464
1652
  */
1465
- async fetchBalances(walletAddress, tokenAddresses) {
1466
- const balances = {};
1467
- const wallet = walletAddress;
1653
+ async execute(signal) {
1654
+ if (signal.action === "hold") {
1655
+ return { success: true };
1656
+ }
1468
1657
  try {
1469
- const nativeBalance = await this.client.getBalance({ address: wallet });
1470
- balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
1471
- const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
1472
- try {
1473
- const balance = await this.client.readContract({
1474
- address: tokenAddress,
1475
- abi: import_viem.erc20Abi,
1476
- functionName: "balanceOf",
1477
- args: [wallet]
1478
- });
1479
- return { address: tokenAddress.toLowerCase(), balance };
1480
- } catch (error) {
1481
- return { address: tokenAddress.toLowerCase(), balance: 0n };
1482
- }
1483
- });
1484
- const results = await Promise.all(erc20Promises);
1485
- for (const { address, balance } of results) {
1486
- balances[address] = balance;
1658
+ console.log(`Executing ${signal.action}: ${signal.tokenIn} -> ${signal.tokenOut}`);
1659
+ console.log(`Amount: ${signal.amountIn.toString()}, Confidence: ${signal.confidence}`);
1660
+ if (!this.validateSignal(signal)) {
1661
+ return { success: false, error: "Signal exceeds position limits" };
1487
1662
  }
1663
+ const configHash = this.configHashFn?.();
1664
+ const result = await this.client.trade({
1665
+ tokenIn: signal.tokenIn,
1666
+ tokenOut: signal.tokenOut,
1667
+ amountIn: signal.amountIn,
1668
+ maxSlippageBps: this.config.trading?.maxSlippageBps ?? 100,
1669
+ ...configHash && { configHash }
1670
+ });
1671
+ console.log(`Trade executed: ${result.hash}`);
1672
+ return { success: true, txHash: result.hash };
1488
1673
  } catch (error) {
1489
- console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
1490
- balances[NATIVE_ETH.toLowerCase()] = 0n;
1491
- for (const address of tokenAddresses) {
1492
- balances[address.toLowerCase()] = 0n;
1674
+ const message = error instanceof Error ? error.message : "Unknown error";
1675
+ const classified = classifyTradeError(message);
1676
+ console.error(`Trade failed (${classified.category}): ${classified.userMessage}`);
1677
+ return { success: false, error: classified.userMessage };
1678
+ }
1679
+ }
1680
+ /**
1681
+ * Execute multiple trade signals
1682
+ * Returns results for each signal
1683
+ */
1684
+ async executeAll(signals) {
1685
+ const results = [];
1686
+ for (const signal of signals) {
1687
+ const result = await this.execute(signal);
1688
+ results.push({ signal, ...result });
1689
+ if (signals.indexOf(signal) < signals.length - 1) {
1690
+ await this.delay(1e3);
1493
1691
  }
1494
1692
  }
1495
- return balances;
1693
+ return results;
1496
1694
  }
1497
1695
  /**
1498
- * Calculate total portfolio value in USD
1696
+ * Validate a signal against config limits and token restrictions
1499
1697
  */
1500
- calculatePortfolioValue(balances, prices) {
1501
- let total = 0;
1502
- for (const [address, balance] of Object.entries(balances)) {
1503
- const price = prices[address.toLowerCase()] || 0;
1504
- const decimals = getTokenDecimals(address);
1505
- const amount = Number(balance) / Math.pow(10, decimals);
1506
- total += amount * price;
1698
+ validateSignal(signal) {
1699
+ if (signal.confidence < 0.5) {
1700
+ console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
1701
+ return false;
1507
1702
  }
1508
- return total;
1703
+ if (this.allowedTokens.size > 0) {
1704
+ const tokenInAllowed = this.allowedTokens.has(signal.tokenIn.toLowerCase());
1705
+ const tokenOutAllowed = this.allowedTokens.has(signal.tokenOut.toLowerCase());
1706
+ if (!tokenInAllowed) {
1707
+ console.warn(`Token ${signal.tokenIn} not in allowed list for this agent's risk universe \u2014 skipping`);
1708
+ return false;
1709
+ }
1710
+ if (!tokenOutAllowed) {
1711
+ console.warn(`Token ${signal.tokenOut} not in allowed list for this agent's risk universe \u2014 skipping`);
1712
+ return false;
1713
+ }
1714
+ }
1715
+ return true;
1716
+ }
1717
+ delay(ms) {
1718
+ return new Promise((resolve) => setTimeout(resolve, ms));
1509
1719
  }
1510
1720
  };
1721
+ function classifyTradeError(message) {
1722
+ const lower = message.toLowerCase();
1723
+ if (lower.includes("configmismatch") || lower.includes("config mismatch")) {
1724
+ return {
1725
+ category: "config_mismatch",
1726
+ userMessage: "Config hash mismatch \u2014 on-chain config does not match. Restart the agent to re-sync."
1727
+ };
1728
+ }
1729
+ if (lower.includes("insufficient funds") || lower.includes("exceeds the balance")) {
1730
+ return {
1731
+ category: "insufficient_funds",
1732
+ userMessage: "Insufficient ETH for gas \u2014 fund your trading wallet."
1733
+ };
1734
+ }
1735
+ if (lower.includes("out of gas") || lower.includes("intrinsic gas too low")) {
1736
+ return {
1737
+ category: "out_of_gas",
1738
+ userMessage: "Transaction ran out of gas during execution. This is usually transient \u2014 retrying with more gas."
1739
+ };
1740
+ }
1741
+ if (lower.includes("minamountnotmet") || lower.includes("min amount") || lower.includes("slippage")) {
1742
+ return {
1743
+ category: "slippage",
1744
+ userMessage: "Swap failed due to slippage \u2014 price moved beyond tolerance. Will retry next cycle."
1745
+ };
1746
+ }
1747
+ if (lower.includes("notauthorizedforagent") || lower.includes("not authorized")) {
1748
+ return {
1749
+ category: "not_authorized",
1750
+ userMessage: "Wallet not authorized for this agent. Ensure your wallet is linked via the registry."
1751
+ };
1752
+ }
1753
+ if (lower.includes("aggregatornotwhitelisted")) {
1754
+ return {
1755
+ category: "aggregator_error",
1756
+ userMessage: "DEX aggregator not whitelisted on the router. Contact support."
1757
+ };
1758
+ }
1759
+ if (lower.includes("reverted") || lower.includes("execution reverted")) {
1760
+ return {
1761
+ category: "reverted",
1762
+ userMessage: `Transaction reverted: ${message.slice(0, 200)}`
1763
+ };
1764
+ }
1765
+ if (lower.includes("timeout") || lower.includes("econnrefused") || lower.includes("network")) {
1766
+ return {
1767
+ category: "network",
1768
+ userMessage: "Network error \u2014 RPC node may be down. Will retry next cycle."
1769
+ };
1770
+ }
1771
+ return {
1772
+ category: "unknown",
1773
+ userMessage: message.slice(0, 300)
1774
+ };
1775
+ }
1511
1776
 
1512
1777
  // src/trading/risk.ts
1513
1778
  var RiskManager = class {
@@ -1619,6 +1884,31 @@ var RiskManager = class {
1619
1884
  updateFees(fees) {
1620
1885
  this.dailyFees += fees;
1621
1886
  }
1887
+ /**
1888
+ * Export current risk state for persistence across restarts.
1889
+ */
1890
+ exportState() {
1891
+ return {
1892
+ dailyPnL: this.dailyPnL,
1893
+ dailyFees: this.dailyFees,
1894
+ lastResetDate: this.lastResetDate
1895
+ };
1896
+ }
1897
+ /**
1898
+ * Restore risk state from persistence (called on startup).
1899
+ * Only restores if the saved state is from today — expired state is ignored.
1900
+ */
1901
+ restoreState(state) {
1902
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1903
+ if (state.lastResetDate === today) {
1904
+ this.dailyPnL = state.dailyPnL;
1905
+ this.dailyFees = state.dailyFees;
1906
+ this.lastResetDate = state.lastResetDate;
1907
+ console.log(`Risk state restored: PnL=$${this.dailyPnL.toFixed(2)}, Fees=$${this.dailyFees.toFixed(2)}`);
1908
+ } else {
1909
+ console.log("Risk state expired (different day), starting fresh");
1910
+ }
1911
+ }
1622
1912
  /**
1623
1913
  * Get current risk status
1624
1914
  * @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
@@ -2082,7 +2372,6 @@ var VaultManager = class {
2082
2372
  // src/relay.ts
2083
2373
  var import_ws = __toESM(require("ws"));
2084
2374
  var import_accounts2 = require("viem/accounts");
2085
- var import_sdk = require("@exagent/sdk");
2086
2375
  var RelayClient = class {
2087
2376
  config;
2088
2377
  ws = null;
@@ -2186,7 +2475,7 @@ var RelayClient = class {
2186
2475
  wallet: account.address,
2187
2476
  timestamp,
2188
2477
  signature,
2189
- sdkVersion: import_sdk.SDK_VERSION
2478
+ sdkVersion: AGENT_VERSION
2190
2479
  });
2191
2480
  }
2192
2481
  /**
@@ -3601,10 +3890,12 @@ var AgentRuntime = class {
3601
3890
  lastCycleAt = 0;
3602
3891
  lastPortfolioValue = 0;
3603
3892
  lastEthBalance = "0";
3893
+ lastPrices = {};
3604
3894
  processAlive = true;
3605
3895
  riskUniverse = 0;
3606
3896
  allowedTokens = /* @__PURE__ */ new Set();
3607
3897
  strategyContext;
3898
+ positionTracker;
3608
3899
  // Perp trading components (null if perp not enabled)
3609
3900
  perpClient = null;
3610
3901
  perpSigner = null;
@@ -3629,7 +3920,7 @@ var AgentRuntime = class {
3629
3920
  */
3630
3921
  async initialize() {
3631
3922
  console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
3632
- this.client = new import_sdk2.ExagentClient({
3923
+ this.client = new import_sdk.ExagentClient({
3633
3924
  privateKey: this.config.privateKey,
3634
3925
  network: this.config.network
3635
3926
  });
@@ -3647,14 +3938,20 @@ var AgentRuntime = class {
3647
3938
  console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
3648
3939
  await this.syncConfigHash();
3649
3940
  this.strategy = await loadStrategy();
3941
+ const store = new FileStore();
3650
3942
  this.strategyContext = {
3651
- store: new FileStore(),
3943
+ store,
3652
3944
  agentId: Number(this.config.agentId),
3653
3945
  walletAddress: this.client.address
3654
3946
  };
3947
+ this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
3655
3948
  this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
3656
3949
  this.riskManager = new RiskManager(this.config.trading);
3657
3950
  this.marketData = new MarketDataService(this.getRpcUrl());
3951
+ const savedRisk = this.positionTracker.getRiskState();
3952
+ if (savedRisk.lastResetDate) {
3953
+ this.riskManager.restoreState(savedRisk);
3954
+ }
3658
3955
  await this.initializeVaultManager();
3659
3956
  await this.initializePerp();
3660
3957
  await this.initializeRelay();
@@ -3840,7 +4137,7 @@ var AgentRuntime = class {
3840
4137
  if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
3841
4138
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
3842
4139
  const nonce = await this.client.registry.getNonce(address);
3843
- const linkMessage = import_sdk2.ExagentRegistry.generateLinkMessage(
4140
+ const linkMessage = import_sdk.ExagentRegistry.generateLinkMessage(
3844
4141
  address,
3845
4142
  agentId,
3846
4143
  nonce
@@ -3931,15 +4228,14 @@ var AgentRuntime = class {
3931
4228
  async syncConfigHash() {
3932
4229
  const agentId = BigInt(this.config.agentId);
3933
4230
  const llmMeta = this.llm.getMetadata();
3934
- this.configHash = import_sdk2.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
4231
+ this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
3935
4232
  console.log(`Config hash: ${this.configHash}`);
3936
4233
  const onChainHash = await this.client.registry.getConfigHash(agentId);
3937
4234
  if (onChainHash !== this.configHash) {
3938
4235
  console.log("Config changed, updating on-chain...");
3939
4236
  try {
3940
4237
  await this.client.registry.updateConfig(agentId, this.configHash);
3941
- const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
3942
- console.log(`Config updated, new epoch started: ${newEpoch}`);
4238
+ console.log(`Config updated on-chain`);
3943
4239
  } catch (error) {
3944
4240
  const message = error instanceof Error ? error.message : String(error);
3945
4241
  if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
@@ -3970,8 +4266,7 @@ var AgentRuntime = class {
3970
4266
  console.log(" ETH detected! Retrying config update...");
3971
4267
  console.log("");
3972
4268
  await this.client.registry.updateConfig(agentId, this.configHash);
3973
- const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
3974
- console.log(`Config updated, new epoch started: ${newEpoch}`);
4269
+ console.log(`Config updated on-chain`);
3975
4270
  return;
3976
4271
  }
3977
4272
  process.stdout.write(".");
@@ -3982,8 +4277,7 @@ var AgentRuntime = class {
3982
4277
  }
3983
4278
  }
3984
4279
  } else {
3985
- const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
3986
- console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
4280
+ console.log("Config hash matches on-chain");
3987
4281
  }
3988
4282
  }
3989
4283
  /**
@@ -4117,6 +4411,10 @@ var AgentRuntime = class {
4117
4411
  }
4118
4412
  if (updated) {
4119
4413
  this.riskManager = new RiskManager(this.config.trading);
4414
+ const savedRiskState = this.positionTracker.getRiskState();
4415
+ if (savedRiskState.lastResetDate) {
4416
+ this.riskManager.restoreState(savedRiskState);
4417
+ }
4120
4418
  console.log("Risk params updated via command center");
4121
4419
  this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
4122
4420
  this.relay?.sendMessage(
@@ -4320,6 +4618,7 @@ var AgentRuntime = class {
4320
4618
  mode: this.mode,
4321
4619
  agentId: String(this.config.agentId),
4322
4620
  wallet: this.client?.address,
4621
+ sdkVersion: AGENT_VERSION,
4323
4622
  cycleCount: this.cycleCount,
4324
4623
  lastCycleAt: this.lastCycleAt,
4325
4624
  tradingIntervalMs: this.config.trading.tradingIntervalMs,
@@ -4348,7 +4647,8 @@ var AgentRuntime = class {
4348
4647
  openPositions: 0,
4349
4648
  effectiveLeverage: 0,
4350
4649
  pendingRecords: this.perpRecorder?.pendingRetries ?? 0
4351
- } : void 0
4650
+ } : void 0,
4651
+ positions: this.positionTracker ? this.positionTracker.getPositionSummary(this.lastPrices) : void 0
4352
4652
  };
4353
4653
  if (this.perpConnected && this.perpPositions && status.perp) {
4354
4654
  this.perpPositions.getAccountSummary().then((account) => {
@@ -4379,14 +4679,19 @@ var AgentRuntime = class {
4379
4679
  const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
4380
4680
  console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
4381
4681
  this.lastPortfolioValue = marketData.portfolioValue;
4682
+ this.lastPrices = marketData.prices;
4382
4683
  const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4383
4684
  this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
4685
+ this.positionTracker.syncBalances(marketData.balances, marketData.prices);
4384
4686
  const fundsOk = this.checkFundsLow(marketData);
4385
4687
  if (!fundsOk) {
4386
4688
  console.warn("Skipping trading cycle \u2014 ETH balance critically low");
4387
4689
  this.sendRelayStatus();
4388
4690
  return;
4389
4691
  }
4692
+ this.strategyContext.positions = this.positionTracker.getPositions();
4693
+ this.strategyContext.tradeHistory = this.positionTracker.getTradeHistory(20);
4694
+ this.strategyContext.positionTracker = this.positionTracker;
4390
4695
  let signals;
4391
4696
  try {
4392
4697
  signals = await this.strategy(marketData, this.llm, this.config, this.strategyContext);
@@ -4452,13 +4757,30 @@ var AgentRuntime = class {
4452
4757
  );
4453
4758
  }
4454
4759
  }
4760
+ for (const result of results) {
4761
+ const tokenIn = result.signal.tokenIn.toLowerCase();
4762
+ const tokenOut = result.signal.tokenOut.toLowerCase();
4763
+ this.positionTracker.recordTrade({
4764
+ action: result.signal.action,
4765
+ tokenIn,
4766
+ tokenOut,
4767
+ amountIn: result.signal.amountIn,
4768
+ priceIn: marketData.prices[tokenIn] || 0,
4769
+ priceOut: marketData.prices[tokenOut] || 0,
4770
+ txHash: result.txHash,
4771
+ reasoning: result.signal.reasoning,
4772
+ success: result.success
4773
+ });
4774
+ }
4455
4775
  const postTokens = this.config.allowedTokens || this.getDefaultTokens();
4456
4776
  const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
4457
4777
  const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
4458
4778
  this.riskManager.updatePnL(marketPnL);
4779
+ this.positionTracker.saveRiskState(this.riskManager.exportState());
4459
4780
  if (marketPnL !== 0) {
4460
4781
  console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
4461
4782
  }
4783
+ this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
4462
4784
  this.lastPortfolioValue = postTradeData.portfolioValue;
4463
4785
  const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4464
4786
  this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
@@ -4914,8 +5236,12 @@ function loadSecureEnv(basePath, passphrase) {
4914
5236
  }
4915
5237
  return false;
4916
5238
  }
5239
+
5240
+ // src/index.ts
5241
+ var AGENT_VERSION = "0.1.21";
4917
5242
  // Annotate the CommonJS export names for ESM import in node:
4918
5243
  0 && (module.exports = {
5244
+ AGENT_VERSION,
4919
5245
  AgentConfigSchema,
4920
5246
  AgentRuntime,
4921
5247
  AnthropicAdapter,
@@ -4939,6 +5265,7 @@ function loadSecureEnv(basePath, passphrase) {
4939
5265
  PerpOnboarding,
4940
5266
  PerpTradeRecorder,
4941
5267
  PositionManager,
5268
+ PositionTracker,
4942
5269
  RelayClient,
4943
5270
  RelayConfigSchema,
4944
5271
  RiskManager,