@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/cli.js CHANGED
@@ -31,7 +31,7 @@ var fs2 = __toESM(require("fs"));
31
31
  var path2 = __toESM(require("path"));
32
32
 
33
33
  // src/runtime.ts
34
- var import_sdk = require("@exagent/sdk");
34
+ var import_sdk2 = require("@exagent/sdk");
35
35
  var import_viem3 = require("viem");
36
36
  var import_chains2 = require("viem/chains");
37
37
 
@@ -779,7 +779,7 @@ export const generateSignals: StrategyFunction = async (marketData, llm, config)
779
779
  riskWarnings: [
780
780
  "Custom strategies have no guardrails - you are fully responsible",
781
781
  "LLMs can hallucinate or make errors - always validate outputs",
782
- "Test thoroughly on testnet before using real funds",
782
+ "Start with small amounts before scaling up",
783
783
  "Consider edge cases: what happens if the LLM returns invalid JSON?",
784
784
  "Your prompts and strategy logic are your competitive advantage - protect them",
785
785
  "Agents may not behave exactly as expected based on your prompts"
@@ -838,9 +838,13 @@ function getAllStrategyTemplates() {
838
838
  var TradeExecutor = class {
839
839
  client;
840
840
  config;
841
+ allowedTokens;
841
842
  constructor(client, config) {
842
843
  this.client = client;
843
844
  this.config = config;
845
+ this.allowedTokens = new Set(
846
+ (config.allowedTokens || []).map((t) => t.toLowerCase())
847
+ );
844
848
  }
845
849
  /**
846
850
  * Execute a single trade signal
@@ -859,8 +863,7 @@ var TradeExecutor = class {
859
863
  tokenIn: signal.tokenIn,
860
864
  tokenOut: signal.tokenOut,
861
865
  amountIn: signal.amountIn,
862
- maxSlippageBps: 100
863
- // 1% default slippage
866
+ maxSlippageBps: this.config.trading?.maxSlippageBps ?? 100
864
867
  });
865
868
  console.log(`Trade executed: ${result.hash}`);
866
869
  return { success: true, txHash: result.hash };
@@ -886,13 +889,25 @@ var TradeExecutor = class {
886
889
  return results;
887
890
  }
888
891
  /**
889
- * Validate a signal against config limits
892
+ * Validate a signal against config limits and token restrictions
890
893
  */
891
894
  validateSignal(signal) {
892
895
  if (signal.confidence < 0.5) {
893
896
  console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
894
897
  return false;
895
898
  }
899
+ if (this.allowedTokens.size > 0) {
900
+ const tokenInAllowed = this.allowedTokens.has(signal.tokenIn.toLowerCase());
901
+ const tokenOutAllowed = this.allowedTokens.has(signal.tokenOut.toLowerCase());
902
+ if (!tokenInAllowed) {
903
+ console.warn(`Token ${signal.tokenIn} not in allowed list for this agent's risk universe \u2014 skipping`);
904
+ return false;
905
+ }
906
+ if (!tokenOutAllowed) {
907
+ console.warn(`Token ${signal.tokenOut} not in allowed list for this agent's risk universe \u2014 skipping`);
908
+ return false;
909
+ }
910
+ }
896
911
  return true;
897
912
  }
898
913
  delay(ms) {
@@ -900,103 +915,99 @@ var TradeExecutor = class {
900
915
  }
901
916
  };
902
917
 
903
- // src/trading/risk.ts
904
- var RiskManager = class {
905
- config;
906
- dailyPnL = 0;
907
- lastResetDate = "";
908
- constructor(config) {
909
- this.config = config;
910
- }
911
- /**
912
- * Filter signals through risk checks
913
- * Returns only signals that pass all guardrails
914
- */
915
- filterSignals(signals, marketData) {
916
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
917
- if (today !== this.lastResetDate) {
918
- this.dailyPnL = 0;
919
- this.lastResetDate = today;
920
- }
921
- if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
922
- console.warn("Daily loss limit reached - no new trades");
923
- return [];
924
- }
925
- return signals.filter((signal) => this.validateSignal(signal, marketData));
926
- }
927
- /**
928
- * Validate individual signal against risk limits
929
- */
930
- validateSignal(signal, marketData) {
931
- if (signal.action === "hold") {
932
- return true;
933
- }
934
- const signalValue = this.estimateSignalValue(signal, marketData);
935
- const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
936
- if (signalValue > maxPositionValue) {
937
- console.warn(
938
- `Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
939
- );
940
- return false;
941
- }
942
- if (signal.confidence < 0.5) {
943
- console.warn(`Signal confidence too low: ${signal.confidence}`);
944
- return false;
945
- }
946
- return true;
947
- }
948
- /**
949
- * Check if daily loss limit has been hit
950
- */
951
- isDailyLossLimitHit(portfolioValue) {
952
- const maxLoss = portfolioValue * this.config.maxDailyLossBps / 1e4;
953
- return this.dailyPnL < -maxLoss;
954
- }
955
- /**
956
- * Estimate USD value of a trade signal
957
- */
958
- estimateSignalValue(signal, marketData) {
959
- const price = marketData.prices[signal.tokenIn] || 0;
960
- const amount = Number(signal.amountIn) / 1e18;
961
- return amount * price;
962
- }
963
- /**
964
- * Update daily PnL after a trade
965
- */
966
- updatePnL(pnl) {
967
- this.dailyPnL += pnl;
968
- }
969
- /**
970
- * Get current risk status
971
- */
972
- getStatus() {
973
- return {
974
- dailyPnL: this.dailyPnL,
975
- dailyLossLimit: this.config.maxDailyLossBps / 100,
976
- // As percentage
977
- isLimitHit: this.dailyPnL < -(this.config.maxDailyLossBps / 100)
978
- };
979
- }
980
- };
981
-
982
918
  // src/trading/market.ts
983
919
  var import_viem = require("viem");
984
920
  var NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
985
- var USDC_DECIMALS = {
921
+ var TOKEN_DECIMALS = {
922
+ // Base Mainnet — Core tokens
986
923
  "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
987
- // USDC on Base
988
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6
989
- // USDbC on Base
924
+ // USDC
925
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
926
+ // USDbC
927
+ "0x4200000000000000000000000000000000000006": 18,
928
+ // WETH
929
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
930
+ // DAI
931
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": 18,
932
+ // cbETH
933
+ [NATIVE_ETH.toLowerCase()]: 18,
934
+ // Native ETH
935
+ // Base Mainnet — Established tokens
936
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": 18,
937
+ // AERO (Aerodrome)
938
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": 18,
939
+ // BRETT
940
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": 18,
941
+ // DEGEN
942
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": 18,
943
+ // VIRTUAL
944
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": 18,
945
+ // TOSHI
946
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
947
+ // cbBTC
948
+ "0x2416092f143378750bb29b79ed961ab195cceea5": 18,
949
+ // ezETH (Renzo)
950
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18
951
+ // wstETH (Lido)
990
952
  };
953
+ function getTokenDecimals(address) {
954
+ const decimals = TOKEN_DECIMALS[address.toLowerCase()];
955
+ if (decimals === void 0) {
956
+ console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
957
+ return 18;
958
+ }
959
+ return decimals;
960
+ }
961
+ var TOKEN_TO_COINGECKO = {
962
+ // Core
963
+ "0x4200000000000000000000000000000000000006": "ethereum",
964
+ // WETH
965
+ [NATIVE_ETH.toLowerCase()]: "ethereum",
966
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
967
+ // USDC
968
+ "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
969
+ // USDbC
970
+ "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
971
+ // cbETH
972
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
973
+ // DAI
974
+ // Established
975
+ "0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
976
+ // AERO
977
+ "0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
978
+ // BRETT
979
+ "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
980
+ // DEGEN
981
+ "0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
982
+ // VIRTUAL
983
+ "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
984
+ // TOSHI
985
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
986
+ // cbBTC
987
+ "0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
988
+ // ezETH
989
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
990
+ // wstETH
991
+ };
992
+ var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
993
+ var PRICE_STALENESS_MS = 6e4;
991
994
  var MarketDataService = class {
992
995
  rpcUrl;
993
996
  client;
997
+ /** Cached prices from last fetch */
998
+ cachedPrices = {};
999
+ /** Timestamp of last successful price fetch */
1000
+ lastPriceFetchAt = 0;
994
1001
  constructor(rpcUrl) {
995
1002
  this.rpcUrl = rpcUrl;
996
1003
  this.client = (0, import_viem.createPublicClient)({
997
1004
  transport: (0, import_viem.http)(rpcUrl)
998
1005
  });
999
1006
  }
1007
+ /** Cached volume data */
1008
+ cachedVolume24h = {};
1009
+ /** Cached price change data */
1010
+ cachedPriceChange24h = {};
1000
1011
  /**
1001
1012
  * Fetch current market data for the agent
1002
1013
  */
@@ -1004,31 +1015,126 @@ var MarketDataService = class {
1004
1015
  const prices = await this.fetchPrices(tokenAddresses);
1005
1016
  const balances = await this.fetchBalances(walletAddress, tokenAddresses);
1006
1017
  const portfolioValue = this.calculatePortfolioValue(balances, prices);
1018
+ let gasPrice;
1019
+ try {
1020
+ gasPrice = await this.client.getGasPrice();
1021
+ } catch {
1022
+ }
1007
1023
  return {
1008
1024
  timestamp: Date.now(),
1009
1025
  prices,
1010
1026
  balances,
1011
- portfolioValue
1027
+ portfolioValue,
1028
+ volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
1029
+ priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
1030
+ gasPrice,
1031
+ network: {
1032
+ chainId: this.client.chain?.id ?? 8453
1033
+ }
1012
1034
  };
1013
1035
  }
1014
1036
  /**
1015
- * Fetch token prices from price oracle
1037
+ * Check if cached prices are still fresh
1038
+ */
1039
+ get pricesAreFresh() {
1040
+ return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
1041
+ }
1042
+ /**
1043
+ * Fetch token prices from CoinGecko free API
1044
+ * Returns cached prices if still fresh (<60s old)
1016
1045
  */
1017
1046
  async fetchPrices(tokenAddresses) {
1047
+ if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
1048
+ const prices2 = { ...this.cachedPrices };
1049
+ for (const addr of tokenAddresses) {
1050
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1051
+ if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
1052
+ prices2[addr.toLowerCase()] = 1;
1053
+ }
1054
+ }
1055
+ return prices2;
1056
+ }
1018
1057
  const prices = {};
1019
- const knownPrices = {
1020
- // Native ETH (sentinel address)
1021
- [NATIVE_ETH.toLowerCase()]: 3500,
1022
- // WETH
1023
- "0x4200000000000000000000000000000000000006": 3500,
1024
- // USDC
1025
- "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 1,
1026
- // USDbC (bridged USDC)
1027
- "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 1
1028
- };
1029
- prices[NATIVE_ETH.toLowerCase()] = knownPrices[NATIVE_ETH.toLowerCase()];
1030
- for (const address of tokenAddresses) {
1031
- prices[address.toLowerCase()] = knownPrices[address.toLowerCase()] || 0;
1058
+ const idsToFetch = /* @__PURE__ */ new Set();
1059
+ for (const addr of tokenAddresses) {
1060
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1061
+ if (cgId && !STABLECOIN_IDS.has(cgId)) {
1062
+ idsToFetch.add(cgId);
1063
+ }
1064
+ }
1065
+ idsToFetch.add("ethereum");
1066
+ if (idsToFetch.size > 0) {
1067
+ try {
1068
+ const ids = Array.from(idsToFetch).join(",");
1069
+ const response = await fetch(
1070
+ `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
1071
+ { signal: AbortSignal.timeout(5e3) }
1072
+ );
1073
+ if (response.ok) {
1074
+ const data = await response.json();
1075
+ for (const [cgId, priceData] of Object.entries(data)) {
1076
+ for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
1077
+ if (id === cgId) {
1078
+ const key = addr.toLowerCase();
1079
+ prices[key] = priceData.usd;
1080
+ if (priceData.usd_24h_vol !== void 0) {
1081
+ this.cachedVolume24h[key] = priceData.usd_24h_vol;
1082
+ }
1083
+ if (priceData.usd_24h_change !== void 0) {
1084
+ this.cachedPriceChange24h[key] = priceData.usd_24h_change;
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+ this.lastPriceFetchAt = Date.now();
1090
+ } else {
1091
+ console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
1092
+ }
1093
+ } catch (error) {
1094
+ console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
1095
+ }
1096
+ }
1097
+ for (const addr of tokenAddresses) {
1098
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
1099
+ if (cgId && STABLECOIN_IDS.has(cgId)) {
1100
+ prices[addr.toLowerCase()] = 1;
1101
+ }
1102
+ }
1103
+ const missingAddrs = tokenAddresses.filter(
1104
+ (addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
1105
+ );
1106
+ if (missingAddrs.length > 0) {
1107
+ try {
1108
+ const coins = missingAddrs.map((a) => `base:${a}`).join(",");
1109
+ const llamaResponse = await fetch(
1110
+ `https://coins.llama.fi/prices/current/${coins}`,
1111
+ { signal: AbortSignal.timeout(5e3) }
1112
+ );
1113
+ if (llamaResponse.ok) {
1114
+ const llamaData = await llamaResponse.json();
1115
+ for (const [key, data] of Object.entries(llamaData.coins)) {
1116
+ const addr = key.replace("base:", "").toLowerCase();
1117
+ if (data.price && data.confidence > 0.5) {
1118
+ prices[addr] = data.price;
1119
+ }
1120
+ }
1121
+ if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
1122
+ }
1123
+ } catch {
1124
+ }
1125
+ }
1126
+ if (Object.keys(prices).length > 0) {
1127
+ this.cachedPrices = prices;
1128
+ }
1129
+ if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
1130
+ console.warn("Using cached prices (last successful fetch was stale)");
1131
+ return { ...this.cachedPrices };
1132
+ }
1133
+ for (const addr of tokenAddresses) {
1134
+ if (!prices[addr.toLowerCase()]) {
1135
+ console.warn(`No price available for ${addr}, using 0`);
1136
+ prices[addr.toLowerCase()] = 0;
1137
+ }
1032
1138
  }
1033
1139
  return prices;
1034
1140
  }
@@ -1074,7 +1180,7 @@ var MarketDataService = class {
1074
1180
  let total = 0;
1075
1181
  for (const [address, balance] of Object.entries(balances)) {
1076
1182
  const price = prices[address.toLowerCase()] || 0;
1077
- const decimals = USDC_DECIMALS[address.toLowerCase()] || 18;
1183
+ const decimals = getTokenDecimals(address);
1078
1184
  const amount = Number(balance) / Math.pow(10, decimals);
1079
1185
  total += amount * price;
1080
1186
  }
@@ -1082,22 +1188,138 @@ var MarketDataService = class {
1082
1188
  }
1083
1189
  };
1084
1190
 
1191
+ // src/trading/risk.ts
1192
+ var RiskManager = class {
1193
+ config;
1194
+ dailyPnL = 0;
1195
+ dailyFees = 0;
1196
+ lastResetDate = "";
1197
+ /** Minimum trade value in USD — trades below this are rejected as dust */
1198
+ minTradeValueUSD;
1199
+ constructor(config) {
1200
+ this.config = config;
1201
+ this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
1202
+ }
1203
+ /**
1204
+ * Filter signals through risk checks
1205
+ * Returns only signals that pass all guardrails
1206
+ */
1207
+ filterSignals(signals, marketData) {
1208
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1209
+ if (today !== this.lastResetDate) {
1210
+ this.dailyPnL = 0;
1211
+ this.dailyFees = 0;
1212
+ this.lastResetDate = today;
1213
+ }
1214
+ if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
1215
+ console.warn("Daily loss limit reached - no new trades");
1216
+ return [];
1217
+ }
1218
+ return signals.filter((signal) => this.validateSignal(signal, marketData));
1219
+ }
1220
+ /**
1221
+ * Validate individual signal against risk limits
1222
+ */
1223
+ validateSignal(signal, marketData) {
1224
+ if (signal.action === "hold") {
1225
+ return true;
1226
+ }
1227
+ const signalValue = this.estimateSignalValue(signal, marketData);
1228
+ const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
1229
+ if (signalValue > maxPositionValue) {
1230
+ console.warn(
1231
+ `Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
1232
+ );
1233
+ return false;
1234
+ }
1235
+ if (signal.confidence < 0.5) {
1236
+ console.warn(`Signal confidence too low: ${signal.confidence}`);
1237
+ return false;
1238
+ }
1239
+ if (signal.action === "buy" && this.config.maxConcurrentPositions) {
1240
+ const activePositions = this.countActivePositions(marketData);
1241
+ if (activePositions >= this.config.maxConcurrentPositions) {
1242
+ console.warn(
1243
+ `Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
1244
+ );
1245
+ return false;
1246
+ }
1247
+ }
1248
+ if (signalValue < this.minTradeValueUSD) {
1249
+ console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
1250
+ return false;
1251
+ }
1252
+ return true;
1253
+ }
1254
+ /**
1255
+ * Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
1256
+ */
1257
+ countActivePositions(marketData) {
1258
+ const NATIVE_ETH_KEY = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
1259
+ let count = 0;
1260
+ for (const [address, balance] of Object.entries(marketData.balances)) {
1261
+ if (address.toLowerCase() === NATIVE_ETH_KEY) continue;
1262
+ if (balance > 0n) count++;
1263
+ }
1264
+ return count;
1265
+ }
1266
+ /**
1267
+ * Check if daily loss limit has been hit
1268
+ */
1269
+ isDailyLossLimitHit(portfolioValue) {
1270
+ const maxLoss = portfolioValue * this.config.maxDailyLossBps / 1e4;
1271
+ return this.dailyPnL < -maxLoss;
1272
+ }
1273
+ /**
1274
+ * Estimate USD value of a trade signal
1275
+ */
1276
+ estimateSignalValue(signal, marketData) {
1277
+ const price = marketData.prices[signal.tokenIn.toLowerCase()] || 0;
1278
+ const tokenDecimals = getTokenDecimals(signal.tokenIn);
1279
+ const amount = Number(signal.amountIn) / Math.pow(10, tokenDecimals);
1280
+ return amount * price;
1281
+ }
1282
+ /**
1283
+ * Update daily PnL after a trade (market gains/losses only)
1284
+ */
1285
+ updatePnL(pnl) {
1286
+ this.dailyPnL += pnl;
1287
+ }
1288
+ /**
1289
+ * Update daily fees (trading fees, gas costs, etc.)
1290
+ * Fees are tracked separately and do NOT count toward the daily loss limit.
1291
+ * This prevents protocol fees from triggering circuit breakers.
1292
+ */
1293
+ updateFees(fees) {
1294
+ this.dailyFees += fees;
1295
+ }
1296
+ /**
1297
+ * Get current risk status
1298
+ * @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
1299
+ */
1300
+ getStatus(portfolioValue) {
1301
+ const pv = portfolioValue || 0;
1302
+ const maxLossUSD = pv * this.config.maxDailyLossBps / 1e4;
1303
+ return {
1304
+ dailyPnL: this.dailyPnL,
1305
+ dailyFees: this.dailyFees,
1306
+ dailyNetPnL: this.dailyPnL - this.dailyFees,
1307
+ dailyLossLimit: maxLossUSD,
1308
+ // Only market PnL triggers the limit — fees are excluded
1309
+ isLimitHit: pv > 0 ? this.dailyPnL < -maxLossUSD : false
1310
+ };
1311
+ }
1312
+ };
1313
+
1085
1314
  // src/vault/manager.ts
1086
1315
  var import_viem2 = require("viem");
1087
1316
  var import_accounts = require("viem/accounts");
1088
1317
  var import_chains = require("viem/chains");
1089
1318
  var ADDRESSES = {
1090
- testnet: {
1091
- vaultFactory: "0x5c099daaE33801a907Bb57011c6749655b55dc75",
1092
- registry: "0xCF48C341e3FebeCA5ECB7eb2535f61A2Ba855d9C",
1093
- usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
1094
- },
1095
1319
  mainnet: {
1096
- vaultFactory: "0x0000000000000000000000000000000000000000",
1097
- // TODO: Deploy
1098
- registry: "0x0000000000000000000000000000000000000000",
1320
+ vaultFactory: process.env.EXAGENT_VAULT_FACTORY_ADDRESS || "0x0000000000000000000000000000000000000000",
1321
+ registry: process.env.EXAGENT_REGISTRY_ADDRESS || "0x0000000000000000000000000000000000000000",
1099
1322
  usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
1100
- // Base mainnet USDC
1101
1323
  }
1102
1324
  };
1103
1325
  var VAULT_FACTORY_ABI = [
@@ -1121,6 +1343,7 @@ var VAULT_FACTORY_ABI = [
1121
1343
  inputs: [
1122
1344
  { name: "agentId", type: "uint256" },
1123
1345
  { name: "asset", type: "address" },
1346
+ { name: "seedAmount", type: "uint256" },
1124
1347
  { name: "name", type: "string" },
1125
1348
  { name: "symbol", type: "string" },
1126
1349
  { name: "feeRecipient", type: "address" }
@@ -1134,13 +1357,6 @@ var VAULT_FACTORY_ABI = [
1134
1357
  inputs: [],
1135
1358
  outputs: [{ type: "uint256" }],
1136
1359
  stateMutability: "view"
1137
- },
1138
- {
1139
- type: "function",
1140
- name: "eXABurnFee",
1141
- inputs: [],
1142
- outputs: [{ type: "uint256" }],
1143
- stateMutability: "view"
1144
1360
  }
1145
1361
  ];
1146
1362
  var VAULT_ABI = [
@@ -1180,12 +1396,13 @@ var VaultManager = class {
1180
1396
  lastVaultCheck = 0;
1181
1397
  VAULT_CACHE_TTL = 6e4;
1182
1398
  // 1 minute
1399
+ enabled = true;
1183
1400
  constructor(config) {
1184
1401
  this.config = config;
1185
1402
  this.addresses = ADDRESSES[config.network];
1186
1403
  this.account = (0, import_accounts.privateKeyToAccount)(config.walletKey);
1187
- this.chain = config.network === "mainnet" ? import_chains.base : import_chains.baseSepolia;
1188
- const rpcUrl = config.network === "mainnet" ? "https://mainnet.base.org" : "https://sepolia.base.org";
1404
+ this.chain = import_chains.base;
1405
+ const rpcUrl = "https://mainnet.base.org";
1189
1406
  this.publicClient = (0, import_viem2.createPublicClient)({
1190
1407
  chain: this.chain,
1191
1408
  transport: (0, import_viem2.http)(rpcUrl)
@@ -1195,6 +1412,10 @@ var VaultManager = class {
1195
1412
  chain: this.chain,
1196
1413
  transport: (0, import_viem2.http)(rpcUrl)
1197
1414
  });
1415
+ if (this.addresses.vaultFactory === "0x0000000000000000000000000000000000000000") {
1416
+ console.warn("VaultFactory address is zero \u2014 vault operations will be disabled");
1417
+ this.enabled = false;
1418
+ }
1198
1419
  }
1199
1420
  /**
1200
1421
  * Get the agent's vault policy
@@ -1212,6 +1433,17 @@ var VaultManager = class {
1212
1433
  * Get comprehensive vault status
1213
1434
  */
1214
1435
  async getVaultStatus() {
1436
+ if (!this.enabled) {
1437
+ return {
1438
+ hasVault: false,
1439
+ vaultAddress: null,
1440
+ totalAssets: BigInt(0),
1441
+ canCreateVault: false,
1442
+ cannotCreateReason: "Vault operations disabled (contract address not set)",
1443
+ requirementsMet: false,
1444
+ requirements: { veXARequired: BigInt(0), isBypassed: false }
1445
+ };
1446
+ }
1215
1447
  const vaultAddress = await this.getVaultAddress();
1216
1448
  const hasVault = vaultAddress !== null;
1217
1449
  let totalAssets = BigInt(0);
@@ -1246,22 +1478,16 @@ var VaultManager = class {
1246
1478
  }
1247
1479
  /**
1248
1480
  * Get vault creation requirements
1481
+ * Note: No burnFee on mainnet — vault creation requires USDC seed instead
1249
1482
  */
1250
1483
  async getRequirements() {
1251
- const [veXARequired, burnFee] = await Promise.all([
1252
- this.publicClient.readContract({
1253
- address: this.addresses.vaultFactory,
1254
- abi: VAULT_FACTORY_ABI,
1255
- functionName: "minimumVeEXARequired"
1256
- }),
1257
- this.publicClient.readContract({
1258
- address: this.addresses.vaultFactory,
1259
- abi: VAULT_FACTORY_ABI,
1260
- functionName: "eXABurnFee"
1261
- })
1262
- ]);
1263
- const isBypassed = veXARequired === BigInt(0) && burnFee === BigInt(0);
1264
- return { veXARequired, burnFee, isBypassed };
1484
+ const veXARequired = await this.publicClient.readContract({
1485
+ address: this.addresses.vaultFactory,
1486
+ abi: VAULT_FACTORY_ABI,
1487
+ functionName: "minimumVeEXARequired"
1488
+ });
1489
+ const isBypassed = veXARequired === BigInt(0);
1490
+ return { veXARequired, isBypassed };
1265
1491
  }
1266
1492
  /**
1267
1493
  * Get the agent's vault address (cached)
@@ -1285,30 +1511,15 @@ var VaultManager = class {
1285
1511
  this.cachedVaultAddress = vaultAddress;
1286
1512
  return vaultAddress;
1287
1513
  }
1288
- /**
1289
- * Check if the agent should create a vault based on policy and qualification
1290
- */
1291
- async shouldCreateVault() {
1292
- if (this.policy === "disabled") {
1293
- return { should: false, reason: "Vault creation disabled by policy" };
1294
- }
1295
- if (this.policy === "manual") {
1296
- return { should: false, reason: "Vault creation set to manual - waiting for owner instruction" };
1297
- }
1298
- const status = await this.getVaultStatus();
1299
- if (status.hasVault) {
1300
- return { should: false, reason: "Vault already exists" };
1301
- }
1302
- if (!status.canCreateVault) {
1303
- return { should: false, reason: status.cannotCreateReason || "Requirements not met" };
1304
- }
1305
- return { should: true, reason: "Agent is qualified and auto-creation is enabled" };
1306
- }
1307
1514
  /**
1308
1515
  * Create a vault for the agent
1516
+ * @param seedAmount - USDC seed amount in raw units (default: 100e6 = 100 USDC)
1309
1517
  * @returns Vault address if successful
1310
1518
  */
1311
- async createVault() {
1519
+ async createVault(seedAmount) {
1520
+ if (!this.enabled) {
1521
+ return { success: false, error: "Vault operations disabled (contract address not set)" };
1522
+ }
1312
1523
  if (this.policy === "disabled") {
1313
1524
  return { success: false, error: "Vault creation disabled by policy" };
1314
1525
  }
@@ -1320,6 +1531,7 @@ var VaultManager = class {
1320
1531
  if (!status.canCreateVault) {
1321
1532
  return { success: false, error: status.cannotCreateReason || "Requirements not met" };
1322
1533
  }
1534
+ const seed = seedAmount || BigInt(1e8);
1323
1535
  const vaultName = this.config.vaultConfig.defaultName || `${this.config.agentName} Trading Vault`;
1324
1536
  const vaultSymbol = this.config.vaultConfig.defaultSymbol || `ex${this.config.agentName.replace(/[^a-zA-Z]/g, "").slice(0, 4).toUpperCase()}`;
1325
1537
  const feeRecipient = this.config.vaultConfig.feeRecipient || this.account.address;
@@ -1331,6 +1543,7 @@ var VaultManager = class {
1331
1543
  args: [
1332
1544
  this.config.agentId,
1333
1545
  this.addresses.usdc,
1546
+ seed,
1334
1547
  vaultName,
1335
1548
  vaultSymbol,
1336
1549
  feeRecipient
@@ -1394,37 +1607,12 @@ var VaultManager = class {
1394
1607
  };
1395
1608
  }
1396
1609
  }
1397
- /**
1398
- * Run the auto-creation check (call this periodically in the agent loop)
1399
- * Only creates vault if policy is 'auto_when_qualified'
1400
- */
1401
- async checkAndAutoCreateVault() {
1402
- const shouldCreate = await this.shouldCreateVault();
1403
- if (!shouldCreate.should) {
1404
- const status = await this.getVaultStatus();
1405
- if (status.hasVault) {
1406
- return { action: "already_exists", vaultAddress: status.vaultAddress, reason: "Vault already exists" };
1407
- }
1408
- if (this.policy !== "auto_when_qualified") {
1409
- return { action: "skipped", reason: shouldCreate.reason };
1410
- }
1411
- return { action: "not_qualified", reason: shouldCreate.reason };
1412
- }
1413
- const result = await this.createVault();
1414
- if (result.success) {
1415
- return {
1416
- action: "created",
1417
- vaultAddress: result.vaultAddress,
1418
- reason: "Vault created automatically"
1419
- };
1420
- }
1421
- return { action: "not_qualified", reason: result.error || "Creation failed" };
1422
- }
1423
1610
  };
1424
1611
 
1425
1612
  // src/relay.ts
1426
1613
  var import_ws = __toESM(require("ws"));
1427
1614
  var import_accounts2 = require("viem/accounts");
1615
+ var import_sdk = require("@exagent/sdk");
1428
1616
  var RelayClient = class {
1429
1617
  config;
1430
1618
  ws = null;
@@ -1527,7 +1715,8 @@ var RelayClient = class {
1527
1715
  agentId: this.config.agentId,
1528
1716
  wallet: account.address,
1529
1717
  timestamp,
1530
- signature
1718
+ signature,
1719
+ sdkVersion: import_sdk.SDK_VERSION
1531
1720
  });
1532
1721
  }
1533
1722
  /**
@@ -1689,6 +1878,7 @@ function openBrowser(url) {
1689
1878
 
1690
1879
  // src/runtime.ts
1691
1880
  var FUNDS_LOW_THRESHOLD = 5e-3;
1881
+ var FUNDS_CRITICAL_THRESHOLD = 1e-3;
1692
1882
  var AgentRuntime = class {
1693
1883
  config;
1694
1884
  client;
@@ -1702,14 +1892,13 @@ var AgentRuntime = class {
1702
1892
  isRunning = false;
1703
1893
  mode = "idle";
1704
1894
  configHash;
1705
- lastVaultCheck = 0;
1706
1895
  cycleCount = 0;
1707
1896
  lastCycleAt = 0;
1708
1897
  lastPortfolioValue = 0;
1709
1898
  lastEthBalance = "0";
1710
1899
  processAlive = true;
1711
- VAULT_CHECK_INTERVAL = 3e5;
1712
- // Check vault status every 5 minutes
1900
+ riskUniverse = 0;
1901
+ allowedTokens = /* @__PURE__ */ new Set();
1713
1902
  constructor(config) {
1714
1903
  this.config = config;
1715
1904
  }
@@ -1718,7 +1907,7 @@ var AgentRuntime = class {
1718
1907
  */
1719
1908
  async initialize() {
1720
1909
  console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
1721
- this.client = new import_sdk.ExagentClient({
1910
+ this.client = new import_sdk2.ExagentClient({
1722
1911
  privateKey: this.config.privateKey,
1723
1912
  network: this.config.network
1724
1913
  });
@@ -1729,6 +1918,7 @@ var AgentRuntime = class {
1729
1918
  }
1730
1919
  console.log(`Agent verified: ${agent.name}`);
1731
1920
  await this.ensureWalletLinked();
1921
+ await this.loadTradingRestrictions();
1732
1922
  console.log(`Initializing LLM: ${this.config.llm.provider}`);
1733
1923
  this.llm = await createLLMAdapter(this.config.llm);
1734
1924
  const llmMeta = this.llm.getMetadata();
@@ -1796,12 +1986,8 @@ var AgentRuntime = class {
1796
1986
  console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
1797
1987
  } else {
1798
1988
  console.log("No vault exists for this agent");
1799
- if (vaultConfig.policy === "auto_when_qualified") {
1800
- if (status.canCreateVault) {
1801
- console.log("Agent is qualified to create vault - will attempt on next check");
1802
- } else {
1803
- console.log(`Cannot create vault yet: ${status.cannotCreateReason}`);
1804
- }
1989
+ if (vaultConfig.policy === "manual") {
1990
+ console.log("Vault creation is manual \u2014 use the command center to create one");
1805
1991
  }
1806
1992
  }
1807
1993
  }
@@ -1820,7 +2006,7 @@ var AgentRuntime = class {
1820
2006
  if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
1821
2007
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
1822
2008
  const nonce = await this.client.registry.getNonce(address);
1823
- const linkMessage = import_sdk.ExagentRegistry.generateLinkMessage(
2009
+ const linkMessage = import_sdk2.ExagentRegistry.generateLinkMessage(
1824
2010
  address,
1825
2011
  agentId,
1826
2012
  nonce
@@ -1860,6 +2046,49 @@ var AgentRuntime = class {
1860
2046
  console.log("Wallet already linked");
1861
2047
  }
1862
2048
  }
2049
+ /**
2050
+ * Load risk universe and allowed tokens from on-chain registry.
2051
+ * This prevents the agent from wasting gas on trades that will revert.
2052
+ */
2053
+ async loadTradingRestrictions() {
2054
+ const agentId = BigInt(this.config.agentId);
2055
+ const RISK_UNIVERSE_NAMES = ["Core", "Established", "Derivatives", "Emerging", "Frontier"];
2056
+ try {
2057
+ this.riskUniverse = await this.client.registry.getRiskUniverse(agentId);
2058
+ console.log(`Risk universe: ${RISK_UNIVERSE_NAMES[this.riskUniverse] || this.riskUniverse}`);
2059
+ const configTokens = this.config.allowedTokens || this.getDefaultTokens();
2060
+ const verified = [];
2061
+ for (const token of configTokens) {
2062
+ try {
2063
+ const allowed = await this.client.registry.isTradeAllowed(
2064
+ agentId,
2065
+ token,
2066
+ "0x0000000000000000000000000000000000000000"
2067
+ // zero = check token only
2068
+ );
2069
+ if (allowed) {
2070
+ this.allowedTokens.add(token.toLowerCase());
2071
+ verified.push(token);
2072
+ } else {
2073
+ console.warn(`Token ${token} not allowed for this agent's risk universe \u2014 excluded`);
2074
+ }
2075
+ } catch {
2076
+ this.allowedTokens.add(token.toLowerCase());
2077
+ verified.push(token);
2078
+ }
2079
+ }
2080
+ this.config.allowedTokens = verified;
2081
+ console.log(`Allowed tokens loaded: ${verified.length} tokens verified`);
2082
+ if (this.riskUniverse === 4) {
2083
+ console.warn("Frontier risk universe: vault creation is disabled");
2084
+ }
2085
+ } catch (error) {
2086
+ console.warn(
2087
+ "Could not load trading restrictions from registry (using defaults):",
2088
+ error instanceof Error ? error.message : error
2089
+ );
2090
+ }
2091
+ }
1863
2092
  /**
1864
2093
  * Sync the LLM config hash to chain for epoch tracking.
1865
2094
  * If the wallet has insufficient gas, enters a recovery loop
@@ -1868,7 +2097,7 @@ var AgentRuntime = class {
1868
2097
  async syncConfigHash() {
1869
2098
  const agentId = BigInt(this.config.agentId);
1870
2099
  const llmMeta = this.llm.getMetadata();
1871
- this.configHash = import_sdk.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
2100
+ this.configHash = import_sdk2.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
1872
2101
  console.log(`Config hash: ${this.configHash}`);
1873
2102
  const onChainHash = await this.client.registry.getConfigHash(agentId);
1874
2103
  if (onChainHash !== this.configHash) {
@@ -1881,7 +2110,7 @@ var AgentRuntime = class {
1881
2110
  const message = error instanceof Error ? error.message : String(error);
1882
2111
  if (message.includes("insufficient funds") || message.includes("gas") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
1883
2112
  const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
1884
- const chain = this.config.network === "mainnet" ? import_chains2.base : import_chains2.baseSepolia;
2113
+ const chain = import_chains2.base;
1885
2114
  const publicClientInstance = (0, import_viem3.createPublicClient)({
1886
2115
  chain,
1887
2116
  transport: (0, import_viem3.http)(this.getRpcUrl())
@@ -2032,21 +2261,38 @@ var AgentRuntime = class {
2032
2261
  break;
2033
2262
  case "update_risk_params": {
2034
2263
  const params = cmd.params || {};
2264
+ let updated = false;
2035
2265
  if (params.maxPositionSizeBps !== void 0) {
2036
- this.config.trading.maxPositionSizeBps = Number(params.maxPositionSizeBps);
2266
+ const value = Number(params.maxPositionSizeBps);
2267
+ if (isNaN(value) || value < 100 || value > 1e4) {
2268
+ this.relay?.sendCommandResult(cmd.id, false, "maxPositionSizeBps must be 100-10000");
2269
+ break;
2270
+ }
2271
+ this.config.trading.maxPositionSizeBps = value;
2272
+ updated = true;
2037
2273
  }
2038
2274
  if (params.maxDailyLossBps !== void 0) {
2039
- this.config.trading.maxDailyLossBps = Number(params.maxDailyLossBps);
2275
+ const value = Number(params.maxDailyLossBps);
2276
+ if (isNaN(value) || value < 50 || value > 5e3) {
2277
+ this.relay?.sendCommandResult(cmd.id, false, "maxDailyLossBps must be 50-5000");
2278
+ break;
2279
+ }
2280
+ this.config.trading.maxDailyLossBps = value;
2281
+ updated = true;
2282
+ }
2283
+ if (updated) {
2284
+ this.riskManager = new RiskManager(this.config.trading);
2285
+ console.log("Risk params updated via command center");
2286
+ this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
2287
+ this.relay?.sendMessage(
2288
+ "config_updated",
2289
+ "info",
2290
+ "Risk Parameters Updated",
2291
+ `Max position: ${this.config.trading.maxPositionSizeBps / 100}%, Max daily loss: ${this.config.trading.maxDailyLossBps / 100}%`
2292
+ );
2293
+ } else {
2294
+ this.relay?.sendCommandResult(cmd.id, false, "No valid parameters provided");
2040
2295
  }
2041
- this.riskManager = new RiskManager(this.config.trading);
2042
- console.log("Risk params updated via command center");
2043
- this.relay?.sendCommandResult(cmd.id, true, "Risk params updated");
2044
- this.relay?.sendMessage(
2045
- "config_updated",
2046
- "info",
2047
- "Risk Parameters Updated",
2048
- `Max position: ${this.config.trading.maxPositionSizeBps / 100}%, Max daily loss: ${this.config.trading.maxDailyLossBps / 100}%`
2049
- );
2050
2296
  break;
2051
2297
  }
2052
2298
  case "update_trading_interval": {
@@ -2123,7 +2369,7 @@ var AgentRuntime = class {
2123
2369
  provider: this.config.llm.provider,
2124
2370
  model: this.config.llm.model || "default"
2125
2371
  },
2126
- risk: this.riskManager?.getStatus() || {
2372
+ risk: this.riskManager?.getStatus(this.lastPortfolioValue) || {
2127
2373
  dailyPnL: 0,
2128
2374
  dailyLossLimit: 0,
2129
2375
  isLimitHit: false
@@ -2144,14 +2390,18 @@ var AgentRuntime = class {
2144
2390
  --- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
2145
2391
  this.cycleCount++;
2146
2392
  this.lastCycleAt = Date.now();
2147
- await this.checkVaultAutoCreation();
2148
2393
  const tokens = this.config.allowedTokens || this.getDefaultTokens();
2149
2394
  const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
2150
2395
  console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
2151
2396
  this.lastPortfolioValue = marketData.portfolioValue;
2152
2397
  const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
2153
2398
  this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
2154
- this.checkFundsLow(marketData);
2399
+ const fundsOk = this.checkFundsLow(marketData);
2400
+ if (!fundsOk) {
2401
+ console.warn("Skipping trading cycle \u2014 ETH balance critically low");
2402
+ this.sendRelayStatus();
2403
+ return;
2404
+ }
2155
2405
  let signals;
2156
2406
  try {
2157
2407
  signals = await this.strategy(marketData, this.llm, this.config);
@@ -2169,19 +2419,31 @@ var AgentRuntime = class {
2169
2419
  console.log(`Strategy generated ${signals.length} signals`);
2170
2420
  const filteredSignals = this.riskManager.filterSignals(signals, marketData);
2171
2421
  console.log(`${filteredSignals.length} signals passed risk checks`);
2172
- if (this.riskManager.getStatus().isLimitHit) {
2422
+ if (this.riskManager.getStatus(marketData.portfolioValue).isLimitHit) {
2173
2423
  this.relay?.sendMessage(
2174
2424
  "risk_limit_hit",
2175
2425
  "warning",
2176
2426
  "Risk Limit Hit",
2177
- `Daily loss limit reached: ${this.riskManager.getStatus().dailyPnL.toFixed(2)}`
2427
+ `Daily loss limit reached: $${this.riskManager.getStatus(marketData.portfolioValue).dailyPnL.toFixed(2)}`
2178
2428
  );
2179
2429
  }
2180
2430
  if (filteredSignals.length > 0) {
2431
+ const vaultStatus = await this.vaultManager?.getVaultStatus();
2432
+ if (vaultStatus?.hasVault && this.vaultManager?.preferVaultTrading) {
2433
+ console.log(`Trading through vault: ${vaultStatus.vaultAddress}`);
2434
+ }
2435
+ const preTradePortfolioValue = marketData.portfolioValue;
2181
2436
  const results = await this.executor.executeAll(filteredSignals);
2437
+ let totalFeesUSD = 0;
2182
2438
  for (const result of results) {
2183
2439
  if (result.success) {
2184
2440
  console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
2441
+ const feeCostBps = 20;
2442
+ const signalPrice = marketData.prices[result.signal.tokenIn.toLowerCase()] || 0;
2443
+ const amountUSD = Number(result.signal.amountIn) / Math.pow(10, getTokenDecimals(result.signal.tokenIn)) * signalPrice;
2444
+ const feeCostUSD = amountUSD * feeCostBps / 1e4;
2445
+ totalFeesUSD += feeCostUSD;
2446
+ this.riskManager.updateFees(feeCostUSD);
2185
2447
  this.relay?.sendMessage(
2186
2448
  "trade_executed",
2187
2449
  "success",
@@ -2205,18 +2467,43 @@ var AgentRuntime = class {
2205
2467
  );
2206
2468
  }
2207
2469
  }
2470
+ const postTokens = this.config.allowedTokens || this.getDefaultTokens();
2471
+ const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
2472
+ const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
2473
+ this.riskManager.updatePnL(marketPnL);
2474
+ if (marketPnL !== 0) {
2475
+ console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
2476
+ }
2477
+ this.lastPortfolioValue = postTradeData.portfolioValue;
2478
+ const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
2479
+ this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
2208
2480
  }
2209
2481
  this.sendRelayStatus();
2210
2482
  }
2211
2483
  /**
2212
- * Check if ETH balance is below threshold and notify
2484
+ * Check if ETH balance is below threshold and notify.
2485
+ * Returns true if trading should continue, false if ETH is critically low.
2213
2486
  */
2214
2487
  checkFundsLow(marketData) {
2215
- if (!this.relay) return;
2216
2488
  const ethBalance = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
2217
2489
  const ethAmount = Number(ethBalance) / 1e18;
2490
+ if (ethAmount < FUNDS_CRITICAL_THRESHOLD) {
2491
+ console.error(`ETH balance critically low: ${ethAmount.toFixed(6)} ETH \u2014 halting trading`);
2492
+ this.relay?.sendMessage(
2493
+ "funds_low",
2494
+ "error",
2495
+ "Funds Critical",
2496
+ `ETH balance is ${ethAmount.toFixed(6)} ETH (below ${FUNDS_CRITICAL_THRESHOLD} ETH minimum). Trading halted \u2014 fund your wallet.`,
2497
+ {
2498
+ ethBalance: ethAmount.toFixed(6),
2499
+ wallet: this.client.address,
2500
+ threshold: FUNDS_CRITICAL_THRESHOLD
2501
+ }
2502
+ );
2503
+ return false;
2504
+ }
2218
2505
  if (ethAmount < FUNDS_LOW_THRESHOLD) {
2219
- this.relay.sendMessage(
2506
+ this.relay?.sendMessage(
2220
2507
  "funds_low",
2221
2508
  "warning",
2222
2509
  "Low Funds",
@@ -2228,36 +2515,7 @@ var AgentRuntime = class {
2228
2515
  }
2229
2516
  );
2230
2517
  }
2231
- }
2232
- /**
2233
- * Check for vault auto-creation based on policy
2234
- */
2235
- async checkVaultAutoCreation() {
2236
- const now = Date.now();
2237
- if (now - this.lastVaultCheck < this.VAULT_CHECK_INTERVAL) {
2238
- return;
2239
- }
2240
- this.lastVaultCheck = now;
2241
- const result = await this.vaultManager.checkAndAutoCreateVault();
2242
- switch (result.action) {
2243
- case "created":
2244
- console.log(`Vault created automatically: ${result.vaultAddress}`);
2245
- this.relay?.sendMessage(
2246
- "vault_created",
2247
- "success",
2248
- "Vault Auto-Created",
2249
- `Vault deployed at ${result.vaultAddress}`,
2250
- { vaultAddress: result.vaultAddress }
2251
- );
2252
- break;
2253
- case "already_exists":
2254
- break;
2255
- case "skipped":
2256
- break;
2257
- case "not_qualified":
2258
- console.log(`Vault auto-creation pending: ${result.reason}`);
2259
- break;
2260
- }
2518
+ return true;
2261
2519
  }
2262
2520
  /**
2263
2521
  * Stop the agent process completely
@@ -2275,26 +2533,44 @@ var AgentRuntime = class {
2275
2533
  * Get RPC URL based on network
2276
2534
  */
2277
2535
  getRpcUrl() {
2278
- if (this.config.network === "mainnet") {
2279
- return "https://mainnet.base.org";
2280
- }
2281
- return "https://sepolia.base.org";
2536
+ return "https://mainnet.base.org";
2282
2537
  }
2283
2538
  /**
2284
- * Default tokens to track
2539
+ * Default tokens to track.
2540
+ * These are validated against the on-chain registry's isTradeAllowed() during init —
2541
+ * agents in restricted risk universes will have ineligible tokens filtered out.
2285
2542
  */
2286
2543
  getDefaultTokens() {
2287
- if (this.config.network === "mainnet") {
2288
- return [
2289
- "0x4200000000000000000000000000000000000006",
2290
- // WETH
2291
- "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
2292
- // USDC
2293
- ];
2294
- }
2295
2544
  return [
2296
- "0x4200000000000000000000000000000000000006"
2545
+ // Core
2546
+ "0x4200000000000000000000000000000000000006",
2297
2547
  // WETH
2548
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
2549
+ // USDC
2550
+ "0x2Ae3F1Ec7F1F5012CFEab0185bFC7aa3cf0DEC22",
2551
+ // cbETH
2552
+ "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA",
2553
+ // USDbC
2554
+ "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
2555
+ // DAI
2556
+ // Established
2557
+ "0x940181a94A35A4569E4529A3CDfB74e38FD98631",
2558
+ // AERO
2559
+ "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
2560
+ // cbBTC
2561
+ "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
2562
+ // wstETH
2563
+ "0x2416092f143378750bb29b79eD961ab195CcEea5",
2564
+ // ezETH
2565
+ // Emerging (filtered by risk universe at init)
2566
+ "0x532f27101965dd16442E59d40670FaF5eBB142E4",
2567
+ // BRETT
2568
+ "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
2569
+ // DEGEN
2570
+ "0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b",
2571
+ // VIRTUAL
2572
+ "0xAC1Bd2486Aaf3B5C0fc3Fd868558b082a531B2B4"
2573
+ // TOSHI
2298
2574
  ];
2299
2575
  }
2300
2576
  sleep(ms) {
@@ -2315,7 +2591,7 @@ var AgentRuntime = class {
2315
2591
  model: this.config.llm.model || "default"
2316
2592
  },
2317
2593
  configHash: this.configHash || "not initialized",
2318
- risk: this.riskManager?.getStatus() || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false },
2594
+ risk: this.riskManager?.getStatus(this.lastPortfolioValue) || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false },
2319
2595
  vault: {
2320
2596
  policy: vaultConfig.policy,
2321
2597
  hasVault: false,
@@ -2377,16 +2653,18 @@ var TradingConfigSchema = import_zod.z.object({
2377
2653
  maxDailyLossBps: import_zod.z.number().min(0).max(1e4).default(500),
2378
2654
  // 0-100%
2379
2655
  maxConcurrentPositions: import_zod.z.number().min(1).max(100).default(5),
2380
- tradingIntervalMs: import_zod.z.number().min(1e3).default(6e4)
2656
+ tradingIntervalMs: import_zod.z.number().min(1e3).default(6e4),
2381
2657
  // minimum 1 second
2658
+ maxSlippageBps: import_zod.z.number().min(10).max(1e3).default(100),
2659
+ // 0.1-10%, default 1%
2660
+ minTradeValueUSD: import_zod.z.number().min(0).default(1)
2661
+ // minimum trade value in USD
2382
2662
  });
2383
2663
  var VaultPolicySchema = import_zod.z.enum([
2384
2664
  "disabled",
2385
2665
  // Never create a vault - trade with agent's own capital only
2386
- "manual",
2666
+ "manual"
2387
2667
  // Only create vault when explicitly directed by owner
2388
- "auto_when_qualified"
2389
- // Automatically create vault when requirements are met
2390
2668
  ]);
2391
2669
  var VaultConfigSchema = import_zod.z.object({
2392
2670
  // Policy for vault creation (asked during deployment)
@@ -2414,7 +2692,7 @@ var AgentConfigSchema = import_zod.z.object({
2414
2692
  agentId: import_zod.z.union([import_zod.z.number().positive(), import_zod.z.string()]),
2415
2693
  name: import_zod.z.string().min(3).max(32),
2416
2694
  // Network
2417
- network: import_zod.z.enum(["mainnet", "testnet"]).default("testnet"),
2695
+ network: import_zod.z.literal("mainnet").default("mainnet"),
2418
2696
  // Wallet setup preference
2419
2697
  wallet: WalletConfigSchema,
2420
2698
  // LLM
@@ -2855,7 +3133,7 @@ async function checkFirstRunSetup(configPath) {
2855
3133
  EXAGENT_PRIVATE_KEY=${privateKey}
2856
3134
 
2857
3135
  # Network
2858
- EXAGENT_NETWORK=${config.network || "testnet"}
3136
+ EXAGENT_NETWORK=${config.network || "mainnet"}
2859
3137
 
2860
3138
  # LLM (${llmProvider})
2861
3139
  ${llmEnvVar}EXAGENT_LLM_MODEL=${config.llm?.model || ""}
@@ -2906,7 +3184,7 @@ ${llmEnvVar}EXAGENT_LLM_MODEL=${config.llm?.model || ""}
2906
3184
  console.log("");
2907
3185
  if (walletSetup === "generate") {
2908
3186
  console.log(" NEXT: Fund your wallet before starting to trade.");
2909
- console.log(` Send testnet ETH to: ${walletAddress}`);
3187
+ console.log(` Send ETH to: ${walletAddress}`);
2910
3188
  }
2911
3189
  console.log("");
2912
3190
  console.log(" The agent will now start...");