@exagent/agent 0.1.20 → 0.1.22

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,105 +84,641 @@ __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
 
90
- // src/llm/openai.ts
91
- var import_openai = __toESM(require("openai"));
92
-
93
- // src/llm/base.ts
94
- var BaseLLMAdapter = class {
95
- config;
96
- constructor(config) {
97
- 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;
98
132
  }
99
- 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
+ }
100
197
  return {
101
- provider: this.config.provider,
102
- model: this.config.model || "unknown",
103
- 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
+ }
104
208
  };
105
209
  }
106
210
  /**
107
- * Format model name for display
211
+ * Check if cached prices are still fresh
108
212
  */
109
- getDisplayModel() {
110
- if (this.config.provider === "ollama") {
111
- return `Local (${this.config.model || "ollama"})`;
112
- }
113
- return this.config.model || this.config.provider;
213
+ get pricesAreFresh() {
214
+ return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
114
215
  }
115
- };
116
-
117
- // src/llm/openai.ts
118
- var OpenAIAdapter = class extends BaseLLMAdapter {
119
- client;
120
- constructor(config) {
121
- super(config);
122
- if (!config.apiKey && !config.endpoint) {
123
- 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;
124
230
  }
125
- this.client = new import_openai.default({
126
- apiKey: config.apiKey || "not-needed-for-custom",
127
- baseURL: config.endpoint
128
- });
129
- }
130
- async chat(messages) {
131
- try {
132
- const response = await this.client.chat.completions.create({
133
- model: this.config.model || "gpt-4.1",
134
- messages: messages.map((m) => ({
135
- role: m.role,
136
- content: m.content
137
- })),
138
- temperature: this.config.temperature,
139
- max_tokens: this.config.maxTokens
140
- });
141
- const choice = response.choices[0];
142
- if (!choice || !choice.message) {
143
- 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);
144
237
  }
145
- return {
146
- content: choice.message.content || "",
147
- usage: response.usage ? {
148
- promptTokens: response.usage.prompt_tokens,
149
- completionTokens: response.usage.completion_tokens,
150
- totalTokens: response.usage.total_tokens
151
- } : void 0
152
- };
153
- } catch (error) {
154
- if (error instanceof import_openai.default.APIError) {
155
- 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);
156
269
  }
157
- throw error;
158
270
  }
159
- }
160
- };
161
-
162
- // src/llm/anthropic.ts
163
- var AnthropicAdapter = class extends BaseLLMAdapter {
164
- apiKey;
165
- baseUrl;
166
- constructor(config) {
167
- super(config);
168
- if (!config.apiKey) {
169
- 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
+ }
170
276
  }
171
- this.apiKey = config.apiKey;
172
- this.baseUrl = config.endpoint || "https://api.anthropic.com";
173
- }
174
- async chat(messages) {
175
- const systemMessage = messages.find((m) => m.role === "system");
176
- const chatMessages = messages.filter((m) => m.role !== "system");
177
- const body = {
178
- model: this.config.model || "claude-opus-4-5-20251101",
179
- max_tokens: this.config.maxTokens || 4096,
180
- temperature: this.config.temperature,
181
- system: systemMessage?.content,
182
- messages: chatMessages.map((m) => ({
183
- 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,
184
722
  content: m.content
185
723
  }))
186
724
  };
@@ -1073,441 +1611,168 @@ function createSampleConfig(agentId, name) {
1073
1611
  llm: {
1074
1612
  provider: "openai",
1075
1613
  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
- };
1190
- }
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
- };
1196
- }
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
- };
1202
- }
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
- };
1208
- }
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
- };
1214
- }
1215
- if (lower.includes("aggregatornotwhitelisted")) {
1216
- return {
1217
- category: "aggregator_error",
1218
- userMessage: "DEX aggregator not whitelisted on the router. Contact support."
1219
- };
1220
- }
1221
- if (lower.includes("reverted") || lower.includes("execution reverted")) {
1222
- return {
1223
- category: "reverted",
1224
- userMessage: `Transaction reverted: ${message.slice(0, 200)}`
1225
- };
1226
- }
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
- };
1232
- }
1233
- return {
1234
- category: "unknown",
1235
- userMessage: message.slice(0, 300)
1236
- };
1237
- }
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;
1279
- }
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
- });
1327
- }
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
- };
1356
- }
1357
- /**
1358
- * Check if cached prices are still fresh
1359
- */
1360
- get pricesAreFresh() {
1361
- return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
1362
- }
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
- }
1423
- }
1424
- const missingAddrs = tokenAddresses.filter(
1425
- (addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
1426
- );
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
- }
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
1459
1632
  }
1460
- return prices;
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())
1648
+ );
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
  /**
@@ -3597,14 +3886,19 @@ var AgentRuntime = class {
3597
3886
  isRunning = false;
3598
3887
  mode = "idle";
3599
3888
  configHash;
3889
+ pendingConfigHash = null;
3890
+ lastConfigCheckAt = 0;
3891
+ // Timestamp of last pending config RPC check
3600
3892
  cycleCount = 0;
3601
3893
  lastCycleAt = 0;
3602
3894
  lastPortfolioValue = 0;
3603
3895
  lastEthBalance = "0";
3896
+ lastPrices = {};
3604
3897
  processAlive = true;
3605
3898
  riskUniverse = 0;
3606
3899
  allowedTokens = /* @__PURE__ */ new Set();
3607
3900
  strategyContext;
3901
+ positionTracker;
3608
3902
  // Perp trading components (null if perp not enabled)
3609
3903
  perpClient = null;
3610
3904
  perpSigner = null;
@@ -3621,6 +3915,12 @@ var AgentRuntime = class {
3621
3915
  // When perpConnected && perpTradingActive: dedicated runPerpCycle() runs every interval
3622
3916
  perpConnected = false;
3623
3917
  perpTradingActive = false;
3918
+ // Cached perp account data for synchronous heartbeat inclusion (refreshed async)
3919
+ cachedPerpEquity = 0;
3920
+ cachedPerpUnrealizedPnl = 0;
3921
+ cachedPerpMarginUsed = 0;
3922
+ cachedPerpLeverage = 0;
3923
+ cachedPerpOpenPositions = 0;
3624
3924
  constructor(config) {
3625
3925
  this.config = config;
3626
3926
  }
@@ -3629,7 +3929,7 @@ var AgentRuntime = class {
3629
3929
  */
3630
3930
  async initialize() {
3631
3931
  console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
3632
- this.client = new import_sdk2.ExagentClient({
3932
+ this.client = new import_sdk.ExagentClient({
3633
3933
  privateKey: this.config.privateKey,
3634
3934
  network: this.config.network
3635
3935
  });
@@ -3647,14 +3947,20 @@ var AgentRuntime = class {
3647
3947
  console.log(`LLM ready: ${llmMeta.provider} (${llmMeta.model})`);
3648
3948
  await this.syncConfigHash();
3649
3949
  this.strategy = await loadStrategy();
3950
+ const store = new FileStore();
3650
3951
  this.strategyContext = {
3651
- store: new FileStore(),
3952
+ store,
3652
3953
  agentId: Number(this.config.agentId),
3653
3954
  walletAddress: this.client.address
3654
3955
  };
3956
+ this.positionTracker = new PositionTracker(store, { maxTradeHistory: 50 });
3655
3957
  this.executor = new TradeExecutor(this.client, this.config, () => this.getConfigHash());
3656
3958
  this.riskManager = new RiskManager(this.config.trading);
3657
3959
  this.marketData = new MarketDataService(this.getRpcUrl());
3960
+ const savedRisk = this.positionTracker.getRiskState();
3961
+ if (savedRisk.lastResetDate) {
3962
+ this.riskManager.restoreState(savedRisk);
3963
+ }
3658
3964
  await this.initializeVaultManager();
3659
3965
  await this.initializePerp();
3660
3966
  await this.initializeRelay();
@@ -3840,7 +4146,7 @@ var AgentRuntime = class {
3840
4146
  if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
3841
4147
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
3842
4148
  const nonce = await this.client.registry.getNonce(address);
3843
- const linkMessage = import_sdk2.ExagentRegistry.generateLinkMessage(
4149
+ const linkMessage = import_sdk.ExagentRegistry.generateLinkMessage(
3844
4150
  address,
3845
4151
  agentId,
3846
4152
  nonce
@@ -3925,65 +4231,104 @@ var AgentRuntime = class {
3925
4231
  }
3926
4232
  /**
3927
4233
  * Sync the LLM config hash to chain for epoch tracking.
3928
- * If the wallet has insufficient gas, enters a recovery loop
3929
- * that waits for the user to fund the wallet.
4234
+ *
4235
+ * If the trading wallet is NOT the agent owner, the on-chain setConfig
4236
+ * call would revert with AgentNotOwner. Instead, we set a pending state
4237
+ * and send a message to the command center so the owner can approve it
4238
+ * from the website with one click.
4239
+ *
4240
+ * Until the config is verified on-chain, the agent won't appear on the
4241
+ * leaderboard (trades still execute normally).
3930
4242
  */
3931
4243
  async syncConfigHash() {
3932
4244
  const agentId = BigInt(this.config.agentId);
3933
4245
  const llmMeta = this.llm.getMetadata();
3934
- this.configHash = import_sdk2.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
4246
+ this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
3935
4247
  console.log(`Config hash: ${this.configHash}`);
3936
4248
  const onChainHash = await this.client.registry.getConfigHash(agentId);
3937
- if (onChainHash !== this.configHash) {
3938
- console.log("Config changed, updating on-chain...");
3939
- try {
3940
- 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}`);
3943
- } catch (error) {
3944
- const message = error instanceof Error ? error.message : String(error);
3945
- if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
3946
- const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
3947
- const chain = import_chains4.base;
3948
- const publicClientInstance = (0, import_viem6.createPublicClient)({
3949
- chain,
3950
- transport: (0, import_viem6.http)(this.getRpcUrl())
4249
+ if (onChainHash === this.configHash) {
4250
+ console.log("Config hash matches on-chain");
4251
+ this.pendingConfigHash = null;
4252
+ return;
4253
+ }
4254
+ console.log("Config changed, updating on-chain...");
4255
+ const agent = await this.client.registry.getAgent(agentId);
4256
+ const isOwner = agent?.owner.toLowerCase() === this.client.address.toLowerCase();
4257
+ if (!isOwner) {
4258
+ this.pendingConfigHash = this.configHash;
4259
+ this.configHash = onChainHash;
4260
+ console.log("");
4261
+ console.log("=== CONFIG VERIFICATION NEEDED ===");
4262
+ console.log("");
4263
+ console.log(" Your trading wallet cannot update the LLM config on-chain.");
4264
+ console.log(" The owner must approve this from the command center.");
4265
+ console.log("");
4266
+ console.log(` LLM: ${llmMeta.provider} / ${llmMeta.model}`);
4267
+ console.log(` Hash: ${this.pendingConfigHash}`);
4268
+ console.log("");
4269
+ console.log(" Until approved:");
4270
+ console.log(" - New agents will not appear on the leaderboard");
4271
+ console.log(" - Trades will still execute normally");
4272
+ console.log("");
4273
+ this.relay?.sendMessage(
4274
+ "system",
4275
+ "warning",
4276
+ "Config Verification Needed",
4277
+ `Your agent is using ${llmMeta.provider}/${llmMeta.model} but this hasn't been recorded on-chain. Open the command center and click "Approve Config" to verify. Until then, your agent won't appear on the leaderboard.`,
4278
+ { configHash: this.pendingConfigHash, provider: llmMeta.provider, model: llmMeta.model }
4279
+ );
4280
+ return;
4281
+ }
4282
+ try {
4283
+ await this.client.registry.updateConfig(agentId, this.configHash);
4284
+ console.log(`Config updated on-chain`);
4285
+ this.pendingConfigHash = null;
4286
+ } catch (error) {
4287
+ const message = error instanceof Error ? error.message : String(error);
4288
+ if (message.includes("insufficient funds") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
4289
+ const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
4290
+ const publicClientInstance = (0, import_viem6.createPublicClient)({
4291
+ chain: import_chains4.base,
4292
+ transport: (0, import_viem6.http)(this.getRpcUrl())
4293
+ });
4294
+ console.log("");
4295
+ console.log("=== ETH NEEDED FOR GAS ===");
4296
+ console.log("");
4297
+ console.log(` Wallet: ${this.client.address}`);
4298
+ console.log(" Your wallet needs ETH to pay for transaction gas.");
4299
+ console.log(" Opening the command center to fund your wallet...");
4300
+ console.log(` ${ccUrl}`);
4301
+ console.log("");
4302
+ openBrowser(ccUrl);
4303
+ console.log(" Waiting for ETH... (checking every 15s)");
4304
+ console.log(" Press Ctrl+C to exit.");
4305
+ console.log("");
4306
+ while (true) {
4307
+ await this.sleep(15e3);
4308
+ const balance = await publicClientInstance.getBalance({
4309
+ address: this.client.address
3951
4310
  });
3952
- console.log("");
3953
- console.log("=== ETH NEEDED FOR GAS ===");
3954
- console.log("");
3955
- console.log(` Wallet: ${this.client.address}`);
3956
- console.log(" Your wallet needs ETH to pay for transaction gas.");
3957
- console.log(" Opening the command center to fund your wallet...");
3958
- console.log(` ${ccUrl}`);
3959
- console.log("");
3960
- openBrowser(ccUrl);
3961
- console.log(" Waiting for ETH... (checking every 15s)");
3962
- console.log(" Press Ctrl+C to exit.");
3963
- console.log("");
3964
- while (true) {
3965
- await this.sleep(15e3);
3966
- const balance = await publicClientInstance.getBalance({
3967
- address: this.client.address
3968
- });
3969
- if (balance > BigInt(0)) {
3970
- console.log(" ETH detected! Retrying config update...");
3971
- console.log("");
4311
+ if (balance > BigInt(0)) {
4312
+ console.log(" ETH detected! Retrying config update...");
4313
+ console.log("");
4314
+ try {
3972
4315
  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}`);
3975
- return;
4316
+ console.log(`Config updated on-chain`);
4317
+ this.pendingConfigHash = null;
4318
+ } catch (retryError) {
4319
+ const retryMsg = retryError instanceof Error ? retryError.message : String(retryError);
4320
+ console.warn(`Config update failed after funding: ${retryMsg}`);
4321
+ console.warn("Continuing with on-chain config.");
4322
+ this.configHash = onChainHash;
3976
4323
  }
3977
- process.stdout.write(".");
4324
+ return;
3978
4325
  }
3979
- } else {
3980
- console.warn(`Config update skipped (continuing with on-chain config): ${message}`);
3981
- this.configHash = onChainHash;
4326
+ process.stdout.write(".");
3982
4327
  }
4328
+ } else {
4329
+ console.warn(`Config update skipped (continuing with on-chain config): ${message}`);
4330
+ this.configHash = onChainHash;
3983
4331
  }
3984
- } else {
3985
- const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
3986
- console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
3987
4332
  }
3988
4333
  }
3989
4334
  /**
@@ -4117,6 +4462,10 @@ var AgentRuntime = class {
4117
4462
  }
4118
4463
  if (updated) {
4119
4464
  this.riskManager = new RiskManager(this.config.trading);
4465
+ const savedRiskState = this.positionTracker.getRiskState();
4466
+ if (savedRiskState.lastResetDate) {
4467
+ this.riskManager.restoreState(savedRiskState);
4468
+ }
4120
4469
  console.log("Risk params updated via command center");
4121
4470
  this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
4122
4471
  this.relay?.sendMessage(
@@ -4310,16 +4659,44 @@ var AgentRuntime = class {
4310
4659
  this.relay?.sendCommandResult(cmd.id, false, message);
4311
4660
  }
4312
4661
  }
4662
+ /**
4663
+ * Periodically check if the owner has approved the pending config hash.
4664
+ * Called from sendRelayStatus at most every 2.5 minutes (timestamp-throttled).
4665
+ */
4666
+ async checkPendingConfigApproval() {
4667
+ if (!this.pendingConfigHash) return;
4668
+ try {
4669
+ const onChain = await this.client.registry.getConfigHash(BigInt(this.config.agentId));
4670
+ if (onChain === this.pendingConfigHash) {
4671
+ this.configHash = this.pendingConfigHash;
4672
+ this.pendingConfigHash = null;
4673
+ console.log("Config verified on-chain! Your agent will now appear on the leaderboard.");
4674
+ this.relay?.sendMessage(
4675
+ "config_updated",
4676
+ "success",
4677
+ "Config Verified",
4678
+ "Your LLM config has been verified on-chain. Your agent will now appear on the leaderboard."
4679
+ );
4680
+ }
4681
+ } catch {
4682
+ }
4683
+ }
4313
4684
  /**
4314
4685
  * Send current status to the relay
4315
4686
  */
4316
4687
  sendRelayStatus() {
4317
4688
  if (!this.relay) return;
4689
+ const CONFIG_CHECK_INTERVAL_MS = 15e4;
4690
+ if (this.pendingConfigHash && Date.now() - this.lastConfigCheckAt >= CONFIG_CHECK_INTERVAL_MS) {
4691
+ this.lastConfigCheckAt = Date.now();
4692
+ this.checkPendingConfigApproval();
4693
+ }
4318
4694
  const vaultConfig = this.config.vault || { policy: "disabled" };
4319
4695
  const status = {
4320
4696
  mode: this.mode,
4321
4697
  agentId: String(this.config.agentId),
4322
4698
  wallet: this.client?.address,
4699
+ sdkVersion: AGENT_VERSION,
4323
4700
  cycleCount: this.cycleCount,
4324
4701
  lastCycleAt: this.lastCycleAt,
4325
4702
  tradingIntervalMs: this.config.trading.tradingIntervalMs,
@@ -4342,30 +4719,32 @@ var AgentRuntime = class {
4342
4719
  perp: this.perpConnected ? {
4343
4720
  enabled: true,
4344
4721
  trading: this.perpTradingActive,
4345
- equity: 0,
4346
- unrealizedPnl: 0,
4347
- marginUsed: 0,
4348
- openPositions: 0,
4349
- effectiveLeverage: 0,
4722
+ equity: this.cachedPerpEquity,
4723
+ unrealizedPnl: this.cachedPerpUnrealizedPnl,
4724
+ marginUsed: this.cachedPerpMarginUsed,
4725
+ openPositions: this.cachedPerpOpenPositions,
4726
+ effectiveLeverage: this.cachedPerpLeverage,
4350
4727
  pendingRecords: this.perpRecorder?.pendingRetries ?? 0
4351
- } : void 0
4728
+ } : void 0,
4729
+ positions: this.positionTracker ? this.positionTracker.getPositionSummary(this.lastPrices) : void 0,
4730
+ configHash: this.configHash || void 0,
4731
+ pendingConfigHash: this.pendingConfigHash
4732
+ // null preserved by JSON.stringify for clearing
4352
4733
  };
4353
- if (this.perpConnected && this.perpPositions && status.perp) {
4734
+ this.relay.sendHeartbeat(status);
4735
+ if (this.perpConnected && this.perpPositions) {
4354
4736
  this.perpPositions.getAccountSummary().then((account) => {
4355
- if (status.perp) {
4356
- status.perp.equity = account.totalEquity;
4357
- status.perp.unrealizedPnl = account.totalUnrealizedPnl;
4358
- status.perp.marginUsed = account.totalMarginUsed;
4359
- status.perp.effectiveLeverage = account.effectiveLeverage;
4360
- }
4737
+ this.cachedPerpEquity = account.totalEquity;
4738
+ this.cachedPerpUnrealizedPnl = account.totalUnrealizedPnl;
4739
+ this.cachedPerpMarginUsed = account.totalMarginUsed;
4740
+ this.cachedPerpLeverage = account.effectiveLeverage;
4361
4741
  }).catch(() => {
4362
4742
  });
4363
4743
  this.perpPositions.getPositionCount().then((count) => {
4364
- if (status.perp) status.perp.openPositions = count;
4744
+ this.cachedPerpOpenPositions = count;
4365
4745
  }).catch(() => {
4366
4746
  });
4367
4747
  }
4368
- this.relay.sendHeartbeat(status);
4369
4748
  }
4370
4749
  /**
4371
4750
  * Run a single trading cycle
@@ -4379,14 +4758,19 @@ var AgentRuntime = class {
4379
4758
  const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
4380
4759
  console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
4381
4760
  this.lastPortfolioValue = marketData.portfolioValue;
4761
+ this.lastPrices = marketData.prices;
4382
4762
  const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4383
4763
  this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
4764
+ this.positionTracker.syncBalances(marketData.balances, marketData.prices);
4384
4765
  const fundsOk = this.checkFundsLow(marketData);
4385
4766
  if (!fundsOk) {
4386
4767
  console.warn("Skipping trading cycle \u2014 ETH balance critically low");
4387
4768
  this.sendRelayStatus();
4388
4769
  return;
4389
4770
  }
4771
+ this.strategyContext.positions = this.positionTracker.getPositions();
4772
+ this.strategyContext.tradeHistory = this.positionTracker.getTradeHistory(20);
4773
+ this.strategyContext.positionTracker = this.positionTracker;
4390
4774
  let signals;
4391
4775
  try {
4392
4776
  signals = await this.strategy(marketData, this.llm, this.config, this.strategyContext);
@@ -4452,13 +4836,30 @@ var AgentRuntime = class {
4452
4836
  );
4453
4837
  }
4454
4838
  }
4839
+ for (const result of results) {
4840
+ const tokenIn = result.signal.tokenIn.toLowerCase();
4841
+ const tokenOut = result.signal.tokenOut.toLowerCase();
4842
+ this.positionTracker.recordTrade({
4843
+ action: result.signal.action,
4844
+ tokenIn,
4845
+ tokenOut,
4846
+ amountIn: result.signal.amountIn,
4847
+ priceIn: marketData.prices[tokenIn] || 0,
4848
+ priceOut: marketData.prices[tokenOut] || 0,
4849
+ txHash: result.txHash,
4850
+ reasoning: result.signal.reasoning,
4851
+ success: result.success
4852
+ });
4853
+ }
4455
4854
  const postTokens = this.config.allowedTokens || this.getDefaultTokens();
4456
4855
  const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
4457
4856
  const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
4458
4857
  this.riskManager.updatePnL(marketPnL);
4858
+ this.positionTracker.saveRiskState(this.riskManager.exportState());
4459
4859
  if (marketPnL !== 0) {
4460
4860
  console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
4461
4861
  }
4862
+ this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
4462
4863
  this.lastPortfolioValue = postTradeData.portfolioValue;
4463
4864
  const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
4464
4865
  this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
@@ -4914,8 +5315,12 @@ function loadSecureEnv(basePath, passphrase) {
4914
5315
  }
4915
5316
  return false;
4916
5317
  }
5318
+
5319
+ // src/index.ts
5320
+ var AGENT_VERSION = "0.1.22";
4917
5321
  // Annotate the CommonJS export names for ESM import in node:
4918
5322
  0 && (module.exports = {
5323
+ AGENT_VERSION,
4919
5324
  AgentConfigSchema,
4920
5325
  AgentRuntime,
4921
5326
  AnthropicAdapter,
@@ -4939,6 +5344,7 @@ function loadSecureEnv(basePath, passphrase) {
4939
5344
  PerpOnboarding,
4940
5345
  PerpTradeRecorder,
4941
5346
  PositionManager,
5347
+ PositionTracker,
4942
5348
  RelayClient,
4943
5349
  RelayConfigSchema,
4944
5350
  RiskManager,