@exagent/agent 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5NE2K3WU.mjs +2852 -0
- package/dist/chunk-BZLZQCKQ.mjs +2853 -0
- package/dist/chunk-DZMU25QF.mjs +3021 -0
- package/dist/chunk-TBITVAFT.mjs +2780 -0
- package/dist/chunk-ZY3ZSYNN.mjs +3023 -0
- package/dist/cli.js +548 -243
- package/dist/cli.mjs +11 -2
- package/dist/index.d.mts +95 -43
- package/dist/index.d.ts +95 -43
- package/dist/index.js +541 -243
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
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
|
|
72
|
+
var import_sdk2 = require("@exagent/sdk");
|
|
73
73
|
var import_viem3 = require("viem");
|
|
74
74
|
var import_chains2 = require("viem/chains");
|
|
75
75
|
|
|
@@ -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,102 @@ 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
|
|
965
|
+
var TOKEN_DECIMALS = {
|
|
966
|
+
// Base Mainnet — Core tokens
|
|
1030
967
|
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
|
|
1031
|
-
// USDC
|
|
1032
|
-
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6
|
|
1033
|
-
// USDbC
|
|
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)
|
|
996
|
+
// Base Sepolia
|
|
997
|
+
"0x036cbd53842c5426634e7929541ec2318f3dcf7e": 6
|
|
998
|
+
// USDC testnet
|
|
1034
999
|
};
|
|
1000
|
+
function getTokenDecimals(address) {
|
|
1001
|
+
const decimals = TOKEN_DECIMALS[address.toLowerCase()];
|
|
1002
|
+
if (decimals === void 0) {
|
|
1003
|
+
console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
|
|
1004
|
+
return 18;
|
|
1005
|
+
}
|
|
1006
|
+
return decimals;
|
|
1007
|
+
}
|
|
1008
|
+
var TOKEN_TO_COINGECKO = {
|
|
1009
|
+
// Core
|
|
1010
|
+
"0x4200000000000000000000000000000000000006": "ethereum",
|
|
1011
|
+
// WETH
|
|
1012
|
+
[NATIVE_ETH.toLowerCase()]: "ethereum",
|
|
1013
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
|
|
1014
|
+
// USDC
|
|
1015
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
|
|
1016
|
+
// USDbC
|
|
1017
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
|
|
1018
|
+
// cbETH
|
|
1019
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
|
|
1020
|
+
// DAI
|
|
1021
|
+
// Established
|
|
1022
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
|
|
1023
|
+
// AERO
|
|
1024
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
|
|
1025
|
+
// BRETT
|
|
1026
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
|
|
1027
|
+
// DEGEN
|
|
1028
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
|
|
1029
|
+
// VIRTUAL
|
|
1030
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
|
|
1031
|
+
// TOSHI
|
|
1032
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
|
|
1033
|
+
// cbBTC
|
|
1034
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
|
|
1035
|
+
// ezETH
|
|
1036
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
|
|
1037
|
+
// wstETH
|
|
1038
|
+
};
|
|
1039
|
+
var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
|
|
1040
|
+
var PRICE_STALENESS_MS = 6e4;
|
|
1035
1041
|
var MarketDataService = class {
|
|
1036
1042
|
rpcUrl;
|
|
1037
1043
|
client;
|
|
1044
|
+
/** Cached prices from last fetch */
|
|
1045
|
+
cachedPrices = {};
|
|
1046
|
+
/** Timestamp of last successful price fetch */
|
|
1047
|
+
lastPriceFetchAt = 0;
|
|
1038
1048
|
constructor(rpcUrl) {
|
|
1039
1049
|
this.rpcUrl = rpcUrl;
|
|
1040
1050
|
this.client = (0, import_viem.createPublicClient)({
|
|
1041
1051
|
transport: (0, import_viem.http)(rpcUrl)
|
|
1042
1052
|
});
|
|
1043
1053
|
}
|
|
1054
|
+
/** Cached volume data */
|
|
1055
|
+
cachedVolume24h = {};
|
|
1056
|
+
/** Cached price change data */
|
|
1057
|
+
cachedPriceChange24h = {};
|
|
1044
1058
|
/**
|
|
1045
1059
|
* Fetch current market data for the agent
|
|
1046
1060
|
*/
|
|
@@ -1048,31 +1062,126 @@ var MarketDataService = class {
|
|
|
1048
1062
|
const prices = await this.fetchPrices(tokenAddresses);
|
|
1049
1063
|
const balances = await this.fetchBalances(walletAddress, tokenAddresses);
|
|
1050
1064
|
const portfolioValue = this.calculatePortfolioValue(balances, prices);
|
|
1065
|
+
let gasPrice;
|
|
1066
|
+
try {
|
|
1067
|
+
gasPrice = await this.client.getGasPrice();
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1051
1070
|
return {
|
|
1052
1071
|
timestamp: Date.now(),
|
|
1053
1072
|
prices,
|
|
1054
1073
|
balances,
|
|
1055
|
-
portfolioValue
|
|
1074
|
+
portfolioValue,
|
|
1075
|
+
volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
|
|
1076
|
+
priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
|
|
1077
|
+
gasPrice,
|
|
1078
|
+
network: {
|
|
1079
|
+
chainId: this.client.chain?.id ?? 8453
|
|
1080
|
+
}
|
|
1056
1081
|
};
|
|
1057
1082
|
}
|
|
1058
1083
|
/**
|
|
1059
|
-
*
|
|
1084
|
+
* Check if cached prices are still fresh
|
|
1085
|
+
*/
|
|
1086
|
+
get pricesAreFresh() {
|
|
1087
|
+
return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Fetch token prices from CoinGecko free API
|
|
1091
|
+
* Returns cached prices if still fresh (<60s old)
|
|
1060
1092
|
*/
|
|
1061
1093
|
async fetchPrices(tokenAddresses) {
|
|
1094
|
+
if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
|
|
1095
|
+
const prices2 = { ...this.cachedPrices };
|
|
1096
|
+
for (const addr of tokenAddresses) {
|
|
1097
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1098
|
+
if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
|
|
1099
|
+
prices2[addr.toLowerCase()] = 1;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return prices2;
|
|
1103
|
+
}
|
|
1062
1104
|
const prices = {};
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
[
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1105
|
+
const idsToFetch = /* @__PURE__ */ new Set();
|
|
1106
|
+
for (const addr of tokenAddresses) {
|
|
1107
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1108
|
+
if (cgId && !STABLECOIN_IDS.has(cgId)) {
|
|
1109
|
+
idsToFetch.add(cgId);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
idsToFetch.add("ethereum");
|
|
1113
|
+
if (idsToFetch.size > 0) {
|
|
1114
|
+
try {
|
|
1115
|
+
const ids = Array.from(idsToFetch).join(",");
|
|
1116
|
+
const response = await fetch(
|
|
1117
|
+
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
|
|
1118
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
1119
|
+
);
|
|
1120
|
+
if (response.ok) {
|
|
1121
|
+
const data = await response.json();
|
|
1122
|
+
for (const [cgId, priceData] of Object.entries(data)) {
|
|
1123
|
+
for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
|
|
1124
|
+
if (id === cgId) {
|
|
1125
|
+
const key = addr.toLowerCase();
|
|
1126
|
+
prices[key] = priceData.usd;
|
|
1127
|
+
if (priceData.usd_24h_vol !== void 0) {
|
|
1128
|
+
this.cachedVolume24h[key] = priceData.usd_24h_vol;
|
|
1129
|
+
}
|
|
1130
|
+
if (priceData.usd_24h_change !== void 0) {
|
|
1131
|
+
this.cachedPriceChange24h[key] = priceData.usd_24h_change;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
this.lastPriceFetchAt = Date.now();
|
|
1137
|
+
} else {
|
|
1138
|
+
console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
|
|
1139
|
+
}
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
for (const addr of tokenAddresses) {
|
|
1145
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1146
|
+
if (cgId && STABLECOIN_IDS.has(cgId)) {
|
|
1147
|
+
prices[addr.toLowerCase()] = 1;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const missingAddrs = tokenAddresses.filter(
|
|
1151
|
+
(addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
|
|
1152
|
+
);
|
|
1153
|
+
if (missingAddrs.length > 0) {
|
|
1154
|
+
try {
|
|
1155
|
+
const coins = missingAddrs.map((a) => `base:${a}`).join(",");
|
|
1156
|
+
const llamaResponse = await fetch(
|
|
1157
|
+
`https://coins.llama.fi/prices/current/${coins}`,
|
|
1158
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
1159
|
+
);
|
|
1160
|
+
if (llamaResponse.ok) {
|
|
1161
|
+
const llamaData = await llamaResponse.json();
|
|
1162
|
+
for (const [key, data] of Object.entries(llamaData.coins)) {
|
|
1163
|
+
const addr = key.replace("base:", "").toLowerCase();
|
|
1164
|
+
if (data.price && data.confidence > 0.5) {
|
|
1165
|
+
prices[addr] = data.price;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
|
|
1169
|
+
}
|
|
1170
|
+
} catch {
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (Object.keys(prices).length > 0) {
|
|
1174
|
+
this.cachedPrices = prices;
|
|
1175
|
+
}
|
|
1176
|
+
if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
|
|
1177
|
+
console.warn("Using cached prices (last successful fetch was stale)");
|
|
1178
|
+
return { ...this.cachedPrices };
|
|
1179
|
+
}
|
|
1180
|
+
for (const addr of tokenAddresses) {
|
|
1181
|
+
if (!prices[addr.toLowerCase()]) {
|
|
1182
|
+
console.warn(`No price available for ${addr}, using 0`);
|
|
1183
|
+
prices[addr.toLowerCase()] = 0;
|
|
1184
|
+
}
|
|
1076
1185
|
}
|
|
1077
1186
|
return prices;
|
|
1078
1187
|
}
|
|
@@ -1118,7 +1227,7 @@ var MarketDataService = class {
|
|
|
1118
1227
|
let total = 0;
|
|
1119
1228
|
for (const [address, balance] of Object.entries(balances)) {
|
|
1120
1229
|
const price = prices[address.toLowerCase()] || 0;
|
|
1121
|
-
const decimals =
|
|
1230
|
+
const decimals = getTokenDecimals(address);
|
|
1122
1231
|
const amount = Number(balance) / Math.pow(10, decimals);
|
|
1123
1232
|
total += amount * price;
|
|
1124
1233
|
}
|
|
@@ -1126,6 +1235,129 @@ var MarketDataService = class {
|
|
|
1126
1235
|
}
|
|
1127
1236
|
};
|
|
1128
1237
|
|
|
1238
|
+
// src/trading/risk.ts
|
|
1239
|
+
var RiskManager = class {
|
|
1240
|
+
config;
|
|
1241
|
+
dailyPnL = 0;
|
|
1242
|
+
dailyFees = 0;
|
|
1243
|
+
lastResetDate = "";
|
|
1244
|
+
/** Minimum trade value in USD — trades below this are rejected as dust */
|
|
1245
|
+
minTradeValueUSD;
|
|
1246
|
+
constructor(config) {
|
|
1247
|
+
this.config = config;
|
|
1248
|
+
this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Filter signals through risk checks
|
|
1252
|
+
* Returns only signals that pass all guardrails
|
|
1253
|
+
*/
|
|
1254
|
+
filterSignals(signals, marketData) {
|
|
1255
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1256
|
+
if (today !== this.lastResetDate) {
|
|
1257
|
+
this.dailyPnL = 0;
|
|
1258
|
+
this.dailyFees = 0;
|
|
1259
|
+
this.lastResetDate = today;
|
|
1260
|
+
}
|
|
1261
|
+
if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
|
|
1262
|
+
console.warn("Daily loss limit reached - no new trades");
|
|
1263
|
+
return [];
|
|
1264
|
+
}
|
|
1265
|
+
return signals.filter((signal) => this.validateSignal(signal, marketData));
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Validate individual signal against risk limits
|
|
1269
|
+
*/
|
|
1270
|
+
validateSignal(signal, marketData) {
|
|
1271
|
+
if (signal.action === "hold") {
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
const signalValue = this.estimateSignalValue(signal, marketData);
|
|
1275
|
+
const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
|
|
1276
|
+
if (signalValue > maxPositionValue) {
|
|
1277
|
+
console.warn(
|
|
1278
|
+
`Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
|
|
1279
|
+
);
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
if (signal.confidence < 0.5) {
|
|
1283
|
+
console.warn(`Signal confidence too low: ${signal.confidence}`);
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
if (signal.action === "buy" && this.config.maxConcurrentPositions) {
|
|
1287
|
+
const activePositions = this.countActivePositions(marketData);
|
|
1288
|
+
if (activePositions >= this.config.maxConcurrentPositions) {
|
|
1289
|
+
console.warn(
|
|
1290
|
+
`Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
|
|
1291
|
+
);
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (signalValue < this.minTradeValueUSD) {
|
|
1296
|
+
console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
return true;
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
|
|
1303
|
+
*/
|
|
1304
|
+
countActivePositions(marketData) {
|
|
1305
|
+
const NATIVE_ETH_KEY = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
1306
|
+
let count = 0;
|
|
1307
|
+
for (const [address, balance] of Object.entries(marketData.balances)) {
|
|
1308
|
+
if (address.toLowerCase() === NATIVE_ETH_KEY) continue;
|
|
1309
|
+
if (balance > 0n) count++;
|
|
1310
|
+
}
|
|
1311
|
+
return count;
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Check if daily loss limit has been hit
|
|
1315
|
+
*/
|
|
1316
|
+
isDailyLossLimitHit(portfolioValue) {
|
|
1317
|
+
const maxLoss = portfolioValue * this.config.maxDailyLossBps / 1e4;
|
|
1318
|
+
return this.dailyPnL < -maxLoss;
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Estimate USD value of a trade signal
|
|
1322
|
+
*/
|
|
1323
|
+
estimateSignalValue(signal, marketData) {
|
|
1324
|
+
const price = marketData.prices[signal.tokenIn.toLowerCase()] || 0;
|
|
1325
|
+
const tokenDecimals = getTokenDecimals(signal.tokenIn);
|
|
1326
|
+
const amount = Number(signal.amountIn) / Math.pow(10, tokenDecimals);
|
|
1327
|
+
return amount * price;
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Update daily PnL after a trade (market gains/losses only)
|
|
1331
|
+
*/
|
|
1332
|
+
updatePnL(pnl) {
|
|
1333
|
+
this.dailyPnL += pnl;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Update daily fees (trading fees, gas costs, etc.)
|
|
1337
|
+
* Fees are tracked separately and do NOT count toward the daily loss limit.
|
|
1338
|
+
* This prevents protocol fees from triggering circuit breakers.
|
|
1339
|
+
*/
|
|
1340
|
+
updateFees(fees) {
|
|
1341
|
+
this.dailyFees += fees;
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Get current risk status
|
|
1345
|
+
* @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
|
|
1346
|
+
*/
|
|
1347
|
+
getStatus(portfolioValue) {
|
|
1348
|
+
const pv = portfolioValue || 0;
|
|
1349
|
+
const maxLossUSD = pv * this.config.maxDailyLossBps / 1e4;
|
|
1350
|
+
return {
|
|
1351
|
+
dailyPnL: this.dailyPnL,
|
|
1352
|
+
dailyFees: this.dailyFees,
|
|
1353
|
+
dailyNetPnL: this.dailyPnL - this.dailyFees,
|
|
1354
|
+
dailyLossLimit: maxLossUSD,
|
|
1355
|
+
// Only market PnL triggers the limit — fees are excluded
|
|
1356
|
+
isLimitHit: pv > 0 ? this.dailyPnL < -maxLossUSD : false
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1129
1361
|
// src/vault/manager.ts
|
|
1130
1362
|
var import_viem2 = require("viem");
|
|
1131
1363
|
var import_accounts = require("viem/accounts");
|
|
@@ -1137,9 +1369,8 @@ var ADDRESSES = {
|
|
|
1137
1369
|
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
1138
1370
|
},
|
|
1139
1371
|
mainnet: {
|
|
1140
|
-
vaultFactory: "0x0000000000000000000000000000000000000000",
|
|
1141
|
-
|
|
1142
|
-
registry: "0x0000000000000000000000000000000000000000",
|
|
1372
|
+
vaultFactory: process.env.EXAGENT_VAULT_FACTORY_ADDRESS || "0x0000000000000000000000000000000000000000",
|
|
1373
|
+
registry: process.env.EXAGENT_REGISTRY_ADDRESS || "0x0000000000000000000000000000000000000000",
|
|
1143
1374
|
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
1144
1375
|
// Base mainnet USDC
|
|
1145
1376
|
}
|
|
@@ -1165,6 +1396,7 @@ var VAULT_FACTORY_ABI = [
|
|
|
1165
1396
|
inputs: [
|
|
1166
1397
|
{ name: "agentId", type: "uint256" },
|
|
1167
1398
|
{ name: "asset", type: "address" },
|
|
1399
|
+
{ name: "seedAmount", type: "uint256" },
|
|
1168
1400
|
{ name: "name", type: "string" },
|
|
1169
1401
|
{ name: "symbol", type: "string" },
|
|
1170
1402
|
{ name: "feeRecipient", type: "address" }
|
|
@@ -1178,13 +1410,6 @@ var VAULT_FACTORY_ABI = [
|
|
|
1178
1410
|
inputs: [],
|
|
1179
1411
|
outputs: [{ type: "uint256" }],
|
|
1180
1412
|
stateMutability: "view"
|
|
1181
|
-
},
|
|
1182
|
-
{
|
|
1183
|
-
type: "function",
|
|
1184
|
-
name: "eXABurnFee",
|
|
1185
|
-
inputs: [],
|
|
1186
|
-
outputs: [{ type: "uint256" }],
|
|
1187
|
-
stateMutability: "view"
|
|
1188
1413
|
}
|
|
1189
1414
|
];
|
|
1190
1415
|
var VAULT_ABI = [
|
|
@@ -1224,6 +1449,7 @@ var VaultManager = class {
|
|
|
1224
1449
|
lastVaultCheck = 0;
|
|
1225
1450
|
VAULT_CACHE_TTL = 6e4;
|
|
1226
1451
|
// 1 minute
|
|
1452
|
+
enabled = true;
|
|
1227
1453
|
constructor(config) {
|
|
1228
1454
|
this.config = config;
|
|
1229
1455
|
this.addresses = ADDRESSES[config.network];
|
|
@@ -1239,6 +1465,10 @@ var VaultManager = class {
|
|
|
1239
1465
|
chain: this.chain,
|
|
1240
1466
|
transport: (0, import_viem2.http)(rpcUrl)
|
|
1241
1467
|
});
|
|
1468
|
+
if (this.addresses.vaultFactory === "0x0000000000000000000000000000000000000000") {
|
|
1469
|
+
console.warn("VaultFactory address is zero \u2014 vault operations will be disabled");
|
|
1470
|
+
this.enabled = false;
|
|
1471
|
+
}
|
|
1242
1472
|
}
|
|
1243
1473
|
/**
|
|
1244
1474
|
* Get the agent's vault policy
|
|
@@ -1256,6 +1486,17 @@ var VaultManager = class {
|
|
|
1256
1486
|
* Get comprehensive vault status
|
|
1257
1487
|
*/
|
|
1258
1488
|
async getVaultStatus() {
|
|
1489
|
+
if (!this.enabled) {
|
|
1490
|
+
return {
|
|
1491
|
+
hasVault: false,
|
|
1492
|
+
vaultAddress: null,
|
|
1493
|
+
totalAssets: BigInt(0),
|
|
1494
|
+
canCreateVault: false,
|
|
1495
|
+
cannotCreateReason: "Vault operations disabled (contract address not set)",
|
|
1496
|
+
requirementsMet: false,
|
|
1497
|
+
requirements: { veXARequired: BigInt(0), isBypassed: false }
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1259
1500
|
const vaultAddress = await this.getVaultAddress();
|
|
1260
1501
|
const hasVault = vaultAddress !== null;
|
|
1261
1502
|
let totalAssets = BigInt(0);
|
|
@@ -1290,22 +1531,16 @@ var VaultManager = class {
|
|
|
1290
1531
|
}
|
|
1291
1532
|
/**
|
|
1292
1533
|
* Get vault creation requirements
|
|
1534
|
+
* Note: No burnFee on mainnet — vault creation requires USDC seed instead
|
|
1293
1535
|
*/
|
|
1294
1536
|
async getRequirements() {
|
|
1295
|
-
const
|
|
1296
|
-
this.
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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 };
|
|
1537
|
+
const veXARequired = await this.publicClient.readContract({
|
|
1538
|
+
address: this.addresses.vaultFactory,
|
|
1539
|
+
abi: VAULT_FACTORY_ABI,
|
|
1540
|
+
functionName: "minimumVeEXARequired"
|
|
1541
|
+
});
|
|
1542
|
+
const isBypassed = veXARequired === BigInt(0);
|
|
1543
|
+
return { veXARequired, isBypassed };
|
|
1309
1544
|
}
|
|
1310
1545
|
/**
|
|
1311
1546
|
* Get the agent's vault address (cached)
|
|
@@ -1329,30 +1564,15 @@ var VaultManager = class {
|
|
|
1329
1564
|
this.cachedVaultAddress = vaultAddress;
|
|
1330
1565
|
return vaultAddress;
|
|
1331
1566
|
}
|
|
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
1567
|
/**
|
|
1352
1568
|
* Create a vault for the agent
|
|
1569
|
+
* @param seedAmount - USDC seed amount in raw units (default: 100e6 = 100 USDC)
|
|
1353
1570
|
* @returns Vault address if successful
|
|
1354
1571
|
*/
|
|
1355
|
-
async createVault() {
|
|
1572
|
+
async createVault(seedAmount) {
|
|
1573
|
+
if (!this.enabled) {
|
|
1574
|
+
return { success: false, error: "Vault operations disabled (contract address not set)" };
|
|
1575
|
+
}
|
|
1356
1576
|
if (this.policy === "disabled") {
|
|
1357
1577
|
return { success: false, error: "Vault creation disabled by policy" };
|
|
1358
1578
|
}
|
|
@@ -1364,6 +1584,7 @@ var VaultManager = class {
|
|
|
1364
1584
|
if (!status.canCreateVault) {
|
|
1365
1585
|
return { success: false, error: status.cannotCreateReason || "Requirements not met" };
|
|
1366
1586
|
}
|
|
1587
|
+
const seed = seedAmount || BigInt(1e8);
|
|
1367
1588
|
const vaultName = this.config.vaultConfig.defaultName || `${this.config.agentName} Trading Vault`;
|
|
1368
1589
|
const vaultSymbol = this.config.vaultConfig.defaultSymbol || `ex${this.config.agentName.replace(/[^a-zA-Z]/g, "").slice(0, 4).toUpperCase()}`;
|
|
1369
1590
|
const feeRecipient = this.config.vaultConfig.feeRecipient || this.account.address;
|
|
@@ -1375,6 +1596,7 @@ var VaultManager = class {
|
|
|
1375
1596
|
args: [
|
|
1376
1597
|
this.config.agentId,
|
|
1377
1598
|
this.addresses.usdc,
|
|
1599
|
+
seed,
|
|
1378
1600
|
vaultName,
|
|
1379
1601
|
vaultSymbol,
|
|
1380
1602
|
feeRecipient
|
|
@@ -1438,37 +1660,12 @@ var VaultManager = class {
|
|
|
1438
1660
|
};
|
|
1439
1661
|
}
|
|
1440
1662
|
}
|
|
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
1663
|
};
|
|
1468
1664
|
|
|
1469
1665
|
// src/relay.ts
|
|
1470
1666
|
var import_ws = __toESM(require("ws"));
|
|
1471
1667
|
var import_accounts2 = require("viem/accounts");
|
|
1668
|
+
var import_sdk = require("@exagent/sdk");
|
|
1472
1669
|
var RelayClient = class {
|
|
1473
1670
|
config;
|
|
1474
1671
|
ws = null;
|
|
@@ -1571,7 +1768,8 @@ var RelayClient = class {
|
|
|
1571
1768
|
agentId: this.config.agentId,
|
|
1572
1769
|
wallet: account.address,
|
|
1573
1770
|
timestamp,
|
|
1574
|
-
signature
|
|
1771
|
+
signature,
|
|
1772
|
+
sdkVersion: import_sdk.SDK_VERSION
|
|
1575
1773
|
});
|
|
1576
1774
|
}
|
|
1577
1775
|
/**
|
|
@@ -1733,6 +1931,7 @@ function openBrowser(url) {
|
|
|
1733
1931
|
|
|
1734
1932
|
// src/runtime.ts
|
|
1735
1933
|
var FUNDS_LOW_THRESHOLD = 5e-3;
|
|
1934
|
+
var FUNDS_CRITICAL_THRESHOLD = 1e-3;
|
|
1736
1935
|
var AgentRuntime = class {
|
|
1737
1936
|
config;
|
|
1738
1937
|
client;
|
|
@@ -1746,14 +1945,13 @@ var AgentRuntime = class {
|
|
|
1746
1945
|
isRunning = false;
|
|
1747
1946
|
mode = "idle";
|
|
1748
1947
|
configHash;
|
|
1749
|
-
lastVaultCheck = 0;
|
|
1750
1948
|
cycleCount = 0;
|
|
1751
1949
|
lastCycleAt = 0;
|
|
1752
1950
|
lastPortfolioValue = 0;
|
|
1753
1951
|
lastEthBalance = "0";
|
|
1754
1952
|
processAlive = true;
|
|
1755
|
-
|
|
1756
|
-
|
|
1953
|
+
riskUniverse = 0;
|
|
1954
|
+
allowedTokens = /* @__PURE__ */ new Set();
|
|
1757
1955
|
constructor(config) {
|
|
1758
1956
|
this.config = config;
|
|
1759
1957
|
}
|
|
@@ -1762,7 +1960,7 @@ var AgentRuntime = class {
|
|
|
1762
1960
|
*/
|
|
1763
1961
|
async initialize() {
|
|
1764
1962
|
console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
|
|
1765
|
-
this.client = new
|
|
1963
|
+
this.client = new import_sdk2.ExagentClient({
|
|
1766
1964
|
privateKey: this.config.privateKey,
|
|
1767
1965
|
network: this.config.network
|
|
1768
1966
|
});
|
|
@@ -1773,6 +1971,7 @@ var AgentRuntime = class {
|
|
|
1773
1971
|
}
|
|
1774
1972
|
console.log(`Agent verified: ${agent.name}`);
|
|
1775
1973
|
await this.ensureWalletLinked();
|
|
1974
|
+
await this.loadTradingRestrictions();
|
|
1776
1975
|
console.log(`Initializing LLM: ${this.config.llm.provider}`);
|
|
1777
1976
|
this.llm = await createLLMAdapter(this.config.llm);
|
|
1778
1977
|
const llmMeta = this.llm.getMetadata();
|
|
@@ -1840,12 +2039,8 @@ var AgentRuntime = class {
|
|
|
1840
2039
|
console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
|
|
1841
2040
|
} else {
|
|
1842
2041
|
console.log("No vault exists for this agent");
|
|
1843
|
-
if (vaultConfig.policy === "
|
|
1844
|
-
|
|
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
|
-
}
|
|
2042
|
+
if (vaultConfig.policy === "manual") {
|
|
2043
|
+
console.log("Vault creation is manual \u2014 use the command center to create one");
|
|
1849
2044
|
}
|
|
1850
2045
|
}
|
|
1851
2046
|
}
|
|
@@ -1864,7 +2059,7 @@ var AgentRuntime = class {
|
|
|
1864
2059
|
if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
|
|
1865
2060
|
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
1866
2061
|
const nonce = await this.client.registry.getNonce(address);
|
|
1867
|
-
const linkMessage =
|
|
2062
|
+
const linkMessage = import_sdk2.ExagentRegistry.generateLinkMessage(
|
|
1868
2063
|
address,
|
|
1869
2064
|
agentId,
|
|
1870
2065
|
nonce
|
|
@@ -1904,6 +2099,49 @@ var AgentRuntime = class {
|
|
|
1904
2099
|
console.log("Wallet already linked");
|
|
1905
2100
|
}
|
|
1906
2101
|
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Load risk universe and allowed tokens from on-chain registry.
|
|
2104
|
+
* This prevents the agent from wasting gas on trades that will revert.
|
|
2105
|
+
*/
|
|
2106
|
+
async loadTradingRestrictions() {
|
|
2107
|
+
const agentId = BigInt(this.config.agentId);
|
|
2108
|
+
const RISK_UNIVERSE_NAMES = ["Core", "Established", "Derivatives", "Emerging", "Frontier"];
|
|
2109
|
+
try {
|
|
2110
|
+
this.riskUniverse = await this.client.registry.getRiskUniverse(agentId);
|
|
2111
|
+
console.log(`Risk universe: ${RISK_UNIVERSE_NAMES[this.riskUniverse] || this.riskUniverse}`);
|
|
2112
|
+
const configTokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
2113
|
+
const verified = [];
|
|
2114
|
+
for (const token of configTokens) {
|
|
2115
|
+
try {
|
|
2116
|
+
const allowed = await this.client.registry.isTradeAllowed(
|
|
2117
|
+
agentId,
|
|
2118
|
+
token,
|
|
2119
|
+
"0x0000000000000000000000000000000000000000"
|
|
2120
|
+
// zero = check token only
|
|
2121
|
+
);
|
|
2122
|
+
if (allowed) {
|
|
2123
|
+
this.allowedTokens.add(token.toLowerCase());
|
|
2124
|
+
verified.push(token);
|
|
2125
|
+
} else {
|
|
2126
|
+
console.warn(`Token ${token} not allowed for this agent's risk universe \u2014 excluded`);
|
|
2127
|
+
}
|
|
2128
|
+
} catch {
|
|
2129
|
+
this.allowedTokens.add(token.toLowerCase());
|
|
2130
|
+
verified.push(token);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
this.config.allowedTokens = verified;
|
|
2134
|
+
console.log(`Allowed tokens loaded: ${verified.length} tokens verified`);
|
|
2135
|
+
if (this.riskUniverse === 4) {
|
|
2136
|
+
console.warn("Frontier risk universe: vault creation is disabled");
|
|
2137
|
+
}
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
console.warn(
|
|
2140
|
+
"Could not load trading restrictions from registry (using defaults):",
|
|
2141
|
+
error instanceof Error ? error.message : error
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
1907
2145
|
/**
|
|
1908
2146
|
* Sync the LLM config hash to chain for epoch tracking.
|
|
1909
2147
|
* If the wallet has insufficient gas, enters a recovery loop
|
|
@@ -1912,7 +2150,7 @@ var AgentRuntime = class {
|
|
|
1912
2150
|
async syncConfigHash() {
|
|
1913
2151
|
const agentId = BigInt(this.config.agentId);
|
|
1914
2152
|
const llmMeta = this.llm.getMetadata();
|
|
1915
|
-
this.configHash =
|
|
2153
|
+
this.configHash = import_sdk2.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
|
|
1916
2154
|
console.log(`Config hash: ${this.configHash}`);
|
|
1917
2155
|
const onChainHash = await this.client.registry.getConfigHash(agentId);
|
|
1918
2156
|
if (onChainHash !== this.configHash) {
|
|
@@ -2076,21 +2314,38 @@ var AgentRuntime = class {
|
|
|
2076
2314
|
break;
|
|
2077
2315
|
case "update_risk_params": {
|
|
2078
2316
|
const params = cmd.params || {};
|
|
2317
|
+
let updated = false;
|
|
2079
2318
|
if (params.maxPositionSizeBps !== void 0) {
|
|
2080
|
-
|
|
2319
|
+
const value = Number(params.maxPositionSizeBps);
|
|
2320
|
+
if (isNaN(value) || value < 100 || value > 1e4) {
|
|
2321
|
+
this.relay?.sendCommandResult(cmd.id, false, "maxPositionSizeBps must be 100-10000");
|
|
2322
|
+
break;
|
|
2323
|
+
}
|
|
2324
|
+
this.config.trading.maxPositionSizeBps = value;
|
|
2325
|
+
updated = true;
|
|
2081
2326
|
}
|
|
2082
2327
|
if (params.maxDailyLossBps !== void 0) {
|
|
2083
|
-
|
|
2328
|
+
const value = Number(params.maxDailyLossBps);
|
|
2329
|
+
if (isNaN(value) || value < 50 || value > 5e3) {
|
|
2330
|
+
this.relay?.sendCommandResult(cmd.id, false, "maxDailyLossBps must be 50-5000");
|
|
2331
|
+
break;
|
|
2332
|
+
}
|
|
2333
|
+
this.config.trading.maxDailyLossBps = value;
|
|
2334
|
+
updated = true;
|
|
2335
|
+
}
|
|
2336
|
+
if (updated) {
|
|
2337
|
+
this.riskManager = new RiskManager(this.config.trading);
|
|
2338
|
+
console.log("Risk params updated via command center");
|
|
2339
|
+
this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
|
|
2340
|
+
this.relay?.sendMessage(
|
|
2341
|
+
"config_updated",
|
|
2342
|
+
"info",
|
|
2343
|
+
"Risk Parameters Updated",
|
|
2344
|
+
`Max position: ${this.config.trading.maxPositionSizeBps / 100}%, Max daily loss: ${this.config.trading.maxDailyLossBps / 100}%`
|
|
2345
|
+
);
|
|
2346
|
+
} else {
|
|
2347
|
+
this.relay?.sendCommandResult(cmd.id, false, "No valid parameters provided");
|
|
2084
2348
|
}
|
|
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
2349
|
break;
|
|
2095
2350
|
}
|
|
2096
2351
|
case "update_trading_interval": {
|
|
@@ -2167,7 +2422,7 @@ var AgentRuntime = class {
|
|
|
2167
2422
|
provider: this.config.llm.provider,
|
|
2168
2423
|
model: this.config.llm.model || "default"
|
|
2169
2424
|
},
|
|
2170
|
-
risk: this.riskManager?.getStatus() || {
|
|
2425
|
+
risk: this.riskManager?.getStatus(this.lastPortfolioValue) || {
|
|
2171
2426
|
dailyPnL: 0,
|
|
2172
2427
|
dailyLossLimit: 0,
|
|
2173
2428
|
isLimitHit: false
|
|
@@ -2188,14 +2443,18 @@ var AgentRuntime = class {
|
|
|
2188
2443
|
--- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
|
|
2189
2444
|
this.cycleCount++;
|
|
2190
2445
|
this.lastCycleAt = Date.now();
|
|
2191
|
-
await this.checkVaultAutoCreation();
|
|
2192
2446
|
const tokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
2193
2447
|
const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
|
|
2194
2448
|
console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
|
|
2195
2449
|
this.lastPortfolioValue = marketData.portfolioValue;
|
|
2196
2450
|
const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
2197
2451
|
this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
|
|
2198
|
-
this.checkFundsLow(marketData);
|
|
2452
|
+
const fundsOk = this.checkFundsLow(marketData);
|
|
2453
|
+
if (!fundsOk) {
|
|
2454
|
+
console.warn("Skipping trading cycle \u2014 ETH balance critically low");
|
|
2455
|
+
this.sendRelayStatus();
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2199
2458
|
let signals;
|
|
2200
2459
|
try {
|
|
2201
2460
|
signals = await this.strategy(marketData, this.llm, this.config);
|
|
@@ -2213,19 +2472,31 @@ var AgentRuntime = class {
|
|
|
2213
2472
|
console.log(`Strategy generated ${signals.length} signals`);
|
|
2214
2473
|
const filteredSignals = this.riskManager.filterSignals(signals, marketData);
|
|
2215
2474
|
console.log(`${filteredSignals.length} signals passed risk checks`);
|
|
2216
|
-
if (this.riskManager.getStatus().isLimitHit) {
|
|
2475
|
+
if (this.riskManager.getStatus(marketData.portfolioValue).isLimitHit) {
|
|
2217
2476
|
this.relay?.sendMessage(
|
|
2218
2477
|
"risk_limit_hit",
|
|
2219
2478
|
"warning",
|
|
2220
2479
|
"Risk Limit Hit",
|
|
2221
|
-
`Daily loss limit reached:
|
|
2480
|
+
`Daily loss limit reached: $${this.riskManager.getStatus(marketData.portfolioValue).dailyPnL.toFixed(2)}`
|
|
2222
2481
|
);
|
|
2223
2482
|
}
|
|
2224
2483
|
if (filteredSignals.length > 0) {
|
|
2484
|
+
const vaultStatus = await this.vaultManager?.getVaultStatus();
|
|
2485
|
+
if (vaultStatus?.hasVault && this.vaultManager?.preferVaultTrading) {
|
|
2486
|
+
console.log(`Trading through vault: ${vaultStatus.vaultAddress}`);
|
|
2487
|
+
}
|
|
2488
|
+
const preTradePortfolioValue = marketData.portfolioValue;
|
|
2225
2489
|
const results = await this.executor.executeAll(filteredSignals);
|
|
2490
|
+
let totalFeesUSD = 0;
|
|
2226
2491
|
for (const result of results) {
|
|
2227
2492
|
if (result.success) {
|
|
2228
2493
|
console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
|
|
2494
|
+
const feeCostBps = 20;
|
|
2495
|
+
const signalPrice = marketData.prices[result.signal.tokenIn.toLowerCase()] || 0;
|
|
2496
|
+
const amountUSD = Number(result.signal.amountIn) / Math.pow(10, getTokenDecimals(result.signal.tokenIn)) * signalPrice;
|
|
2497
|
+
const feeCostUSD = amountUSD * feeCostBps / 1e4;
|
|
2498
|
+
totalFeesUSD += feeCostUSD;
|
|
2499
|
+
this.riskManager.updateFees(feeCostUSD);
|
|
2229
2500
|
this.relay?.sendMessage(
|
|
2230
2501
|
"trade_executed",
|
|
2231
2502
|
"success",
|
|
@@ -2249,18 +2520,43 @@ var AgentRuntime = class {
|
|
|
2249
2520
|
);
|
|
2250
2521
|
}
|
|
2251
2522
|
}
|
|
2523
|
+
const postTokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
2524
|
+
const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
|
|
2525
|
+
const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
|
|
2526
|
+
this.riskManager.updatePnL(marketPnL);
|
|
2527
|
+
if (marketPnL !== 0) {
|
|
2528
|
+
console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
|
|
2529
|
+
}
|
|
2530
|
+
this.lastPortfolioValue = postTradeData.portfolioValue;
|
|
2531
|
+
const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
2532
|
+
this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
|
|
2252
2533
|
}
|
|
2253
2534
|
this.sendRelayStatus();
|
|
2254
2535
|
}
|
|
2255
2536
|
/**
|
|
2256
|
-
* Check if ETH balance is below threshold and notify
|
|
2537
|
+
* Check if ETH balance is below threshold and notify.
|
|
2538
|
+
* Returns true if trading should continue, false if ETH is critically low.
|
|
2257
2539
|
*/
|
|
2258
2540
|
checkFundsLow(marketData) {
|
|
2259
|
-
if (!this.relay) return;
|
|
2260
2541
|
const ethBalance = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
2261
2542
|
const ethAmount = Number(ethBalance) / 1e18;
|
|
2543
|
+
if (ethAmount < FUNDS_CRITICAL_THRESHOLD) {
|
|
2544
|
+
console.error(`ETH balance critically low: ${ethAmount.toFixed(6)} ETH \u2014 halting trading`);
|
|
2545
|
+
this.relay?.sendMessage(
|
|
2546
|
+
"funds_low",
|
|
2547
|
+
"error",
|
|
2548
|
+
"Funds Critical",
|
|
2549
|
+
`ETH balance is ${ethAmount.toFixed(6)} ETH (below ${FUNDS_CRITICAL_THRESHOLD} ETH minimum). Trading halted \u2014 fund your wallet.`,
|
|
2550
|
+
{
|
|
2551
|
+
ethBalance: ethAmount.toFixed(6),
|
|
2552
|
+
wallet: this.client.address,
|
|
2553
|
+
threshold: FUNDS_CRITICAL_THRESHOLD
|
|
2554
|
+
}
|
|
2555
|
+
);
|
|
2556
|
+
return false;
|
|
2557
|
+
}
|
|
2262
2558
|
if (ethAmount < FUNDS_LOW_THRESHOLD) {
|
|
2263
|
-
this.relay
|
|
2559
|
+
this.relay?.sendMessage(
|
|
2264
2560
|
"funds_low",
|
|
2265
2561
|
"warning",
|
|
2266
2562
|
"Low Funds",
|
|
@@ -2272,36 +2568,7 @@ var AgentRuntime = class {
|
|
|
2272
2568
|
}
|
|
2273
2569
|
);
|
|
2274
2570
|
}
|
|
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
|
-
}
|
|
2571
|
+
return true;
|
|
2305
2572
|
}
|
|
2306
2573
|
/**
|
|
2307
2574
|
* Stop the agent process completely
|
|
@@ -2325,15 +2592,42 @@ var AgentRuntime = class {
|
|
|
2325
2592
|
return "https://sepolia.base.org";
|
|
2326
2593
|
}
|
|
2327
2594
|
/**
|
|
2328
|
-
* Default tokens to track
|
|
2595
|
+
* Default tokens to track.
|
|
2596
|
+
* These are validated against the on-chain registry's isTradeAllowed() during init —
|
|
2597
|
+
* agents in restricted risk universes will have ineligible tokens filtered out.
|
|
2329
2598
|
*/
|
|
2330
2599
|
getDefaultTokens() {
|
|
2331
2600
|
if (this.config.network === "mainnet") {
|
|
2332
2601
|
return [
|
|
2602
|
+
// Core
|
|
2333
2603
|
"0x4200000000000000000000000000000000000006",
|
|
2334
2604
|
// WETH
|
|
2335
|
-
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
2605
|
+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
2336
2606
|
// USDC
|
|
2607
|
+
"0x2Ae3F1Ec7F1F5012CFEab0185bFC7aa3cf0DEC22",
|
|
2608
|
+
// cbETH
|
|
2609
|
+
"0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA",
|
|
2610
|
+
// USDbC
|
|
2611
|
+
"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
|
|
2612
|
+
// DAI
|
|
2613
|
+
// Established
|
|
2614
|
+
"0x940181a94A35A4569E4529A3CDfB74e38FD98631",
|
|
2615
|
+
// AERO
|
|
2616
|
+
"0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
|
|
2617
|
+
// cbBTC
|
|
2618
|
+
"0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
|
|
2619
|
+
// wstETH
|
|
2620
|
+
"0x2416092f143378750bb29b79eD961ab195CcEea5",
|
|
2621
|
+
// ezETH
|
|
2622
|
+
// Emerging (filtered by risk universe at init)
|
|
2623
|
+
"0x532f27101965dd16442E59d40670FaF5eBB142E4",
|
|
2624
|
+
// BRETT
|
|
2625
|
+
"0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
|
|
2626
|
+
// DEGEN
|
|
2627
|
+
"0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b",
|
|
2628
|
+
// VIRTUAL
|
|
2629
|
+
"0xAC1Bd2486Aaf3B5C0fc3Fd868558b082a531B2B4"
|
|
2630
|
+
// TOSHI
|
|
2337
2631
|
];
|
|
2338
2632
|
}
|
|
2339
2633
|
return [
|
|
@@ -2359,7 +2653,7 @@ var AgentRuntime = class {
|
|
|
2359
2653
|
model: this.config.llm.model || "default"
|
|
2360
2654
|
},
|
|
2361
2655
|
configHash: this.configHash || "not initialized",
|
|
2362
|
-
risk: this.riskManager?.getStatus() || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false },
|
|
2656
|
+
risk: this.riskManager?.getStatus(this.lastPortfolioValue) || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false },
|
|
2363
2657
|
vault: {
|
|
2364
2658
|
policy: vaultConfig.policy,
|
|
2365
2659
|
hasVault: false,
|
|
@@ -2421,16 +2715,18 @@ var TradingConfigSchema = import_zod.z.object({
|
|
|
2421
2715
|
maxDailyLossBps: import_zod.z.number().min(0).max(1e4).default(500),
|
|
2422
2716
|
// 0-100%
|
|
2423
2717
|
maxConcurrentPositions: import_zod.z.number().min(1).max(100).default(5),
|
|
2424
|
-
tradingIntervalMs: import_zod.z.number().min(1e3).default(6e4)
|
|
2718
|
+
tradingIntervalMs: import_zod.z.number().min(1e3).default(6e4),
|
|
2425
2719
|
// minimum 1 second
|
|
2720
|
+
maxSlippageBps: import_zod.z.number().min(10).max(1e3).default(100),
|
|
2721
|
+
// 0.1-10%, default 1%
|
|
2722
|
+
minTradeValueUSD: import_zod.z.number().min(0).default(1)
|
|
2723
|
+
// minimum trade value in USD
|
|
2426
2724
|
});
|
|
2427
2725
|
var VaultPolicySchema = import_zod.z.enum([
|
|
2428
2726
|
"disabled",
|
|
2429
2727
|
// Never create a vault - trade with agent's own capital only
|
|
2430
|
-
"manual"
|
|
2728
|
+
"manual"
|
|
2431
2729
|
// Only create vault when explicitly directed by owner
|
|
2432
|
-
"auto_when_qualified"
|
|
2433
|
-
// Automatically create vault when requirements are met
|
|
2434
2730
|
]);
|
|
2435
2731
|
var VaultConfigSchema = import_zod.z.object({
|
|
2436
2732
|
// Policy for vault creation (asked during deployment)
|
|
@@ -2558,7 +2854,9 @@ function createSampleConfig(agentId, name) {
|
|
|
2558
2854
|
maxPositionSizeBps: 1e3,
|
|
2559
2855
|
maxDailyLossBps: 500,
|
|
2560
2856
|
maxConcurrentPositions: 5,
|
|
2561
|
-
tradingIntervalMs: 6e4
|
|
2857
|
+
tradingIntervalMs: 6e4,
|
|
2858
|
+
maxSlippageBps: 100,
|
|
2859
|
+
minTradeValueUSD: 1
|
|
2562
2860
|
},
|
|
2563
2861
|
vault: {
|
|
2564
2862
|
// Default to manual - user must explicitly enable auto-creation
|