@exagent/agent 0.1.11 → 0.1.13

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
@@ -69,7 +69,7 @@ __export(index_exports, {
69
69
  module.exports = __toCommonJS(index_exports);
70
70
 
71
71
  // src/runtime.ts
72
- var import_sdk = require("@exagent/sdk");
72
+ var import_sdk2 = require("@exagent/sdk");
73
73
  var import_viem3 = require("viem");
74
74
  var import_chains2 = require("viem/chains");
75
75
 
@@ -820,7 +820,7 @@ export const generateSignals: StrategyFunction = async (marketData, llm, config)
820
820
  riskWarnings: [
821
821
  "Custom strategies have no guardrails - you are fully responsible",
822
822
  "LLMs can hallucinate or make errors - always validate outputs",
823
- "Test thoroughly on testnet before using real funds",
823
+ "Start with small amounts before scaling up",
824
824
  "Consider edge cases: what happens if the LLM returns invalid JSON?",
825
825
  "Your prompts and strategy logic are your competitive advantage - protect them",
826
826
  "Agents may not behave exactly as expected based on your prompts"
@@ -882,9 +882,13 @@ function getAllStrategyTemplates() {
882
882
  var TradeExecutor = class {
883
883
  client;
884
884
  config;
885
+ allowedTokens;
885
886
  constructor(client, config) {
886
887
  this.client = client;
887
888
  this.config = config;
889
+ this.allowedTokens = new Set(
890
+ (config.allowedTokens || []).map((t) => t.toLowerCase())
891
+ );
888
892
  }
889
893
  /**
890
894
  * Execute a single trade signal
@@ -903,8 +907,7 @@ var TradeExecutor = class {
903
907
  tokenIn: signal.tokenIn,
904
908
  tokenOut: signal.tokenOut,
905
909
  amountIn: signal.amountIn,
906
- maxSlippageBps: 100
907
- // 1% default slippage
910
+ maxSlippageBps: this.config.trading?.maxSlippageBps ?? 100
908
911
  });
909
912
  console.log(`Trade executed: ${result.hash}`);
910
913
  return { success: true, txHash: result.hash };
@@ -930,13 +933,25 @@ var TradeExecutor = class {
930
933
  return results;
931
934
  }
932
935
  /**
933
- * Validate a signal against config limits
936
+ * Validate a signal against config limits and token restrictions
934
937
  */
935
938
  validateSignal(signal) {
936
939
  if (signal.confidence < 0.5) {
937
940
  console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
938
941
  return false;
939
942
  }
943
+ if (this.allowedTokens.size > 0) {
944
+ const tokenInAllowed = this.allowedTokens.has(signal.tokenIn.toLowerCase());
945
+ const tokenOutAllowed = this.allowedTokens.has(signal.tokenOut.toLowerCase());
946
+ if (!tokenInAllowed) {
947
+ console.warn(`Token ${signal.tokenIn} not in allowed list for this agent's risk universe \u2014 skipping`);
948
+ return false;
949
+ }
950
+ if (!tokenOutAllowed) {
951
+ console.warn(`Token ${signal.tokenOut} not in allowed list for this agent's risk universe \u2014 skipping`);
952
+ return false;
953
+ }
954
+ }
940
955
  return true;
941
956
  }
942
957
  delay(ms) {
@@ -944,103 +959,99 @@ var TradeExecutor = class {
944
959
  }
945
960
  };
946
961
 
947
- // src/trading/risk.ts
948
- var RiskManager = class {
949
- config;
950
- dailyPnL = 0;
951
- lastResetDate = "";
952
- constructor(config) {
953
- this.config = config;
954
- }
955
- /**
956
- * Filter signals through risk checks
957
- * Returns only signals that pass all guardrails
958
- */
959
- filterSignals(signals, marketData) {
960
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
961
- if (today !== this.lastResetDate) {
962
- this.dailyPnL = 0;
963
- this.lastResetDate = today;
964
- }
965
- if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
966
- console.warn("Daily loss limit reached - no new trades");
967
- return [];
968
- }
969
- return signals.filter((signal) => this.validateSignal(signal, marketData));
970
- }
971
- /**
972
- * Validate individual signal against risk limits
973
- */
974
- validateSignal(signal, marketData) {
975
- if (signal.action === "hold") {
976
- return true;
977
- }
978
- const signalValue = this.estimateSignalValue(signal, marketData);
979
- const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
980
- if (signalValue > maxPositionValue) {
981
- console.warn(
982
- `Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
983
- );
984
- return false;
985
- }
986
- if (signal.confidence < 0.5) {
987
- console.warn(`Signal confidence too low: ${signal.confidence}`);
988
- return false;
989
- }
990
- return true;
991
- }
992
- /**
993
- * Check if daily loss limit has been hit
994
- */
995
- isDailyLossLimitHit(portfolioValue) {
996
- const maxLoss = portfolioValue * this.config.maxDailyLossBps / 1e4;
997
- return this.dailyPnL < -maxLoss;
998
- }
999
- /**
1000
- * Estimate USD value of a trade signal
1001
- */
1002
- estimateSignalValue(signal, marketData) {
1003
- const price = marketData.prices[signal.tokenIn] || 0;
1004
- const amount = Number(signal.amountIn) / 1e18;
1005
- return amount * price;
1006
- }
1007
- /**
1008
- * Update daily PnL after a trade
1009
- */
1010
- updatePnL(pnl) {
1011
- this.dailyPnL += pnl;
1012
- }
1013
- /**
1014
- * Get current risk status
1015
- */
1016
- getStatus() {
1017
- return {
1018
- dailyPnL: this.dailyPnL,
1019
- dailyLossLimit: this.config.maxDailyLossBps / 100,
1020
- // As percentage
1021
- isLimitHit: this.dailyPnL < -(this.config.maxDailyLossBps / 100)
1022
- };
1023
- }
1024
- };
1025
-
1026
962
  // src/trading/market.ts
1027
963
  var import_viem = require("viem");
1028
964
  var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
1029
- var USDC_DECIMALS = {
965
+ var TOKEN_DECIMALS = {
966
+ // Base Mainnet — Core tokens
1030
967
  "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
1031
- // USDC on Base
1032
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6
1033
- // USDbC on Base
968
+ // USDC
969
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
970
+ // USDbC
971
+ "0x4200000000000000000000000000000000000006": 18,
972
+ // WETH
973
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
974
+ // DAI
975
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
976
+ // cbETH
977
+ [NATIVE_ETH.toLowerCase()]: 18,
978
+ // Native ETH
979
+ // Base Mainnet — Established tokens
980
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
981
+ // AERO (Aerodrome)
982
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
983
+ // BRETT
984
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
985
+ // DEGEN
986
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
987
+ // VIRTUAL
988
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
989
+ // TOSHI
990
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
991
+ // cbBTC
992
+ "0x2416092f143378750bb29b79ed961ab195cceea5": 18,
993
+ // ezETH (Renzo)
994
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
995
+ // wstETH (Lido)
1034
996
  };
997
+ function getTokenDecimals(address) {
998
+ const decimals = TOKEN_DECIMALS[address.toLowerCase()];
999
+ if (decimals === void 0) {
1000
+ console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
1001
+ return 18;
1002
+ }
1003
+ return decimals;
1004
+ }
1005
+ var TOKEN_TO_COINGECKO = {
1006
+ // Core
1007
+ "0x4200000000000000000000000000000000000006": "ethereum",
1008
+ // WETH
1009
+ [NATIVE_ETH.toLowerCase()]: "ethereum",
1010
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
1011
+ // USDC
1012
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
1013
+ // USDbC
1014
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
1015
+ // cbETH
1016
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
1017
+ // DAI
1018
+ // Established
1019
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
1020
+ // AERO
1021
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
1022
+ // BRETT
1023
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
1024
+ // DEGEN
1025
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
1026
+ // VIRTUAL
1027
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
1028
+ // TOSHI
1029
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
1030
+ // cbBTC
1031
+ "0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
1032
+ // ezETH
1033
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
1034
+ // wstETH
1035
+ };
1036
+ var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
1037
+ var PRICE_STALENESS_MS = 6e4;
1035
1038
  var MarketDataService = class {
1036
1039
  rpcUrl;
1037
1040
  client;
1041
+ /** Cached prices from last fetch */
1042
+ cachedPrices = {};
1043
+ /** Timestamp of last successful price fetch */
1044
+ lastPriceFetchAt = 0;
1038
1045
  constructor(rpcUrl) {
1039
1046
  this.rpcUrl = rpcUrl;
1040
1047
  this.client = (0, import_viem.createPublicClient)({
1041
1048
  transport: (0, import_viem.http)(rpcUrl)
1042
1049
  });
1043
1050
  }
1051
+ /** Cached volume data */
1052
+ cachedVolume24h = {};
1053
+ /** Cached price change data */
1054
+ cachedPriceChange24h = {};
1044
1055
  /**
1045
1056
  * Fetch current market data for the agent
1046
1057
  */
@@ -1048,31 +1059,126 @@ var MarketDataService = class {
1048
1059
  const prices = await this.fetchPrices(tokenAddresses);
1049
1060
  const balances = await this.fetchBalances(walletAddress, tokenAddresses);
1050
1061
  const portfolioValue = this.calculatePortfolioValue(balances, prices);
1062
+ let gasPrice;
1063
+ try {
1064
+ gasPrice = await this.client.getGasPrice();
1065
+ } catch {
1066
+ }
1051
1067
  return {
1052
1068
  timestamp: Date.now(),
1053
1069
  prices,
1054
1070
  balances,
1055
- portfolioValue
1071
+ portfolioValue,
1072
+ volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
1073
+ priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
1074
+ gasPrice,
1075
+ network: {
1076
+ chainId: this.client.chain?.id ?? 8453
1077
+ }
1056
1078
  };
1057
1079
  }
1058
1080
  /**
1059
- * Fetch token prices from price oracle
1081
+ * Check if cached prices are still fresh
1082
+ */
1083
+ get pricesAreFresh() {
1084
+ return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
1085
+ }
1086
+ /**
1087
+ * Fetch token prices from CoinGecko free API
1088
+ * Returns cached prices if still fresh (<60s old)
1060
1089
  */
1061
1090
  async fetchPrices(tokenAddresses) {
1091
+ if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
1092
+ const prices2 = { ...this.cachedPrices };
1093
+ for (const addr of tokenAddresses) {
1094
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1095
+ if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
1096
+ prices2[addr.toLowerCase()] = 1;
1097
+ }
1098
+ }
1099
+ return prices2;
1100
+ }
1062
1101
  const prices = {};
1063
- const knownPrices = {
1064
- // Native ETH (sentinel address)
1065
- [NATIVE_ETH.toLowerCase()]: 3500,
1066
- // WETH
1067
- "0x4200000000000000000000000000000000000006": 3500,
1068
- // USDC
1069
- "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 1,
1070
- // USDbC (bridged USDC)
1071
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 1
1072
- };
1073
- prices[NATIVE_ETH.toLowerCase()] = knownPrices[NATIVE_ETH.toLowerCase()];
1074
- for (const address of tokenAddresses) {
1075
- prices[address.toLowerCase()] = knownPrices[address.toLowerCase()] || 0;
1102
+ const idsToFetch = /* @__PURE__ */ new Set();
1103
+ for (const addr of tokenAddresses) {
1104
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1105
+ if (cgId && !STABLECOIN_IDS.has(cgId)) {
1106
+ idsToFetch.add(cgId);
1107
+ }
1108
+ }
1109
+ idsToFetch.add("ethereum");
1110
+ if (idsToFetch.size > 0) {
1111
+ try {
1112
+ const ids = Array.from(idsToFetch).join(",");
1113
+ const response = await fetch(
1114
+ `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
1115
+ { signal: AbortSignal.timeout(5e3) }
1116
+ );
1117
+ if (response.ok) {
1118
+ const data = await response.json();
1119
+ for (const [cgId, priceData] of Object.entries(data)) {
1120
+ for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
1121
+ if (id === cgId) {
1122
+ const key = addr.toLowerCase();
1123
+ prices[key] = priceData.usd;
1124
+ if (priceData.usd_24h_vol !== void 0) {
1125
+ this.cachedVolume24h[key] = priceData.usd_24h_vol;
1126
+ }
1127
+ if (priceData.usd_24h_change !== void 0) {
1128
+ this.cachedPriceChange24h[key] = priceData.usd_24h_change;
1129
+ }
1130
+ }
1131
+ }
1132
+ }
1133
+ this.lastPriceFetchAt = Date.now();
1134
+ } else {
1135
+ console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
1136
+ }
1137
+ } catch (error) {
1138
+ console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
1139
+ }
1140
+ }
1141
+ for (const addr of tokenAddresses) {
1142
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1143
+ if (cgId && STABLECOIN_IDS.has(cgId)) {
1144
+ prices[addr.toLowerCase()] = 1;
1145
+ }
1146
+ }
1147
+ const missingAddrs = tokenAddresses.filter(
1148
+ (addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
1149
+ );
1150
+ if (missingAddrs.length > 0) {
1151
+ try {
1152
+ const coins = missingAddrs.map((a) => `base:${a}`).join(",");
1153
+ const llamaResponse = await fetch(
1154
+ `https://coins.llama.fi/prices/current/${coins}`,
1155
+ { signal: AbortSignal.timeout(5e3) }
1156
+ );
1157
+ if (llamaResponse.ok) {
1158
+ const llamaData = await llamaResponse.json();
1159
+ for (const [key, data] of Object.entries(llamaData.coins)) {
1160
+ const addr = key.replace("base:", "").toLowerCase();
1161
+ if (data.price && data.confidence > 0.5) {
1162
+ prices[addr] = data.price;
1163
+ }
1164
+ }
1165
+ if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
1166
+ }
1167
+ } catch {
1168
+ }
1169
+ }
1170
+ if (Object.keys(prices).length > 0) {
1171
+ this.cachedPrices = prices;
1172
+ }
1173
+ if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
1174
+ console.warn("Using cached prices (last successful fetch was stale)");
1175
+ return { ...this.cachedPrices };
1176
+ }
1177
+ for (const addr of tokenAddresses) {
1178
+ if (!prices[addr.toLowerCase()]) {
1179
+ console.warn(`No price available for ${addr}, using 0`);
1180
+ prices[addr.toLowerCase()] = 0;
1181
+ }
1076
1182
  }
1077
1183
  return prices;
1078
1184
  }
@@ -1118,7 +1224,7 @@ var MarketDataService = class {
1118
1224
  let total = 0;
1119
1225
  for (const [address, balance] of Object.entries(balances)) {
1120
1226
  const price = prices[address.toLowerCase()] || 0;
1121
- const decimals = USDC_DECIMALS[address.toLowerCase()] || 18;
1227
+ const decimals = getTokenDecimals(address);
1122
1228
  const amount = Number(balance) / Math.pow(10, decimals);
1123
1229
  total += amount * price;
1124
1230
  }
@@ -1126,22 +1232,138 @@ var MarketDataService = class {
1126
1232
  }
1127
1233
  };
1128
1234
 
1235
+ // src/trading/risk.ts
1236
+ var RiskManager = class {
1237
+ config;
1238
+ dailyPnL = 0;
1239
+ dailyFees = 0;
1240
+ lastResetDate = "";
1241
+ /** Minimum trade value in USD — trades below this are rejected as dust */
1242
+ minTradeValueUSD;
1243
+ constructor(config) {
1244
+ this.config = config;
1245
+ this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
1246
+ }
1247
+ /**
1248
+ * Filter signals through risk checks
1249
+ * Returns only signals that pass all guardrails
1250
+ */
1251
+ filterSignals(signals, marketData) {
1252
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1253
+ if (today !== this.lastResetDate) {
1254
+ this.dailyPnL = 0;
1255
+ this.dailyFees = 0;
1256
+ this.lastResetDate = today;
1257
+ }
1258
+ if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
1259
+ console.warn("Daily loss limit reached - no new trades");
1260
+ return [];
1261
+ }
1262
+ return signals.filter((signal) => this.validateSignal(signal, marketData));
1263
+ }
1264
+ /**
1265
+ * Validate individual signal against risk limits
1266
+ */
1267
+ validateSignal(signal, marketData) {
1268
+ if (signal.action === "hold") {
1269
+ return true;
1270
+ }
1271
+ const signalValue = this.estimateSignalValue(signal, marketData);
1272
+ const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
1273
+ if (signalValue > maxPositionValue) {
1274
+ console.warn(
1275
+ `Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
1276
+ );
1277
+ return false;
1278
+ }
1279
+ if (signal.confidence < 0.5) {
1280
+ console.warn(`Signal confidence too low: ${signal.confidence}`);
1281
+ return false;
1282
+ }
1283
+ if (signal.action === "buy" && this.config.maxConcurrentPositions) {
1284
+ const activePositions = this.countActivePositions(marketData);
1285
+ if (activePositions >= this.config.maxConcurrentPositions) {
1286
+ console.warn(
1287
+ `Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
1288
+ );
1289
+ return false;
1290
+ }
1291
+ }
1292
+ if (signalValue < this.minTradeValueUSD) {
1293
+ console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
1294
+ return false;
1295
+ }
1296
+ return true;
1297
+ }
1298
+ /**
1299
+ * Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
1300
+ */
1301
+ countActivePositions(marketData) {
1302
+ const NATIVE_ETH_KEY = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
1303
+ let count = 0;
1304
+ for (const [address, balance] of Object.entries(marketData.balances)) {
1305
+ if (address.toLowerCase() === NATIVE_ETH_KEY) continue;
1306
+ if (balance > 0n) count++;
1307
+ }
1308
+ return count;
1309
+ }
1310
+ /**
1311
+ * Check if daily loss limit has been hit
1312
+ */
1313
+ isDailyLossLimitHit(portfolioValue) {
1314
+ const maxLoss = portfolioValue * this.config.maxDailyLossBps / 1e4;
1315
+ return this.dailyPnL < -maxLoss;
1316
+ }
1317
+ /**
1318
+ * Estimate USD value of a trade signal
1319
+ */
1320
+ estimateSignalValue(signal, marketData) {
1321
+ const price = marketData.prices[signal.tokenIn.toLowerCase()] || 0;
1322
+ const tokenDecimals = getTokenDecimals(signal.tokenIn);
1323
+ const amount = Number(signal.amountIn) / Math.pow(10, tokenDecimals);
1324
+ return amount * price;
1325
+ }
1326
+ /**
1327
+ * Update daily PnL after a trade (market gains/losses only)
1328
+ */
1329
+ updatePnL(pnl) {
1330
+ this.dailyPnL += pnl;
1331
+ }
1332
+ /**
1333
+ * Update daily fees (trading fees, gas costs, etc.)
1334
+ * Fees are tracked separately and do NOT count toward the daily loss limit.
1335
+ * This prevents protocol fees from triggering circuit breakers.
1336
+ */
1337
+ updateFees(fees) {
1338
+ this.dailyFees += fees;
1339
+ }
1340
+ /**
1341
+ * Get current risk status
1342
+ * @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
1343
+ */
1344
+ getStatus(portfolioValue) {
1345
+ const pv = portfolioValue || 0;
1346
+ const maxLossUSD = pv * this.config.maxDailyLossBps / 1e4;
1347
+ return {
1348
+ dailyPnL: this.dailyPnL,
1349
+ dailyFees: this.dailyFees,
1350
+ dailyNetPnL: this.dailyPnL - this.dailyFees,
1351
+ dailyLossLimit: maxLossUSD,
1352
+ // Only market PnL triggers the limit — fees are excluded
1353
+ isLimitHit: pv > 0 ? this.dailyPnL < -maxLossUSD : false
1354
+ };
1355
+ }
1356
+ };
1357
+
1129
1358
  // src/vault/manager.ts
1130
1359
  var import_viem2 = require("viem");
1131
1360
  var import_accounts = require("viem/accounts");
1132
1361
  var import_chains = require("viem/chains");
1133
1362
  var ADDRESSES = {
1134
- testnet: {
1135
- vaultFactory: "0x5c099daaE33801a907Bb57011c6749655b55dc75",
1136
- registry: "0xCF48C341e3FebeCA5ECB7eb2535f61A2Ba855d9C",
1137
- usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
1138
- },
1139
1363
  mainnet: {
1140
- vaultFactory: "0x0000000000000000000000000000000000000000",
1141
- // TODO: Deploy
1142
- registry: "0x0000000000000000000000000000000000000000",
1364
+ vaultFactory: process.env.EXAGENT_VAULT_FACTORY_ADDRESS || "0x0000000000000000000000000000000000000000",
1365
+ registry: process.env.EXAGENT_REGISTRY_ADDRESS || "0x0000000000000000000000000000000000000000",
1143
1366
  usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
1144
- // Base mainnet USDC
1145
1367
  }
1146
1368
  };
1147
1369
  var VAULT_FACTORY_ABI = [
@@ -1165,6 +1387,7 @@ var VAULT_FACTORY_ABI = [
1165
1387
  inputs: [
1166
1388
  { name: "agentId", type: "uint256" },
1167
1389
  { name: "asset", type: "address" },
1390
+ { name: "seedAmount", type: "uint256" },
1168
1391
  { name: "name", type: "string" },
1169
1392
  { name: "symbol", type: "string" },
1170
1393
  { name: "feeRecipient", type: "address" }
@@ -1178,13 +1401,6 @@ var VAULT_FACTORY_ABI = [
1178
1401
  inputs: [],
1179
1402
  outputs: [{ type: "uint256" }],
1180
1403
  stateMutability: "view"
1181
- },
1182
- {
1183
- type: "function",
1184
- name: "eXABurnFee",
1185
- inputs: [],
1186
- outputs: [{ type: "uint256" }],
1187
- stateMutability: "view"
1188
1404
  }
1189
1405
  ];
1190
1406
  var VAULT_ABI = [
@@ -1224,12 +1440,13 @@ var VaultManager = class {
1224
1440
  lastVaultCheck = 0;
1225
1441
  VAULT_CACHE_TTL = 6e4;
1226
1442
  // 1 minute
1443
+ enabled = true;
1227
1444
  constructor(config) {
1228
1445
  this.config = config;
1229
1446
  this.addresses = ADDRESSES[config.network];
1230
1447
  this.account = (0, import_accounts.privateKeyToAccount)(config.walletKey);
1231
- this.chain = config.network === "mainnet" ? import_chains.base : import_chains.baseSepolia;
1232
- const rpcUrl = config.network === "mainnet" ? "https://mainnet.base.org" : "https://sepolia.base.org";
1448
+ this.chain = import_chains.base;
1449
+ const rpcUrl = "https://mainnet.base.org";
1233
1450
  this.publicClient = (0, import_viem2.createPublicClient)({
1234
1451
  chain: this.chain,
1235
1452
  transport: (0, import_viem2.http)(rpcUrl)
@@ -1239,6 +1456,10 @@ var VaultManager = class {
1239
1456
  chain: this.chain,
1240
1457
  transport: (0, import_viem2.http)(rpcUrl)
1241
1458
  });
1459
+ if (this.addresses.vaultFactory === "0x0000000000000000000000000000000000000000") {
1460
+ console.warn("VaultFactory address is zero \u2014 vault operations will be disabled");
1461
+ this.enabled = false;
1462
+ }
1242
1463
  }
1243
1464
  /**
1244
1465
  * Get the agent's vault policy
@@ -1256,6 +1477,17 @@ var VaultManager = class {
1256
1477
  * Get comprehensive vault status
1257
1478
  */
1258
1479
  async getVaultStatus() {
1480
+ if (!this.enabled) {
1481
+ return {
1482
+ hasVault: false,
1483
+ vaultAddress: null,
1484
+ totalAssets: BigInt(0),
1485
+ canCreateVault: false,
1486
+ cannotCreateReason: "Vault operations disabled (contract address not set)",
1487
+ requirementsMet: false,
1488
+ requirements: { veXARequired: BigInt(0), isBypassed: false }
1489
+ };
1490
+ }
1259
1491
  const vaultAddress = await this.getVaultAddress();
1260
1492
  const hasVault = vaultAddress !== null;
1261
1493
  let totalAssets = BigInt(0);
@@ -1290,22 +1522,16 @@ var VaultManager = class {
1290
1522
  }
1291
1523
  /**
1292
1524
  * Get vault creation requirements
1525
+ * Note: No burnFee on mainnet — vault creation requires USDC seed instead
1293
1526
  */
1294
1527
  async getRequirements() {
1295
- const [veXARequired, burnFee] = await Promise.all([
1296
- this.publicClient.readContract({
1297
- address: this.addresses.vaultFactory,
1298
- abi: VAULT_FACTORY_ABI,
1299
- functionName: "minimumVeEXARequired"
1300
- }),
1301
- this.publicClient.readContract({
1302
- address: this.addresses.vaultFactory,
1303
- abi: VAULT_FACTORY_ABI,
1304
- functionName: "eXABurnFee"
1305
- })
1306
- ]);
1307
- const isBypassed = veXARequired === BigInt(0) && burnFee === BigInt(0);
1308
- return { veXARequired, burnFee, isBypassed };
1528
+ const veXARequired = await this.publicClient.readContract({
1529
+ address: this.addresses.vaultFactory,
1530
+ abi: VAULT_FACTORY_ABI,
1531
+ functionName: "minimumVeEXARequired"
1532
+ });
1533
+ const isBypassed = veXARequired === BigInt(0);
1534
+ return { veXARequired, isBypassed };
1309
1535
  }
1310
1536
  /**
1311
1537
  * Get the agent's vault address (cached)
@@ -1329,30 +1555,15 @@ var VaultManager = class {
1329
1555
  this.cachedVaultAddress = vaultAddress;
1330
1556
  return vaultAddress;
1331
1557
  }
1332
- /**
1333
- * Check if the agent should create a vault based on policy and qualification
1334
- */
1335
- async shouldCreateVault() {
1336
- if (this.policy === "disabled") {
1337
- return { should: false, reason: "Vault creation disabled by policy" };
1338
- }
1339
- if (this.policy === "manual") {
1340
- return { should: false, reason: "Vault creation set to manual - waiting for owner instruction" };
1341
- }
1342
- const status = await this.getVaultStatus();
1343
- if (status.hasVault) {
1344
- return { should: false, reason: "Vault already exists" };
1345
- }
1346
- if (!status.canCreateVault) {
1347
- return { should: false, reason: status.cannotCreateReason || "Requirements not met" };
1348
- }
1349
- return { should: true, reason: "Agent is qualified and auto-creation is enabled" };
1350
- }
1351
1558
  /**
1352
1559
  * Create a vault for the agent
1560
+ * @param seedAmount - USDC seed amount in raw units (default: 100e6 = 100 USDC)
1353
1561
  * @returns Vault address if successful
1354
1562
  */
1355
- async createVault() {
1563
+ async createVault(seedAmount) {
1564
+ if (!this.enabled) {
1565
+ return { success: false, error: "Vault operations disabled (contract address not set)" };
1566
+ }
1356
1567
  if (this.policy === "disabled") {
1357
1568
  return { success: false, error: "Vault creation disabled by policy" };
1358
1569
  }
@@ -1364,6 +1575,7 @@ var VaultManager = class {
1364
1575
  if (!status.canCreateVault) {
1365
1576
  return { success: false, error: status.cannotCreateReason || "Requirements not met" };
1366
1577
  }
1578
+ const seed = seedAmount || BigInt(1e8);
1367
1579
  const vaultName = this.config.vaultConfig.defaultName || `${this.config.agentName} Trading Vault`;
1368
1580
  const vaultSymbol = this.config.vaultConfig.defaultSymbol || `ex${this.config.agentName.replace(/[^a-zA-Z]/g, "").slice(0, 4).toUpperCase()}`;
1369
1581
  const feeRecipient = this.config.vaultConfig.feeRecipient || this.account.address;
@@ -1375,6 +1587,7 @@ var VaultManager = class {
1375
1587
  args: [
1376
1588
  this.config.agentId,
1377
1589
  this.addresses.usdc,
1590
+ seed,
1378
1591
  vaultName,
1379
1592
  vaultSymbol,
1380
1593
  feeRecipient
@@ -1438,37 +1651,12 @@ var VaultManager = class {
1438
1651
  };
1439
1652
  }
1440
1653
  }
1441
- /**
1442
- * Run the auto-creation check (call this periodically in the agent loop)
1443
- * Only creates vault if policy is 'auto_when_qualified'
1444
- */
1445
- async checkAndAutoCreateVault() {
1446
- const shouldCreate = await this.shouldCreateVault();
1447
- if (!shouldCreate.should) {
1448
- const status = await this.getVaultStatus();
1449
- if (status.hasVault) {
1450
- return { action: "already_exists", vaultAddress: status.vaultAddress, reason: "Vault already exists" };
1451
- }
1452
- if (this.policy !== "auto_when_qualified") {
1453
- return { action: "skipped", reason: shouldCreate.reason };
1454
- }
1455
- return { action: "not_qualified", reason: shouldCreate.reason };
1456
- }
1457
- const result = await this.createVault();
1458
- if (result.success) {
1459
- return {
1460
- action: "created",
1461
- vaultAddress: result.vaultAddress,
1462
- reason: "Vault created automatically"
1463
- };
1464
- }
1465
- return { action: "not_qualified", reason: result.error || "Creation failed" };
1466
- }
1467
1654
  };
1468
1655
 
1469
1656
  // src/relay.ts
1470
1657
  var import_ws = __toESM(require("ws"));
1471
1658
  var import_accounts2 = require("viem/accounts");
1659
+ var import_sdk = require("@exagent/sdk");
1472
1660
  var RelayClient = class {
1473
1661
  config;
1474
1662
  ws = null;
@@ -1571,7 +1759,8 @@ var RelayClient = class {
1571
1759
  agentId: this.config.agentId,
1572
1760
  wallet: account.address,
1573
1761
  timestamp,
1574
- signature
1762
+ signature,
1763
+ sdkVersion: import_sdk.SDK_VERSION
1575
1764
  });
1576
1765
  }
1577
1766
  /**
@@ -1733,6 +1922,7 @@ function openBrowser(url) {
1733
1922
 
1734
1923
  // src/runtime.ts
1735
1924
  var FUNDS_LOW_THRESHOLD = 5e-3;
1925
+ var FUNDS_CRITICAL_THRESHOLD = 1e-3;
1736
1926
  var AgentRuntime = class {
1737
1927
  config;
1738
1928
  client;
@@ -1746,14 +1936,13 @@ var AgentRuntime = class {
1746
1936
  isRunning = false;
1747
1937
  mode = "idle";
1748
1938
  configHash;
1749
- lastVaultCheck = 0;
1750
1939
  cycleCount = 0;
1751
1940
  lastCycleAt = 0;
1752
1941
  lastPortfolioValue = 0;
1753
1942
  lastEthBalance = "0";
1754
1943
  processAlive = true;
1755
- VAULT_CHECK_INTERVAL = 3e5;
1756
- // Check vault status every 5 minutes
1944
+ riskUniverse = 0;
1945
+ allowedTokens = /* @__PURE__ */ new Set();
1757
1946
  constructor(config) {
1758
1947
  this.config = config;
1759
1948
  }
@@ -1762,7 +1951,7 @@ var AgentRuntime = class {
1762
1951
  */
1763
1952
  async initialize() {
1764
1953
  console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
1765
- this.client = new import_sdk.ExagentClient({
1954
+ this.client = new import_sdk2.ExagentClient({
1766
1955
  privateKey: this.config.privateKey,
1767
1956
  network: this.config.network
1768
1957
  });
@@ -1773,6 +1962,7 @@ var AgentRuntime = class {
1773
1962
  }
1774
1963
  console.log(`Agent verified: ${agent.name}`);
1775
1964
  await this.ensureWalletLinked();
1965
+ await this.loadTradingRestrictions();
1776
1966
  console.log(`Initializing LLM: ${this.config.llm.provider}`);
1777
1967
  this.llm = await createLLMAdapter(this.config.llm);
1778
1968
  const llmMeta = this.llm.getMetadata();
@@ -1840,12 +2030,8 @@ var AgentRuntime = class {
1840
2030
  console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
1841
2031
  } else {
1842
2032
  console.log("No vault exists for this agent");
1843
- if (vaultConfig.policy === "auto_when_qualified") {
1844
- if (status.canCreateVault) {
1845
- console.log("Agent is qualified to create vault - will attempt on next check");
1846
- } else {
1847
- console.log(`Cannot create vault yet: ${status.cannotCreateReason}`);
1848
- }
2033
+ if (vaultConfig.policy === "manual") {
2034
+ console.log("Vault creation is manual \u2014 use the command center to create one");
1849
2035
  }
1850
2036
  }
1851
2037
  }
@@ -1864,7 +2050,7 @@ var AgentRuntime = class {
1864
2050
  if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
1865
2051
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
1866
2052
  const nonce = await this.client.registry.getNonce(address);
1867
- const linkMessage = import_sdk.ExagentRegistry.generateLinkMessage(
2053
+ const linkMessage = import_sdk2.ExagentRegistry.generateLinkMessage(
1868
2054
  address,
1869
2055
  agentId,
1870
2056
  nonce
@@ -1904,6 +2090,49 @@ var AgentRuntime = class {
1904
2090
  console.log("Wallet already linked");
1905
2091
  }
1906
2092
  }
2093
+ /**
2094
+ * Load risk universe and allowed tokens from on-chain registry.
2095
+ * This prevents the agent from wasting gas on trades that will revert.
2096
+ */
2097
+ async loadTradingRestrictions() {
2098
+ const agentId = BigInt(this.config.agentId);
2099
+ const RISK_UNIVERSE_NAMES = ["Core", "Established", "Derivatives", "Emerging", "Frontier"];
2100
+ try {
2101
+ this.riskUniverse = await this.client.registry.getRiskUniverse(agentId);
2102
+ console.log(`Risk universe: ${RISK_UNIVERSE_NAMES[this.riskUniverse] || this.riskUniverse}`);
2103
+ const configTokens = this.config.allowedTokens || this.getDefaultTokens();
2104
+ const verified = [];
2105
+ for (const token of configTokens) {
2106
+ try {
2107
+ const allowed = await this.client.registry.isTradeAllowed(
2108
+ agentId,
2109
+ token,
2110
+ "0x0000000000000000000000000000000000000000"
2111
+ // zero = check token only
2112
+ );
2113
+ if (allowed) {
2114
+ this.allowedTokens.add(token.toLowerCase());
2115
+ verified.push(token);
2116
+ } else {
2117
+ console.warn(`Token ${token} not allowed for this agent's risk universe \u2014 excluded`);
2118
+ }
2119
+ } catch {
2120
+ this.allowedTokens.add(token.toLowerCase());
2121
+ verified.push(token);
2122
+ }
2123
+ }
2124
+ this.config.allowedTokens = verified;
2125
+ console.log(`Allowed tokens loaded: ${verified.length} tokens verified`);
2126
+ if (this.riskUniverse === 4) {
2127
+ console.warn("Frontier risk universe: vault creation is disabled");
2128
+ }
2129
+ } catch (error) {
2130
+ console.warn(
2131
+ "Could not load trading restrictions from registry (using defaults):",
2132
+ error instanceof Error ? error.message : error
2133
+ );
2134
+ }
2135
+ }
1907
2136
  /**
1908
2137
  * Sync the LLM config hash to chain for epoch tracking.
1909
2138
  * If the wallet has insufficient gas, enters a recovery loop
@@ -1912,7 +2141,7 @@ var AgentRuntime = class {
1912
2141
  async syncConfigHash() {
1913
2142
  const agentId = BigInt(this.config.agentId);
1914
2143
  const llmMeta = this.llm.getMetadata();
1915
- this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
2144
+ this.configHash = import_sdk2.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
1916
2145
  console.log(`Config hash: ${this.configHash}`);
1917
2146
  const onChainHash = await this.client.registry.getConfigHash(agentId);
1918
2147
  if (onChainHash !== this.configHash) {
@@ -1925,7 +2154,7 @@ var AgentRuntime = class {
1925
2154
  const message = error instanceof Error ? error.message : String(error);
1926
2155
  if (message.includes("insufficient funds") || message.includes("gas") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
1927
2156
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
1928
- const chain = this.config.network === "mainnet" ? import_chains2.base : import_chains2.baseSepolia;
2157
+ const chain = import_chains2.base;
1929
2158
  const publicClientInstance = (0, import_viem3.createPublicClient)({
1930
2159
  chain,
1931
2160
  transport: (0, import_viem3.http)(this.getRpcUrl())
@@ -2076,21 +2305,38 @@ var AgentRuntime = class {
2076
2305
  break;
2077
2306
  case "update_risk_params": {
2078
2307
  const params = cmd.params || {};
2308
+ let updated = false;
2079
2309
  if (params.maxPositionSizeBps !== void 0) {
2080
- this.config.trading.maxPositionSizeBps = Number(params.maxPositionSizeBps);
2310
+ const value = Number(params.maxPositionSizeBps);
2311
+ if (isNaN(value) || value < 100 || value > 1e4) {
2312
+ this.relay?.sendCommandResult(cmd.id, false, "maxPositionSizeBps must be 100-10000");
2313
+ break;
2314
+ }
2315
+ this.config.trading.maxPositionSizeBps = value;
2316
+ updated = true;
2081
2317
  }
2082
2318
  if (params.maxDailyLossBps !== void 0) {
2083
- this.config.trading.maxDailyLossBps = Number(params.maxDailyLossBps);
2319
+ const value = Number(params.maxDailyLossBps);
2320
+ if (isNaN(value) || value < 50 || value > 5e3) {
2321
+ this.relay?.sendCommandResult(cmd.id, false, "maxDailyLossBps must be 50-5000");
2322
+ break;
2323
+ }
2324
+ this.config.trading.maxDailyLossBps = value;
2325
+ updated = true;
2326
+ }
2327
+ if (updated) {
2328
+ this.riskManager = new RiskManager(this.config.trading);
2329
+ console.log("Risk params updated via command center");
2330
+ this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
2331
+ this.relay?.sendMessage(
2332
+ "config_updated",
2333
+ "info",
2334
+ "Risk Parameters Updated",
2335
+ `Max position: ${this.config.trading.maxPositionSizeBps / 100}%, Max daily loss: ${this.config.trading.maxDailyLossBps / 100}%`
2336
+ );
2337
+ } else {
2338
+ this.relay?.sendCommandResult(cmd.id, false, "No valid parameters provided");
2084
2339
  }
2085
- this.riskManager = new RiskManager(this.config.trading);
2086
- console.log("Risk params updated via command center");
2087
- this.relay?.sendCommandResult(cmd.id, true, "Risk params updated");
2088
- this.relay?.sendMessage(
2089
- "config_updated",
2090
- "info",
2091
- "Risk Parameters Updated",
2092
- `Max position: ${this.config.trading.maxPositionSizeBps / 100}%, Max daily loss: ${this.config.trading.maxDailyLossBps / 100}%`
2093
- );
2094
2340
  break;
2095
2341
  }
2096
2342
  case "update_trading_interval": {
@@ -2167,7 +2413,7 @@ var AgentRuntime = class {
2167
2413
  provider: this.config.llm.provider,
2168
2414
  model: this.config.llm.model || "default"
2169
2415
  },
2170
- risk: this.riskManager?.getStatus() || {
2416
+ risk: this.riskManager?.getStatus(this.lastPortfolioValue) || {
2171
2417
  dailyPnL: 0,
2172
2418
  dailyLossLimit: 0,
2173
2419
  isLimitHit: false
@@ -2188,14 +2434,18 @@ var AgentRuntime = class {
2188
2434
  --- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
2189
2435
  this.cycleCount++;
2190
2436
  this.lastCycleAt = Date.now();
2191
- await this.checkVaultAutoCreation();
2192
2437
  const tokens = this.config.allowedTokens || this.getDefaultTokens();
2193
2438
  const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
2194
2439
  console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
2195
2440
  this.lastPortfolioValue = marketData.portfolioValue;
2196
2441
  const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
2197
2442
  this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
2198
- this.checkFundsLow(marketData);
2443
+ const fundsOk = this.checkFundsLow(marketData);
2444
+ if (!fundsOk) {
2445
+ console.warn("Skipping trading cycle \u2014 ETH balance critically low");
2446
+ this.sendRelayStatus();
2447
+ return;
2448
+ }
2199
2449
  let signals;
2200
2450
  try {
2201
2451
  signals = await this.strategy(marketData, this.llm, this.config);
@@ -2213,19 +2463,31 @@ var AgentRuntime = class {
2213
2463
  console.log(`Strategy generated ${signals.length} signals`);
2214
2464
  const filteredSignals = this.riskManager.filterSignals(signals, marketData);
2215
2465
  console.log(`${filteredSignals.length} signals passed risk checks`);
2216
- if (this.riskManager.getStatus().isLimitHit) {
2466
+ if (this.riskManager.getStatus(marketData.portfolioValue).isLimitHit) {
2217
2467
  this.relay?.sendMessage(
2218
2468
  "risk_limit_hit",
2219
2469
  "warning",
2220
2470
  "Risk Limit Hit",
2221
- `Daily loss limit reached: ${this.riskManager.getStatus().dailyPnL.toFixed(2)}`
2471
+ `Daily loss limit reached: $${this.riskManager.getStatus(marketData.portfolioValue).dailyPnL.toFixed(2)}`
2222
2472
  );
2223
2473
  }
2224
2474
  if (filteredSignals.length > 0) {
2475
+ const vaultStatus = await this.vaultManager?.getVaultStatus();
2476
+ if (vaultStatus?.hasVault && this.vaultManager?.preferVaultTrading) {
2477
+ console.log(`Trading through vault: ${vaultStatus.vaultAddress}`);
2478
+ }
2479
+ const preTradePortfolioValue = marketData.portfolioValue;
2225
2480
  const results = await this.executor.executeAll(filteredSignals);
2481
+ let totalFeesUSD = 0;
2226
2482
  for (const result of results) {
2227
2483
  if (result.success) {
2228
2484
  console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
2485
+ const feeCostBps = 20;
2486
+ const signalPrice = marketData.prices[result.signal.tokenIn.toLowerCase()] || 0;
2487
+ const amountUSD = Number(result.signal.amountIn) / Math.pow(10, getTokenDecimals(result.signal.tokenIn)) * signalPrice;
2488
+ const feeCostUSD = amountUSD * feeCostBps / 1e4;
2489
+ totalFeesUSD += feeCostUSD;
2490
+ this.riskManager.updateFees(feeCostUSD);
2229
2491
  this.relay?.sendMessage(
2230
2492
  "trade_executed",
2231
2493
  "success",
@@ -2249,18 +2511,43 @@ var AgentRuntime = class {
2249
2511
  );
2250
2512
  }
2251
2513
  }
2514
+ const postTokens = this.config.allowedTokens || this.getDefaultTokens();
2515
+ const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
2516
+ const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
2517
+ this.riskManager.updatePnL(marketPnL);
2518
+ if (marketPnL !== 0) {
2519
+ console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
2520
+ }
2521
+ this.lastPortfolioValue = postTradeData.portfolioValue;
2522
+ const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
2523
+ this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
2252
2524
  }
2253
2525
  this.sendRelayStatus();
2254
2526
  }
2255
2527
  /**
2256
- * Check if ETH balance is below threshold and notify
2528
+ * Check if ETH balance is below threshold and notify.
2529
+ * Returns true if trading should continue, false if ETH is critically low.
2257
2530
  */
2258
2531
  checkFundsLow(marketData) {
2259
- if (!this.relay) return;
2260
2532
  const ethBalance = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
2261
2533
  const ethAmount = Number(ethBalance) / 1e18;
2534
+ if (ethAmount < FUNDS_CRITICAL_THRESHOLD) {
2535
+ console.error(`ETH balance critically low: ${ethAmount.toFixed(6)} ETH \u2014 halting trading`);
2536
+ this.relay?.sendMessage(
2537
+ "funds_low",
2538
+ "error",
2539
+ "Funds Critical",
2540
+ `ETH balance is ${ethAmount.toFixed(6)} ETH (below ${FUNDS_CRITICAL_THRESHOLD} ETH minimum). Trading halted \u2014 fund your wallet.`,
2541
+ {
2542
+ ethBalance: ethAmount.toFixed(6),
2543
+ wallet: this.client.address,
2544
+ threshold: FUNDS_CRITICAL_THRESHOLD
2545
+ }
2546
+ );
2547
+ return false;
2548
+ }
2262
2549
  if (ethAmount < FUNDS_LOW_THRESHOLD) {
2263
- this.relay.sendMessage(
2550
+ this.relay?.sendMessage(
2264
2551
  "funds_low",
2265
2552
  "warning",
2266
2553
  "Low Funds",
@@ -2272,36 +2559,7 @@ var AgentRuntime = class {
2272
2559
  }
2273
2560
  );
2274
2561
  }
2275
- }
2276
- /**
2277
- * Check for vault auto-creation based on policy
2278
- */
2279
- async checkVaultAutoCreation() {
2280
- const now = Date.now();
2281
- if (now - this.lastVaultCheck < this.VAULT_CHECK_INTERVAL) {
2282
- return;
2283
- }
2284
- this.lastVaultCheck = now;
2285
- const result = await this.vaultManager.checkAndAutoCreateVault();
2286
- switch (result.action) {
2287
- case "created":
2288
- console.log(`Vault created automatically: ${result.vaultAddress}`);
2289
- this.relay?.sendMessage(
2290
- "vault_created",
2291
- "success",
2292
- "Vault Auto-Created",
2293
- `Vault deployed at ${result.vaultAddress}`,
2294
- { vaultAddress: result.vaultAddress }
2295
- );
2296
- break;
2297
- case "already_exists":
2298
- break;
2299
- case "skipped":
2300
- break;
2301
- case "not_qualified":
2302
- console.log(`Vault auto-creation pending: ${result.reason}`);
2303
- break;
2304
- }
2562
+ return true;
2305
2563
  }
2306
2564
  /**
2307
2565
  * Stop the agent process completely
@@ -2319,26 +2577,44 @@ var AgentRuntime = class {
2319
2577
  * Get RPC URL based on network
2320
2578
  */
2321
2579
  getRpcUrl() {
2322
- if (this.config.network === "mainnet") {
2323
- return "https://mainnet.base.org";
2324
- }
2325
- return "https://sepolia.base.org";
2580
+ return "https://mainnet.base.org";
2326
2581
  }
2327
2582
  /**
2328
- * Default tokens to track
2583
+ * Default tokens to track.
2584
+ * These are validated against the on-chain registry's isTradeAllowed() during init —
2585
+ * agents in restricted risk universes will have ineligible tokens filtered out.
2329
2586
  */
2330
2587
  getDefaultTokens() {
2331
- if (this.config.network === "mainnet") {
2332
- return [
2333
- "0x4200000000000000000000000000000000000006",
2334
- // WETH
2335
- "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
2336
- // USDC
2337
- ];
2338
- }
2339
2588
  return [
2340
- "0x4200000000000000000000000000000000000006"
2589
+ // Core
2590
+ "0x4200000000000000000000000000000000000006",
2341
2591
  // WETH
2592
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
2593
+ // USDC
2594
+ "0x2Ae3F1Ec7F1F5012CFEab0185bFC7aa3cf0DEC22",
2595
+ // cbETH
2596
+ "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA",
2597
+ // USDbC
2598
+ "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
2599
+ // DAI
2600
+ // Established
2601
+ "0x940181a94A35A4569E4529A3CDfB74e38FD98631",
2602
+ // AERO
2603
+ "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
2604
+ // cbBTC
2605
+ "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
2606
+ // wstETH
2607
+ "0x2416092f143378750bb29b79eD961ab195CcEea5",
2608
+ // ezETH
2609
+ // Emerging (filtered by risk universe at init)
2610
+ "0x532f27101965dd16442E59d40670FaF5eBB142E4",
2611
+ // BRETT
2612
+ "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
2613
+ // DEGEN
2614
+ "0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b",
2615
+ // VIRTUAL
2616
+ "0xAC1Bd2486Aaf3B5C0fc3Fd868558b082a531B2B4"
2617
+ // TOSHI
2342
2618
  ];
2343
2619
  }
2344
2620
  sleep(ms) {
@@ -2359,7 +2635,7 @@ var AgentRuntime = class {
2359
2635
  model: this.config.llm.model || "default"
2360
2636
  },
2361
2637
  configHash: this.configHash || "not initialized",
2362
- risk: this.riskManager?.getStatus() || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false },
2638
+ risk: this.riskManager?.getStatus(this.lastPortfolioValue) || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false },
2363
2639
  vault: {
2364
2640
  policy: vaultConfig.policy,
2365
2641
  hasVault: false,
@@ -2421,16 +2697,18 @@ var TradingConfigSchema = import_zod.z.object({
2421
2697
  maxDailyLossBps: import_zod.z.number().min(0).max(1e4).default(500),
2422
2698
  // 0-100%
2423
2699
  maxConcurrentPositions: import_zod.z.number().min(1).max(100).default(5),
2424
- tradingIntervalMs: import_zod.z.number().min(1e3).default(6e4)
2700
+ tradingIntervalMs: import_zod.z.number().min(1e3).default(6e4),
2425
2701
  // minimum 1 second
2702
+ maxSlippageBps: import_zod.z.number().min(10).max(1e3).default(100),
2703
+ // 0.1-10%, default 1%
2704
+ minTradeValueUSD: import_zod.z.number().min(0).default(1)
2705
+ // minimum trade value in USD
2426
2706
  });
2427
2707
  var VaultPolicySchema = import_zod.z.enum([
2428
2708
  "disabled",
2429
2709
  // Never create a vault - trade with agent's own capital only
2430
- "manual",
2710
+ "manual"
2431
2711
  // Only create vault when explicitly directed by owner
2432
- "auto_when_qualified"
2433
- // Automatically create vault when requirements are met
2434
2712
  ]);
2435
2713
  var VaultConfigSchema = import_zod.z.object({
2436
2714
  // Policy for vault creation (asked during deployment)
@@ -2458,7 +2736,7 @@ var AgentConfigSchema = import_zod.z.object({
2458
2736
  agentId: import_zod.z.union([import_zod.z.number().positive(), import_zod.z.string()]),
2459
2737
  name: import_zod.z.string().min(3).max(32),
2460
2738
  // Network
2461
- network: import_zod.z.enum(["mainnet", "testnet"]).default("testnet"),
2739
+ network: import_zod.z.literal("mainnet").default("mainnet"),
2462
2740
  // Wallet setup preference
2463
2741
  wallet: WalletConfigSchema,
2464
2742
  // LLM
@@ -2545,7 +2823,7 @@ function createSampleConfig(agentId, name) {
2545
2823
  return {
2546
2824
  agentId,
2547
2825
  name,
2548
- network: "testnet",
2826
+ network: "mainnet",
2549
2827
  llm: {
2550
2828
  provider: "openai",
2551
2829
  model: "gpt-4.1",
@@ -2558,7 +2836,9 @@ function createSampleConfig(agentId, name) {
2558
2836
  maxPositionSizeBps: 1e3,
2559
2837
  maxDailyLossBps: 500,
2560
2838
  maxConcurrentPositions: 5,
2561
- tradingIntervalMs: 6e4
2839
+ tradingIntervalMs: 6e4,
2840
+ maxSlippageBps: 100,
2841
+ minTradeValueUSD: 1
2562
2842
  },
2563
2843
  vault: {
2564
2844
  // Default to manual - user must explicitly enable auto-creation