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