@exagent/agent 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,11 +30,13 @@ 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,
36
37
  BaseLLMAdapter: () => BaseLLMAdapter,
37
38
  DeepSeekAdapter: () => DeepSeekAdapter,
39
+ FileStore: () => FileStore,
38
40
  GoogleAdapter: () => GoogleAdapter,
39
41
  GroqAdapter: () => GroqAdapter,
40
42
  HYPERLIQUID_DOMAIN: () => HYPERLIQUID_DOMAIN,
@@ -52,6 +54,7 @@ __export(index_exports, {
52
54
  PerpOnboarding: () => PerpOnboarding,
53
55
  PerpTradeRecorder: () => PerpTradeRecorder,
54
56
  PositionManager: () => PositionManager,
57
+ PositionTracker: () => PositionTracker,
55
58
  RelayClient: () => RelayClient,
56
59
  RelayConfigSchema: () => RelayConfigSchema,
57
60
  RiskManager: () => RiskManager,
@@ -81,105 +84,641 @@ __export(index_exports, {
81
84
  module.exports = __toCommonJS(index_exports);
82
85
 
83
86
  // src/runtime.ts
84
- var import_sdk2 = require("@exagent/sdk");
87
+ var import_sdk = require("@exagent/sdk");
85
88
  var import_viem6 = require("viem");
86
89
  var import_chains4 = require("viem/chains");
87
90
  var import_accounts5 = require("viem/accounts");
88
91
 
89
- // src/llm/openai.ts
90
- var import_openai = __toESM(require("openai"));
91
-
92
- // src/llm/base.ts
93
- var BaseLLMAdapter = class {
94
- config;
95
- constructor(config) {
96
- this.config = config;
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;
97
132
  }
98
- getMetadata() {
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
+ }
99
197
  return {
100
- provider: this.config.provider,
101
- model: this.config.model || "unknown",
102
- isLocal: this.config.provider === "ollama"
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
+ }
103
208
  };
104
209
  }
105
210
  /**
106
- * Format model name for display
211
+ * Check if cached prices are still fresh
107
212
  */
108
- getDisplayModel() {
109
- if (this.config.provider === "ollama") {
110
- return `Local (${this.config.model || "ollama"})`;
111
- }
112
- return this.config.model || this.config.provider;
213
+ get pricesAreFresh() {
214
+ return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
113
215
  }
114
- };
115
-
116
- // src/llm/openai.ts
117
- var OpenAIAdapter = class extends BaseLLMAdapter {
118
- client;
119
- constructor(config) {
120
- super(config);
121
- if (!config.apiKey && !config.endpoint) {
122
- throw new Error("OpenAI API key or custom endpoint required");
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;
123
230
  }
124
- this.client = new import_openai.default({
125
- apiKey: config.apiKey || "not-needed-for-custom",
126
- baseURL: config.endpoint
127
- });
128
- }
129
- async chat(messages) {
130
- try {
131
- const response = await this.client.chat.completions.create({
132
- model: this.config.model || "gpt-4.1",
133
- messages: messages.map((m) => ({
134
- role: m.role,
135
- content: m.content
136
- })),
137
- temperature: this.config.temperature,
138
- max_tokens: this.config.maxTokens
139
- });
140
- const choice = response.choices[0];
141
- if (!choice || !choice.message) {
142
- throw new Error("No response from OpenAI");
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);
143
237
  }
144
- return {
145
- content: choice.message.content || "",
146
- usage: response.usage ? {
147
- promptTokens: response.usage.prompt_tokens,
148
- completionTokens: response.usage.completion_tokens,
149
- totalTokens: response.usage.total_tokens
150
- } : void 0
151
- };
152
- } catch (error) {
153
- if (error instanceof import_openai.default.APIError) {
154
- throw new Error(`OpenAI API error: ${error.message}`);
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);
155
269
  }
156
- throw error;
157
270
  }
158
- }
159
- };
160
-
161
- // src/llm/anthropic.ts
162
- var AnthropicAdapter = class extends BaseLLMAdapter {
163
- apiKey;
164
- baseUrl;
165
- constructor(config) {
166
- super(config);
167
- if (!config.apiKey) {
168
- throw new Error("Anthropic API key required");
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
+ }
169
276
  }
170
- this.apiKey = config.apiKey;
171
- this.baseUrl = config.endpoint || "https://api.anthropic.com";
172
- }
173
- async chat(messages) {
174
- const systemMessage = messages.find((m) => m.role === "system");
175
- const chatMessages = messages.filter((m) => m.role !== "system");
176
- const body = {
177
- model: this.config.model || "claude-opus-4-5-20251101",
178
- max_tokens: this.config.maxTokens || 4096,
179
- temperature: this.config.temperature,
180
- system: systemMessage?.content,
181
- messages: chatMessages.map((m) => ({
182
- role: m.role,
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
+
628
+ // src/llm/openai.ts
629
+ var import_openai = __toESM(require("openai"));
630
+
631
+ // src/llm/base.ts
632
+ var BaseLLMAdapter = class {
633
+ config;
634
+ constructor(config) {
635
+ this.config = config;
636
+ }
637
+ getMetadata() {
638
+ return {
639
+ provider: this.config.provider,
640
+ model: this.config.model || "unknown",
641
+ isLocal: this.config.provider === "ollama"
642
+ };
643
+ }
644
+ /**
645
+ * Format model name for display
646
+ */
647
+ getDisplayModel() {
648
+ if (this.config.provider === "ollama") {
649
+ return `Local (${this.config.model || "ollama"})`;
650
+ }
651
+ return this.config.model || this.config.provider;
652
+ }
653
+ };
654
+
655
+ // src/llm/openai.ts
656
+ var OpenAIAdapter = class extends BaseLLMAdapter {
657
+ client;
658
+ constructor(config) {
659
+ super(config);
660
+ if (!config.apiKey && !config.endpoint) {
661
+ throw new Error("OpenAI API key or custom endpoint required");
662
+ }
663
+ this.client = new import_openai.default({
664
+ apiKey: config.apiKey || "not-needed-for-custom",
665
+ baseURL: config.endpoint
666
+ });
667
+ }
668
+ async chat(messages) {
669
+ try {
670
+ const response = await this.client.chat.completions.create({
671
+ model: this.config.model || "gpt-4.1",
672
+ messages: messages.map((m) => ({
673
+ role: m.role,
674
+ content: m.content
675
+ })),
676
+ temperature: this.config.temperature,
677
+ max_tokens: this.config.maxTokens
678
+ });
679
+ const choice = response.choices[0];
680
+ if (!choice || !choice.message) {
681
+ throw new Error("No response from OpenAI");
682
+ }
683
+ return {
684
+ content: choice.message.content || "",
685
+ usage: response.usage ? {
686
+ promptTokens: response.usage.prompt_tokens,
687
+ completionTokens: response.usage.completion_tokens,
688
+ totalTokens: response.usage.total_tokens
689
+ } : void 0
690
+ };
691
+ } catch (error) {
692
+ if (error instanceof import_openai.default.APIError) {
693
+ throw new Error(`OpenAI API error: ${error.message}`);
694
+ }
695
+ throw error;
696
+ }
697
+ }
698
+ };
699
+
700
+ // src/llm/anthropic.ts
701
+ var AnthropicAdapter = class extends BaseLLMAdapter {
702
+ apiKey;
703
+ baseUrl;
704
+ constructor(config) {
705
+ super(config);
706
+ if (!config.apiKey) {
707
+ throw new Error("Anthropic API key required");
708
+ }
709
+ this.apiKey = config.apiKey;
710
+ this.baseUrl = config.endpoint || "https://api.anthropic.com";
711
+ }
712
+ async chat(messages) {
713
+ const systemMessage = messages.find((m) => m.role === "system");
714
+ const chatMessages = messages.filter((m) => m.role !== "system");
715
+ const body = {
716
+ model: this.config.model || "claude-opus-4-5-20251101",
717
+ max_tokens: this.config.maxTokens || 4096,
718
+ temperature: this.config.temperature,
719
+ system: systemMessage?.content,
720
+ messages: chatMessages.map((m) => ({
721
+ role: m.role,
183
722
  content: m.content
184
723
  }))
185
724
  };
@@ -1052,405 +1591,188 @@ function validateConfig(config) {
1052
1591
  }
1053
1592
  if (config.llm.provider === "ollama" && !config.llm.endpoint) {
1054
1593
  config.llm.endpoint = "http://localhost:11434";
1055
- }
1056
- if (config.llm.provider === "custom" && !config.llm.endpoint) {
1057
- throw new Error("Endpoint required for custom LLM provider");
1058
- }
1059
- if (!config.agentId || Number(config.agentId) <= 0) {
1060
- throw new Error("Valid agent ID required");
1061
- }
1062
- }
1063
- var DEFAULT_RPC_URL = "https://base-rpc.publicnode.com";
1064
- function getRpcUrl() {
1065
- return process.env.BASE_RPC_URL || process.env.EXAGENT_RPC_URL || DEFAULT_RPC_URL;
1066
- }
1067
- function createSampleConfig(agentId, name) {
1068
- return {
1069
- agentId,
1070
- name,
1071
- network: "mainnet",
1072
- llm: {
1073
- provider: "openai",
1074
- model: "gpt-4.1",
1075
- temperature: 0.7,
1076
- maxTokens: 4096
1077
- },
1078
- riskUniverse: "established",
1079
- trading: {
1080
- timeHorizon: "swing",
1081
- maxPositionSizeBps: 1e3,
1082
- maxDailyLossBps: 500,
1083
- maxConcurrentPositions: 5,
1084
- tradingIntervalMs: 6e4,
1085
- maxSlippageBps: 100,
1086
- minTradeValueUSD: 1
1087
- },
1088
- vault: {
1089
- // Default to manual - user must explicitly enable auto-creation
1090
- policy: "manual",
1091
- // Will use agent name for vault name if not set
1092
- preferVaultTrading: true
1093
- }
1094
- };
1095
- }
1096
-
1097
- // src/trading/executor.ts
1098
- var TradeExecutor = class {
1099
- client;
1100
- config;
1101
- allowedTokens;
1102
- configHashFn;
1103
- constructor(client, config, configHashFn) {
1104
- this.client = client;
1105
- this.config = config;
1106
- this.configHashFn = configHashFn;
1107
- this.allowedTokens = new Set(
1108
- (config.allowedTokens || []).map((t) => t.toLowerCase())
1109
- );
1110
- }
1111
- /**
1112
- * Execute a single trade signal
1113
- */
1114
- async execute(signal) {
1115
- if (signal.action === "hold") {
1116
- return { success: true };
1117
- }
1118
- try {
1119
- console.log(`Executing ${signal.action}: ${signal.tokenIn} -> ${signal.tokenOut}`);
1120
- console.log(`Amount: ${signal.amountIn.toString()}, Confidence: ${signal.confidence}`);
1121
- if (!this.validateSignal(signal)) {
1122
- return { success: false, error: "Signal exceeds position limits" };
1123
- }
1124
- const configHash = this.configHashFn?.();
1125
- const result = await this.client.trade({
1126
- tokenIn: signal.tokenIn,
1127
- tokenOut: signal.tokenOut,
1128
- amountIn: signal.amountIn,
1129
- maxSlippageBps: this.config.trading?.maxSlippageBps ?? 100,
1130
- ...configHash && { configHash }
1131
- });
1132
- console.log(`Trade executed: ${result.hash}`);
1133
- return { success: true, txHash: result.hash };
1134
- } catch (error) {
1135
- const message = error instanceof Error ? error.message : "Unknown error";
1136
- console.error(`Trade failed: ${message}`);
1137
- return { success: false, error: message };
1138
- }
1139
- }
1140
- /**
1141
- * Execute multiple trade signals
1142
- * Returns results for each signal
1143
- */
1144
- async executeAll(signals) {
1145
- const results = [];
1146
- for (const signal of signals) {
1147
- const result = await this.execute(signal);
1148
- results.push({ signal, ...result });
1149
- if (signals.indexOf(signal) < signals.length - 1) {
1150
- await this.delay(1e3);
1151
- }
1152
- }
1153
- return results;
1154
- }
1155
- /**
1156
- * Validate a signal against config limits and token restrictions
1157
- */
1158
- validateSignal(signal) {
1159
- if (signal.confidence < 0.5) {
1160
- console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
1161
- return false;
1162
- }
1163
- if (this.allowedTokens.size > 0) {
1164
- const tokenInAllowed = this.allowedTokens.has(signal.tokenIn.toLowerCase());
1165
- const tokenOutAllowed = this.allowedTokens.has(signal.tokenOut.toLowerCase());
1166
- if (!tokenInAllowed) {
1167
- console.warn(`Token ${signal.tokenIn} not in allowed list for this agent's risk universe \u2014 skipping`);
1168
- return false;
1169
- }
1170
- if (!tokenOutAllowed) {
1171
- console.warn(`Token ${signal.tokenOut} not in allowed list for this agent's risk universe \u2014 skipping`);
1172
- return false;
1173
- }
1174
- }
1175
- return true;
1176
- }
1177
- delay(ms) {
1178
- return new Promise((resolve) => setTimeout(resolve, ms));
1179
- }
1180
- };
1181
-
1182
- // src/trading/market.ts
1183
- var import_viem = require("viem");
1184
- var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
1185
- var TOKEN_DECIMALS = {
1186
- // Base Mainnet — Core tokens
1187
- "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
1188
- // USDC
1189
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
1190
- // USDbC
1191
- "0x4200000000000000000000000000000000000006": 18,
1192
- // WETH
1193
- "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
1194
- // DAI
1195
- "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
1196
- // cbETH
1197
- [NATIVE_ETH.toLowerCase()]: 18,
1198
- // Native ETH
1199
- // Base Mainnet — Established tokens
1200
- "0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
1201
- // AERO (Aerodrome)
1202
- "0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
1203
- // BRETT
1204
- "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
1205
- // DEGEN
1206
- "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
1207
- // VIRTUAL
1208
- "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
1209
- // TOSHI
1210
- "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
1211
- // cbBTC
1212
- "0x2416092f143378750bb29b79ed961ab195cceea5": 18,
1213
- // ezETH (Renzo)
1214
- "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
1215
- // wstETH (Lido)
1216
- };
1217
- function getTokenDecimals(address) {
1218
- const decimals = TOKEN_DECIMALS[address.toLowerCase()];
1219
- if (decimals === void 0) {
1220
- console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
1221
- return 18;
1222
- }
1223
- return decimals;
1224
- }
1225
- var TOKEN_TO_COINGECKO = {
1226
- // Core
1227
- "0x4200000000000000000000000000000000000006": "ethereum",
1228
- // WETH
1229
- [NATIVE_ETH.toLowerCase()]: "ethereum",
1230
- "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
1231
- // USDC
1232
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
1233
- // USDbC
1234
- "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
1235
- // cbETH
1236
- "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
1237
- // DAI
1238
- // Established
1239
- "0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
1240
- // AERO
1241
- "0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
1242
- // BRETT
1243
- "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
1244
- // DEGEN
1245
- "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
1246
- // VIRTUAL
1247
- "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
1248
- // TOSHI
1249
- "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
1250
- // cbBTC
1251
- "0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
1252
- // ezETH
1253
- "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
1254
- // wstETH
1255
- };
1256
- var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
1257
- var PRICE_STALENESS_MS = 6e4;
1258
- var MarketDataService = class {
1259
- rpcUrl;
1260
- client;
1261
- /** Cached prices from last fetch */
1262
- cachedPrices = {};
1263
- /** Timestamp of last successful price fetch */
1264
- lastPriceFetchAt = 0;
1265
- constructor(rpcUrl) {
1266
- this.rpcUrl = rpcUrl;
1267
- this.client = (0, import_viem.createPublicClient)({
1268
- transport: (0, import_viem.http)(rpcUrl)
1269
- });
1270
- }
1271
- /** Cached volume data */
1272
- cachedVolume24h = {};
1273
- /** Cached price change data */
1274
- cachedPriceChange24h = {};
1275
- /**
1276
- * Fetch current market data for the agent
1277
- */
1278
- async fetchMarketData(walletAddress, tokenAddresses) {
1279
- const prices = await this.fetchPrices(tokenAddresses);
1280
- const balances = await this.fetchBalances(walletAddress, tokenAddresses);
1281
- const portfolioValue = this.calculatePortfolioValue(balances, prices);
1282
- let gasPrice;
1283
- try {
1284
- gasPrice = await this.client.getGasPrice();
1285
- } catch {
1286
- }
1287
- return {
1288
- timestamp: Date.now(),
1289
- prices,
1290
- balances,
1291
- portfolioValue,
1292
- volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
1293
- priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
1294
- gasPrice,
1295
- network: {
1296
- chainId: this.client.chain?.id ?? 8453
1297
- }
1298
- };
1299
- }
1300
- /**
1301
- * Check if cached prices are still fresh
1302
- */
1303
- get pricesAreFresh() {
1304
- return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
1305
- }
1306
- /**
1307
- * Fetch token prices from CoinGecko free API
1308
- * Returns cached prices if still fresh (<60s old)
1309
- */
1310
- async fetchPrices(tokenAddresses) {
1311
- if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
1312
- const prices2 = { ...this.cachedPrices };
1313
- for (const addr of tokenAddresses) {
1314
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1315
- if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
1316
- prices2[addr.toLowerCase()] = 1;
1317
- }
1318
- }
1319
- return prices2;
1320
- }
1321
- const prices = {};
1322
- const idsToFetch = /* @__PURE__ */ new Set();
1323
- for (const addr of tokenAddresses) {
1324
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1325
- if (cgId && !STABLECOIN_IDS.has(cgId)) {
1326
- idsToFetch.add(cgId);
1327
- }
1328
- }
1329
- idsToFetch.add("ethereum");
1330
- if (idsToFetch.size > 0) {
1331
- try {
1332
- const ids = Array.from(idsToFetch).join(",");
1333
- const response = await fetch(
1334
- `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
1335
- { signal: AbortSignal.timeout(5e3) }
1336
- );
1337
- if (response.ok) {
1338
- const data = await response.json();
1339
- for (const [cgId, priceData] of Object.entries(data)) {
1340
- for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
1341
- if (id === cgId) {
1342
- const key = addr.toLowerCase();
1343
- prices[key] = priceData.usd;
1344
- if (priceData.usd_24h_vol !== void 0) {
1345
- this.cachedVolume24h[key] = priceData.usd_24h_vol;
1346
- }
1347
- if (priceData.usd_24h_change !== void 0) {
1348
- this.cachedPriceChange24h[key] = priceData.usd_24h_change;
1349
- }
1350
- }
1351
- }
1352
- }
1353
- this.lastPriceFetchAt = Date.now();
1354
- } else {
1355
- console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
1356
- }
1357
- } catch (error) {
1358
- console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
1359
- }
1360
- }
1361
- for (const addr of tokenAddresses) {
1362
- const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1363
- if (cgId && STABLECOIN_IDS.has(cgId)) {
1364
- prices[addr.toLowerCase()] = 1;
1365
- }
1594
+ }
1595
+ if (config.llm.provider === "custom" && !config.llm.endpoint) {
1596
+ throw new Error("Endpoint required for custom LLM provider");
1597
+ }
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
1366
1632
  }
1367
- const missingAddrs = tokenAddresses.filter(
1368
- (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())
1369
1648
  );
1370
- if (missingAddrs.length > 0) {
1371
- try {
1372
- const coins = missingAddrs.map((a) => `base:${a}`).join(",");
1373
- const llamaResponse = await fetch(
1374
- `https://coins.llama.fi/prices/current/${coins}`,
1375
- { signal: AbortSignal.timeout(5e3) }
1376
- );
1377
- if (llamaResponse.ok) {
1378
- const llamaData = await llamaResponse.json();
1379
- for (const [key, data] of Object.entries(llamaData.coins)) {
1380
- const addr = key.replace("base:", "").toLowerCase();
1381
- if (data.price && data.confidence > 0.5) {
1382
- prices[addr] = data.price;
1383
- }
1384
- }
1385
- if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
1386
- }
1387
- } catch {
1388
- }
1389
- }
1390
- if (Object.keys(prices).length > 0) {
1391
- this.cachedPrices = prices;
1392
- }
1393
- if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
1394
- console.warn("Using cached prices (last successful fetch was stale)");
1395
- return { ...this.cachedPrices };
1396
- }
1397
- for (const addr of tokenAddresses) {
1398
- if (!prices[addr.toLowerCase()]) {
1399
- console.warn(`No price available for ${addr}, using 0`);
1400
- prices[addr.toLowerCase()] = 0;
1401
- }
1402
- }
1403
- return prices;
1404
1649
  }
1405
1650
  /**
1406
- * Fetch real on-chain balances: native ETH + ERC-20 tokens
1651
+ * Execute a single trade signal
1407
1652
  */
1408
- async fetchBalances(walletAddress, tokenAddresses) {
1409
- const balances = {};
1410
- const wallet = walletAddress;
1653
+ async execute(signal) {
1654
+ if (signal.action === "hold") {
1655
+ return { success: true };
1656
+ }
1411
1657
  try {
1412
- const nativeBalance = await this.client.getBalance({ address: wallet });
1413
- balances[NATIVE_ETH.toLowerCase()] = nativeBalance;
1414
- const erc20Promises = tokenAddresses.map(async (tokenAddress) => {
1415
- try {
1416
- const balance = await this.client.readContract({
1417
- address: tokenAddress,
1418
- abi: import_viem.erc20Abi,
1419
- functionName: "balanceOf",
1420
- args: [wallet]
1421
- });
1422
- return { address: tokenAddress.toLowerCase(), balance };
1423
- } catch (error) {
1424
- return { address: tokenAddress.toLowerCase(), balance: 0n };
1425
- }
1426
- });
1427
- const results = await Promise.all(erc20Promises);
1428
- for (const { address, balance } of results) {
1429
- 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" };
1430
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 };
1431
1673
  } catch (error) {
1432
- console.error("MarketData: Failed to fetch balances:", error instanceof Error ? error.message : error);
1433
- balances[NATIVE_ETH.toLowerCase()] = 0n;
1434
- for (const address of tokenAddresses) {
1435
- 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);
1436
1691
  }
1437
1692
  }
1438
- return balances;
1693
+ return results;
1439
1694
  }
1440
1695
  /**
1441
- * Calculate total portfolio value in USD
1696
+ * Validate a signal against config limits and token restrictions
1442
1697
  */
1443
- calculatePortfolioValue(balances, prices) {
1444
- let total = 0;
1445
- for (const [address, balance] of Object.entries(balances)) {
1446
- const price = prices[address.toLowerCase()] || 0;
1447
- const decimals = getTokenDecimals(address);
1448
- const amount = Number(balance) / Math.pow(10, decimals);
1449
- 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;
1450
1702
  }
1451
- 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));
1452
1719
  }
1453
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
+ }
1454
1776
 
1455
1777
  // src/trading/risk.ts
1456
1778
  var RiskManager = class {
@@ -1482,12 +1804,21 @@ var RiskManager = class {
1482
1804
  return signals.filter((signal) => this.validateSignal(signal, marketData));
1483
1805
  }
1484
1806
  /**
1485
- * Validate individual signal against risk limits
1807
+ * Validate individual signal against risk limits.
1808
+ * Sell signals are exempt from position size and minimum value checks —
1809
+ * those guardrails prevent oversized/dust buys, but blocking exits traps capital.
1486
1810
  */
1487
1811
  validateSignal(signal, marketData) {
1488
1812
  if (signal.action === "hold") {
1489
1813
  return true;
1490
1814
  }
1815
+ if (signal.confidence < 0.5) {
1816
+ console.warn(`Signal confidence too low: ${signal.confidence}`);
1817
+ return false;
1818
+ }
1819
+ if (signal.action === "sell") {
1820
+ return true;
1821
+ }
1491
1822
  const signalValue = this.estimateSignalValue(signal, marketData);
1492
1823
  const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
1493
1824
  if (signalValue > maxPositionValue) {
@@ -1496,11 +1827,7 @@ var RiskManager = class {
1496
1827
  );
1497
1828
  return false;
1498
1829
  }
1499
- if (signal.confidence < 0.5) {
1500
- console.warn(`Signal confidence too low: ${signal.confidence}`);
1501
- return false;
1502
- }
1503
- if (signal.action === "buy" && this.config.maxConcurrentPositions) {
1830
+ if (this.config.maxConcurrentPositions) {
1504
1831
  const activePositions = this.countActivePositions(marketData);
1505
1832
  if (activePositions >= this.config.maxConcurrentPositions) {
1506
1833
  console.warn(
@@ -1557,6 +1884,31 @@ var RiskManager = class {
1557
1884
  updateFees(fees) {
1558
1885
  this.dailyFees += fees;
1559
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
+ }
1560
1912
  /**
1561
1913
  * Get current risk status
1562
1914
  * @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
@@ -1670,6 +2022,55 @@ var RiskManager = class {
1670
2022
  }
1671
2023
  };
1672
2024
 
2025
+ // src/store.ts
2026
+ var import_fs3 = require("fs");
2027
+ var import_path3 = require("path");
2028
+ var FileStore = class {
2029
+ data = {};
2030
+ filePath;
2031
+ dirty = false;
2032
+ constructor(dataDir) {
2033
+ const dir = dataDir || (0, import_path3.join)(process.cwd(), "data");
2034
+ this.filePath = (0, import_path3.join)(dir, "strategy-store.json");
2035
+ if (!(0, import_fs3.existsSync)(dir)) {
2036
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
2037
+ }
2038
+ if ((0, import_fs3.existsSync)(this.filePath)) {
2039
+ try {
2040
+ const raw = (0, import_fs3.readFileSync)(this.filePath, "utf-8");
2041
+ this.data = JSON.parse(raw);
2042
+ } catch {
2043
+ this.data = {};
2044
+ }
2045
+ }
2046
+ }
2047
+ get(key) {
2048
+ return this.data[key];
2049
+ }
2050
+ set(key, value) {
2051
+ this.data[key] = value;
2052
+ this.dirty = true;
2053
+ this.flush();
2054
+ }
2055
+ delete(key) {
2056
+ delete this.data[key];
2057
+ this.dirty = true;
2058
+ this.flush();
2059
+ }
2060
+ keys() {
2061
+ return Object.keys(this.data);
2062
+ }
2063
+ flush() {
2064
+ if (!this.dirty) return;
2065
+ try {
2066
+ (0, import_fs3.writeFileSync)(this.filePath, JSON.stringify(this.data, null, 2), "utf-8");
2067
+ this.dirty = false;
2068
+ } catch (error) {
2069
+ console.warn("Failed to persist strategy store:", error instanceof Error ? error.message : error);
2070
+ }
2071
+ }
2072
+ };
2073
+
1673
2074
  // src/vault/manager.ts
1674
2075
  var import_viem2 = require("viem");
1675
2076
  var import_accounts = require("viem/accounts");
@@ -1971,7 +2372,6 @@ var VaultManager = class {
1971
2372
  // src/relay.ts
1972
2373
  var import_ws = __toESM(require("ws"));
1973
2374
  var import_accounts2 = require("viem/accounts");
1974
- var import_sdk = require("@exagent/sdk");
1975
2375
  var RelayClient = class {
1976
2376
  config;
1977
2377
  ws = null;
@@ -2075,7 +2475,7 @@ var RelayClient = class {
2075
2475
  wallet: account.address,
2076
2476
  timestamp,
2077
2477
  signature,
2078
- sdkVersion: import_sdk.SDK_VERSION
2478
+ sdkVersion: AGENT_VERSION
2079
2479
  });
2080
2480
  }
2081
2481
  /**
@@ -3490,9 +3890,12 @@ var AgentRuntime = class {
3490
3890
  lastCycleAt = 0;
3491
3891
  lastPortfolioValue = 0;
3492
3892
  lastEthBalance = "0";
3893
+ lastPrices = {};
3493
3894
  processAlive = true;
3494
3895
  riskUniverse = 0;
3495
3896
  allowedTokens = /* @__PURE__ */ new Set();
3897
+ strategyContext;
3898
+ positionTracker;
3496
3899
  // Perp trading components (null if perp not enabled)
3497
3900
  perpClient = null;
3498
3901
  perpSigner = null;
@@ -3517,7 +3920,7 @@ var AgentRuntime = class {
3517
3920
  */
3518
3921
  async initialize() {
3519
3922
  console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
3520
- this.client = new import_sdk2.ExagentClient({
3923
+ this.client = new import_sdk.ExagentClient({
3521
3924
  privateKey: this.config.privateKey,
3522
3925
  network: this.config.network
3523
3926
  });
@@ -3535,9 +3938,20 @@ var AgentRuntime = class {
3535
3938
  console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
3536
3939
  await this.syncConfigHash();
3537
3940
  this.strategy = await loadStrategy();
3941
+ const store = new FileStore();
3942
+ this.strategyContext = {
3943
+ store,
3944
+ agentId: Number(this.config.agentId),
3945
+ walletAddress: this.client.address
3946
+ };
3947
+ this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
3538
3948
  this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
3539
3949
  this.riskManager = new RiskManager(this.config.trading);
3540
3950
  this.marketData = new MarketDataService(this.getRpcUrl());
3951
+ const savedRisk = this.positionTracker.getRiskState();
3952
+ if (savedRisk.lastResetDate) {
3953
+ this.riskManager.restoreState(savedRisk);
3954
+ }
3541
3955
  await this.initializeVaultManager();
3542
3956
  await this.initializePerp();
3543
3957
  await this.initializeRelay();
@@ -3723,7 +4137,7 @@ var AgentRuntime = class {
3723
4137
  if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
3724
4138
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
3725
4139
  const nonce = await this.client.registry.getNonce(address);
3726
- const linkMessage = import_sdk2.ExagentRegistry.generateLinkMessage(
4140
+ const linkMessage = import_sdk.ExagentRegistry.generateLinkMessage(
3727
4141
  address,
3728
4142
  agentId,
3729
4143
  nonce
@@ -3814,18 +4228,17 @@ var AgentRuntime = class {
3814
4228
  async syncConfigHash() {
3815
4229
  const agentId = BigInt(this.config.agentId);
3816
4230
  const llmMeta = this.llm.getMetadata();
3817
- this.configHash = import_sdk2.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
4231
+ this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
3818
4232
  console.log(`Config hash: ${this.configHash}`);
3819
4233
  const onChainHash = await this.client.registry.getConfigHash(agentId);
3820
4234
  if (onChainHash !== this.configHash) {
3821
4235
  console.log("Config changed, updating on-chain...");
3822
4236
  try {
3823
4237
  await this.client.registry.updateConfig(agentId, this.configHash);
3824
- const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
3825
- console.log(`Config updated, new epoch started: ${newEpoch}`);
4238
+ console.log(`Config updated on-chain`);
3826
4239
  } catch (error) {
3827
4240
  const message = error instanceof Error ? error.message : String(error);
3828
- if (message.includes("insufficient funds") || message.includes("gas") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
4241
+ if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
3829
4242
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
3830
4243
  const chain = import_chains4.base;
3831
4244
  const publicClientInstance = (0, import_viem6.createPublicClient)({
@@ -3853,19 +4266,18 @@ var AgentRuntime = class {
3853
4266
  console.log(" ETH detected! Retrying config update...");
3854
4267
  console.log("");
3855
4268
  await this.client.registry.updateConfig(agentId, this.configHash);
3856
- const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
3857
- console.log(`Config updated, new epoch started: ${newEpoch}`);
4269
+ console.log(`Config updated on-chain`);
3858
4270
  return;
3859
4271
  }
3860
4272
  process.stdout.write(".");
3861
4273
  }
3862
4274
  } else {
3863
- throw error;
4275
+ console.warn(`Config update skipped (continuing with on-chain config): ${message}`);
4276
+ this.configHash = onChainHash;
3864
4277
  }
3865
4278
  }
3866
4279
  } else {
3867
- const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
3868
- console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
4280
+ console.log("Config hash matches on-chain");
3869
4281
  }
3870
4282
  }
3871
4283
  /**
@@ -3999,6 +4411,10 @@ var AgentRuntime = class {
3999
4411
  }
4000
4412
  if (updated) {
4001
4413
  this.riskManager = new RiskManager(this.config.trading);
4414
+ const savedRiskState = this.positionTracker.getRiskState();
4415
+ if (savedRiskState.lastResetDate) {
4416
+ this.riskManager.restoreState(savedRiskState);
4417
+ }
4002
4418
  console.log("Risk params updated via command center");
4003
4419
  this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
4004
4420
  this.relay?.sendMessage(
@@ -4202,6 +4618,7 @@ var AgentRuntime = class {
4202
4618
  mode: this.mode,
4203
4619
  agentId: String(this.config.agentId),
4204
4620
  wallet: this.client?.address,
4621
+ sdkVersion: AGENT_VERSION,
4205
4622
  cycleCount: this.cycleCount,
4206
4623
  lastCycleAt: this.lastCycleAt,
4207
4624
  tradingIntervalMs: this.config.trading.tradingIntervalMs,
@@ -4230,7 +4647,8 @@ var AgentRuntime = class {
4230
4647
  openPositions: 0,
4231
4648
  effectiveLeverage: 0,
4232
4649
  pendingRecords: this.perpRecorder?.pendingRetries ?? 0
4233
- } : void 0
4650
+ } : void 0,
4651
+ positions: this.positionTracker ? this.positionTracker.getPositionSummary(this.lastPrices) : void 0
4234
4652
  };
4235
4653
  if (this.perpConnected && this.perpPositions && status.perp) {
4236
4654
  this.perpPositions.getAccountSummary().then((account) => {
@@ -4261,17 +4679,22 @@ var AgentRuntime = class {
4261
4679
  const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
4262
4680
  console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
4263
4681
  this.lastPortfolioValue = marketData.portfolioValue;
4682
+ this.lastPrices = marketData.prices;
4264
4683
  const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4265
4684
  this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
4685
+ this.positionTracker.syncBalances(marketData.balances, marketData.prices);
4266
4686
  const fundsOk = this.checkFundsLow(marketData);
4267
4687
  if (!fundsOk) {
4268
4688
  console.warn("Skipping trading cycle \u2014 ETH balance critically low");
4269
4689
  this.sendRelayStatus();
4270
4690
  return;
4271
4691
  }
4692
+ this.strategyContext.positions = this.positionTracker.getPositions();
4693
+ this.strategyContext.tradeHistory = this.positionTracker.getTradeHistory(20);
4694
+ this.strategyContext.positionTracker = this.positionTracker;
4272
4695
  let signals;
4273
4696
  try {
4274
- signals = await this.strategy(marketData, this.llm, this.config);
4697
+ signals = await this.strategy(marketData, this.llm, this.config, this.strategyContext);
4275
4698
  } catch (error) {
4276
4699
  const message = error instanceof Error ? error.message : String(error);
4277
4700
  console.error("LLM/strategy error:", message);
@@ -4334,13 +4757,30 @@ var AgentRuntime = class {
4334
4757
  );
4335
4758
  }
4336
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
+ }
4337
4775
  const postTokens = this.config.allowedTokens || this.getDefaultTokens();
4338
4776
  const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
4339
4777
  const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
4340
4778
  this.riskManager.updatePnL(marketPnL);
4779
+ this.positionTracker.saveRiskState(this.riskManager.exportState());
4341
4780
  if (marketPnL !== 0) {
4342
4781
  console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
4343
4782
  }
4783
+ this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
4344
4784
  this.lastPortfolioValue = postTradeData.portfolioValue;
4345
4785
  const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4346
4786
  this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
@@ -4796,13 +5236,18 @@ function loadSecureEnv(basePath, passphrase) {
4796
5236
  }
4797
5237
  return false;
4798
5238
  }
5239
+
5240
+ // src/index.ts
5241
+ var AGENT_VERSION = "0.1.21";
4799
5242
  // Annotate the CommonJS export names for ESM import in node:
4800
5243
  0 && (module.exports = {
5244
+ AGENT_VERSION,
4801
5245
  AgentConfigSchema,
4802
5246
  AgentRuntime,
4803
5247
  AnthropicAdapter,
4804
5248
  BaseLLMAdapter,
4805
5249
  DeepSeekAdapter,
5250
+ FileStore,
4806
5251
  GoogleAdapter,
4807
5252
  GroqAdapter,
4808
5253
  HYPERLIQUID_DOMAIN,
@@ -4820,6 +5265,7 @@ function loadSecureEnv(basePath, passphrase) {
4820
5265
  PerpOnboarding,
4821
5266
  PerpTradeRecorder,
4822
5267
  PositionManager,
5268
+ PositionTracker,
4823
5269
  RelayClient,
4824
5270
  RelayConfigSchema,
4825
5271
  RiskManager,