@exagent/agent 0.1.11 → 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 +538 -242
- package/dist/cli.mjs +1 -1
- 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/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
|
|
34
|
+
var import_sdk2 = require("@exagent/sdk");
|
|
35
35
|
var import_viem3 = require("viem");
|
|
36
36
|
var import_chains2 = require("viem/chains");
|
|
37
37
|
|
|
@@ -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,102 @@ 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
|
|
921
|
+
var TOKEN_DECIMALS = {
|
|
922
|
+
// Base Mainnet — Core tokens
|
|
986
923
|
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
|
|
987
|
-
// USDC
|
|
988
|
-
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6
|
|
989
|
-
// USDbC
|
|
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)
|
|
952
|
+
// Base Sepolia
|
|
953
|
+
"0x036cbd53842c5426634e7929541ec2318f3dcf7e": 6
|
|
954
|
+
// USDC testnet
|
|
990
955
|
};
|
|
956
|
+
function getTokenDecimals(address) {
|
|
957
|
+
const decimals = TOKEN_DECIMALS[address.toLowerCase()];
|
|
958
|
+
if (decimals === void 0) {
|
|
959
|
+
console.warn(`Unknown token decimals for ${address}, defaulting to 18. THIS MAY BE WRONG.`);
|
|
960
|
+
return 18;
|
|
961
|
+
}
|
|
962
|
+
return decimals;
|
|
963
|
+
}
|
|
964
|
+
var TOKEN_TO_COINGECKO = {
|
|
965
|
+
// Core
|
|
966
|
+
"0x4200000000000000000000000000000000000006": "ethereum",
|
|
967
|
+
// WETH
|
|
968
|
+
[NATIVE_ETH.toLowerCase()]: "ethereum",
|
|
969
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "usd-coin",
|
|
970
|
+
// USDC
|
|
971
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": "usd-coin",
|
|
972
|
+
// USDbC
|
|
973
|
+
"0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": "coinbase-wrapped-staked-eth",
|
|
974
|
+
// cbETH
|
|
975
|
+
"0x50c5725949a6f0c72e6c4a641f24049a917db0cb": "dai",
|
|
976
|
+
// DAI
|
|
977
|
+
// Established
|
|
978
|
+
"0x940181a94a35a4569e4529a3cdfb74e38fd98631": "aerodrome-finance",
|
|
979
|
+
// AERO
|
|
980
|
+
"0x532f27101965dd16442e59d40670faf5ebb142e4": "brett",
|
|
981
|
+
// BRETT
|
|
982
|
+
"0x4ed4e862860bed51a9570b96d89af5e1b0efefed": "degen-base",
|
|
983
|
+
// DEGEN
|
|
984
|
+
"0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b": "virtual-protocol",
|
|
985
|
+
// VIRTUAL
|
|
986
|
+
"0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": "toshi",
|
|
987
|
+
// TOSHI
|
|
988
|
+
"0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": "coinbase-wrapped-btc",
|
|
989
|
+
// cbBTC
|
|
990
|
+
"0x2416092f143378750bb29b79ed961ab195cceea5": "renzo-restaked-eth",
|
|
991
|
+
// ezETH
|
|
992
|
+
"0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": "wrapped-steth"
|
|
993
|
+
// wstETH
|
|
994
|
+
};
|
|
995
|
+
var STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai"]);
|
|
996
|
+
var PRICE_STALENESS_MS = 6e4;
|
|
991
997
|
var MarketDataService = class {
|
|
992
998
|
rpcUrl;
|
|
993
999
|
client;
|
|
1000
|
+
/** Cached prices from last fetch */
|
|
1001
|
+
cachedPrices = {};
|
|
1002
|
+
/** Timestamp of last successful price fetch */
|
|
1003
|
+
lastPriceFetchAt = 0;
|
|
994
1004
|
constructor(rpcUrl) {
|
|
995
1005
|
this.rpcUrl = rpcUrl;
|
|
996
1006
|
this.client = (0, import_viem.createPublicClient)({
|
|
997
1007
|
transport: (0, import_viem.http)(rpcUrl)
|
|
998
1008
|
});
|
|
999
1009
|
}
|
|
1010
|
+
/** Cached volume data */
|
|
1011
|
+
cachedVolume24h = {};
|
|
1012
|
+
/** Cached price change data */
|
|
1013
|
+
cachedPriceChange24h = {};
|
|
1000
1014
|
/**
|
|
1001
1015
|
* Fetch current market data for the agent
|
|
1002
1016
|
*/
|
|
@@ -1004,31 +1018,126 @@ var MarketDataService = class {
|
|
|
1004
1018
|
const prices = await this.fetchPrices(tokenAddresses);
|
|
1005
1019
|
const balances = await this.fetchBalances(walletAddress, tokenAddresses);
|
|
1006
1020
|
const portfolioValue = this.calculatePortfolioValue(balances, prices);
|
|
1021
|
+
let gasPrice;
|
|
1022
|
+
try {
|
|
1023
|
+
gasPrice = await this.client.getGasPrice();
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1007
1026
|
return {
|
|
1008
1027
|
timestamp: Date.now(),
|
|
1009
1028
|
prices,
|
|
1010
1029
|
balances,
|
|
1011
|
-
portfolioValue
|
|
1030
|
+
portfolioValue,
|
|
1031
|
+
volume24h: Object.keys(this.cachedVolume24h).length > 0 ? { ...this.cachedVolume24h } : void 0,
|
|
1032
|
+
priceChange24h: Object.keys(this.cachedPriceChange24h).length > 0 ? { ...this.cachedPriceChange24h } : void 0,
|
|
1033
|
+
gasPrice,
|
|
1034
|
+
network: {
|
|
1035
|
+
chainId: this.client.chain?.id ?? 8453
|
|
1036
|
+
}
|
|
1012
1037
|
};
|
|
1013
1038
|
}
|
|
1014
1039
|
/**
|
|
1015
|
-
*
|
|
1040
|
+
* Check if cached prices are still fresh
|
|
1041
|
+
*/
|
|
1042
|
+
get pricesAreFresh() {
|
|
1043
|
+
return Date.now() - this.lastPriceFetchAt < PRICE_STALENESS_MS;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Fetch token prices from CoinGecko free API
|
|
1047
|
+
* Returns cached prices if still fresh (<60s old)
|
|
1016
1048
|
*/
|
|
1017
1049
|
async fetchPrices(tokenAddresses) {
|
|
1050
|
+
if (this.pricesAreFresh && Object.keys(this.cachedPrices).length > 0) {
|
|
1051
|
+
const prices2 = { ...this.cachedPrices };
|
|
1052
|
+
for (const addr of tokenAddresses) {
|
|
1053
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1054
|
+
if (cgId && STABLECOIN_IDS.has(cgId) && !prices2[addr.toLowerCase()]) {
|
|
1055
|
+
prices2[addr.toLowerCase()] = 1;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return prices2;
|
|
1059
|
+
}
|
|
1018
1060
|
const prices = {};
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
[
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1061
|
+
const idsToFetch = /* @__PURE__ */ new Set();
|
|
1062
|
+
for (const addr of tokenAddresses) {
|
|
1063
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1064
|
+
if (cgId && !STABLECOIN_IDS.has(cgId)) {
|
|
1065
|
+
idsToFetch.add(cgId);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
idsToFetch.add("ethereum");
|
|
1069
|
+
if (idsToFetch.size > 0) {
|
|
1070
|
+
try {
|
|
1071
|
+
const ids = Array.from(idsToFetch).join(",");
|
|
1072
|
+
const response = await fetch(
|
|
1073
|
+
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`,
|
|
1074
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
1075
|
+
);
|
|
1076
|
+
if (response.ok) {
|
|
1077
|
+
const data = await response.json();
|
|
1078
|
+
for (const [cgId, priceData] of Object.entries(data)) {
|
|
1079
|
+
for (const [addr, id] of Object.entries(TOKEN_TO_COINGECKO)) {
|
|
1080
|
+
if (id === cgId) {
|
|
1081
|
+
const key = addr.toLowerCase();
|
|
1082
|
+
prices[key] = priceData.usd;
|
|
1083
|
+
if (priceData.usd_24h_vol !== void 0) {
|
|
1084
|
+
this.cachedVolume24h[key] = priceData.usd_24h_vol;
|
|
1085
|
+
}
|
|
1086
|
+
if (priceData.usd_24h_change !== void 0) {
|
|
1087
|
+
this.cachedPriceChange24h[key] = priceData.usd_24h_change;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
this.lastPriceFetchAt = Date.now();
|
|
1093
|
+
} else {
|
|
1094
|
+
console.warn(`CoinGecko API returned ${response.status}, using cached prices`);
|
|
1095
|
+
}
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
console.warn("Failed to fetch prices from CoinGecko:", error instanceof Error ? error.message : error);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
for (const addr of tokenAddresses) {
|
|
1101
|
+
const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
|
|
1102
|
+
if (cgId && STABLECOIN_IDS.has(cgId)) {
|
|
1103
|
+
prices[addr.toLowerCase()] = 1;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
const missingAddrs = tokenAddresses.filter(
|
|
1107
|
+
(addr) => !prices[addr.toLowerCase()] && !STABLECOIN_IDS.has(TOKEN_TO_COINGECKO[addr.toLowerCase()] || "")
|
|
1108
|
+
);
|
|
1109
|
+
if (missingAddrs.length > 0) {
|
|
1110
|
+
try {
|
|
1111
|
+
const coins = missingAddrs.map((a) => `base:${a}`).join(",");
|
|
1112
|
+
const llamaResponse = await fetch(
|
|
1113
|
+
`https://coins.llama.fi/prices/current/${coins}`,
|
|
1114
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
1115
|
+
);
|
|
1116
|
+
if (llamaResponse.ok) {
|
|
1117
|
+
const llamaData = await llamaResponse.json();
|
|
1118
|
+
for (const [key, data] of Object.entries(llamaData.coins)) {
|
|
1119
|
+
const addr = key.replace("base:", "").toLowerCase();
|
|
1120
|
+
if (data.price && data.confidence > 0.5) {
|
|
1121
|
+
prices[addr] = data.price;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (!this.lastPriceFetchAt) this.lastPriceFetchAt = Date.now();
|
|
1125
|
+
}
|
|
1126
|
+
} catch {
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
if (Object.keys(prices).length > 0) {
|
|
1130
|
+
this.cachedPrices = prices;
|
|
1131
|
+
}
|
|
1132
|
+
if (Object.keys(prices).length === 0 && Object.keys(this.cachedPrices).length > 0) {
|
|
1133
|
+
console.warn("Using cached prices (last successful fetch was stale)");
|
|
1134
|
+
return { ...this.cachedPrices };
|
|
1135
|
+
}
|
|
1136
|
+
for (const addr of tokenAddresses) {
|
|
1137
|
+
if (!prices[addr.toLowerCase()]) {
|
|
1138
|
+
console.warn(`No price available for ${addr}, using 0`);
|
|
1139
|
+
prices[addr.toLowerCase()] = 0;
|
|
1140
|
+
}
|
|
1032
1141
|
}
|
|
1033
1142
|
return prices;
|
|
1034
1143
|
}
|
|
@@ -1074,7 +1183,7 @@ var MarketDataService = class {
|
|
|
1074
1183
|
let total = 0;
|
|
1075
1184
|
for (const [address, balance] of Object.entries(balances)) {
|
|
1076
1185
|
const price = prices[address.toLowerCase()] || 0;
|
|
1077
|
-
const decimals =
|
|
1186
|
+
const decimals = getTokenDecimals(address);
|
|
1078
1187
|
const amount = Number(balance) / Math.pow(10, decimals);
|
|
1079
1188
|
total += amount * price;
|
|
1080
1189
|
}
|
|
@@ -1082,6 +1191,129 @@ var MarketDataService = class {
|
|
|
1082
1191
|
}
|
|
1083
1192
|
};
|
|
1084
1193
|
|
|
1194
|
+
// src/trading/risk.ts
|
|
1195
|
+
var RiskManager = class {
|
|
1196
|
+
config;
|
|
1197
|
+
dailyPnL = 0;
|
|
1198
|
+
dailyFees = 0;
|
|
1199
|
+
lastResetDate = "";
|
|
1200
|
+
/** Minimum trade value in USD — trades below this are rejected as dust */
|
|
1201
|
+
minTradeValueUSD;
|
|
1202
|
+
constructor(config) {
|
|
1203
|
+
this.config = config;
|
|
1204
|
+
this.minTradeValueUSD = config.minTradeValueUSD ?? 1;
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Filter signals through risk checks
|
|
1208
|
+
* Returns only signals that pass all guardrails
|
|
1209
|
+
*/
|
|
1210
|
+
filterSignals(signals, marketData) {
|
|
1211
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1212
|
+
if (today !== this.lastResetDate) {
|
|
1213
|
+
this.dailyPnL = 0;
|
|
1214
|
+
this.dailyFees = 0;
|
|
1215
|
+
this.lastResetDate = today;
|
|
1216
|
+
}
|
|
1217
|
+
if (this.isDailyLossLimitHit(marketData.portfolioValue)) {
|
|
1218
|
+
console.warn("Daily loss limit reached - no new trades");
|
|
1219
|
+
return [];
|
|
1220
|
+
}
|
|
1221
|
+
return signals.filter((signal) => this.validateSignal(signal, marketData));
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Validate individual signal against risk limits
|
|
1225
|
+
*/
|
|
1226
|
+
validateSignal(signal, marketData) {
|
|
1227
|
+
if (signal.action === "hold") {
|
|
1228
|
+
return true;
|
|
1229
|
+
}
|
|
1230
|
+
const signalValue = this.estimateSignalValue(signal, marketData);
|
|
1231
|
+
const maxPositionValue = marketData.portfolioValue * this.config.maxPositionSizeBps / 1e4;
|
|
1232
|
+
if (signalValue > maxPositionValue) {
|
|
1233
|
+
console.warn(
|
|
1234
|
+
`Signal exceeds position limit: ${signalValue.toFixed(2)} > ${maxPositionValue.toFixed(2)}`
|
|
1235
|
+
);
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
if (signal.confidence < 0.5) {
|
|
1239
|
+
console.warn(`Signal confidence too low: ${signal.confidence}`);
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
if (signal.action === "buy" && this.config.maxConcurrentPositions) {
|
|
1243
|
+
const activePositions = this.countActivePositions(marketData);
|
|
1244
|
+
if (activePositions >= this.config.maxConcurrentPositions) {
|
|
1245
|
+
console.warn(
|
|
1246
|
+
`Max concurrent positions reached: ${activePositions}/${this.config.maxConcurrentPositions} \u2014 blocking new buy`
|
|
1247
|
+
);
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (signalValue < this.minTradeValueUSD) {
|
|
1252
|
+
console.warn(`Trade value $${signalValue.toFixed(2)} below minimum $${this.minTradeValueUSD} \u2014 skipping`);
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Count non-zero token positions (excluding native ETH and stablecoins used as base currency)
|
|
1259
|
+
*/
|
|
1260
|
+
countActivePositions(marketData) {
|
|
1261
|
+
const NATIVE_ETH_KEY = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
1262
|
+
let count = 0;
|
|
1263
|
+
for (const [address, balance] of Object.entries(marketData.balances)) {
|
|
1264
|
+
if (address.toLowerCase() === NATIVE_ETH_KEY) continue;
|
|
1265
|
+
if (balance > 0n) count++;
|
|
1266
|
+
}
|
|
1267
|
+
return count;
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Check if daily loss limit has been hit
|
|
1271
|
+
*/
|
|
1272
|
+
isDailyLossLimitHit(portfolioValue) {
|
|
1273
|
+
const maxLoss = portfolioValue * this.config.maxDailyLossBps / 1e4;
|
|
1274
|
+
return this.dailyPnL < -maxLoss;
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Estimate USD value of a trade signal
|
|
1278
|
+
*/
|
|
1279
|
+
estimateSignalValue(signal, marketData) {
|
|
1280
|
+
const price = marketData.prices[signal.tokenIn.toLowerCase()] || 0;
|
|
1281
|
+
const tokenDecimals = getTokenDecimals(signal.tokenIn);
|
|
1282
|
+
const amount = Number(signal.amountIn) / Math.pow(10, tokenDecimals);
|
|
1283
|
+
return amount * price;
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Update daily PnL after a trade (market gains/losses only)
|
|
1287
|
+
*/
|
|
1288
|
+
updatePnL(pnl) {
|
|
1289
|
+
this.dailyPnL += pnl;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Update daily fees (trading fees, gas costs, etc.)
|
|
1293
|
+
* Fees are tracked separately and do NOT count toward the daily loss limit.
|
|
1294
|
+
* This prevents protocol fees from triggering circuit breakers.
|
|
1295
|
+
*/
|
|
1296
|
+
updateFees(fees) {
|
|
1297
|
+
this.dailyFees += fees;
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Get current risk status
|
|
1301
|
+
* @param portfolioValue - Current portfolio value in USD (needed for accurate loss limit)
|
|
1302
|
+
*/
|
|
1303
|
+
getStatus(portfolioValue) {
|
|
1304
|
+
const pv = portfolioValue || 0;
|
|
1305
|
+
const maxLossUSD = pv * this.config.maxDailyLossBps / 1e4;
|
|
1306
|
+
return {
|
|
1307
|
+
dailyPnL: this.dailyPnL,
|
|
1308
|
+
dailyFees: this.dailyFees,
|
|
1309
|
+
dailyNetPnL: this.dailyPnL - this.dailyFees,
|
|
1310
|
+
dailyLossLimit: maxLossUSD,
|
|
1311
|
+
// Only market PnL triggers the limit — fees are excluded
|
|
1312
|
+
isLimitHit: pv > 0 ? this.dailyPnL < -maxLossUSD : false
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1085
1317
|
// src/vault/manager.ts
|
|
1086
1318
|
var import_viem2 = require("viem");
|
|
1087
1319
|
var import_accounts = require("viem/accounts");
|
|
@@ -1093,9 +1325,8 @@ var ADDRESSES = {
|
|
|
1093
1325
|
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
1094
1326
|
},
|
|
1095
1327
|
mainnet: {
|
|
1096
|
-
vaultFactory: "0x0000000000000000000000000000000000000000",
|
|
1097
|
-
|
|
1098
|
-
registry: "0x0000000000000000000000000000000000000000",
|
|
1328
|
+
vaultFactory: process.env.EXAGENT_VAULT_FACTORY_ADDRESS || "0x0000000000000000000000000000000000000000",
|
|
1329
|
+
registry: process.env.EXAGENT_REGISTRY_ADDRESS || "0x0000000000000000000000000000000000000000",
|
|
1099
1330
|
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
1100
1331
|
// Base mainnet USDC
|
|
1101
1332
|
}
|
|
@@ -1121,6 +1352,7 @@ var VAULT_FACTORY_ABI = [
|
|
|
1121
1352
|
inputs: [
|
|
1122
1353
|
{ name: "agentId", type: "uint256" },
|
|
1123
1354
|
{ name: "asset", type: "address" },
|
|
1355
|
+
{ name: "seedAmount", type: "uint256" },
|
|
1124
1356
|
{ name: "name", type: "string" },
|
|
1125
1357
|
{ name: "symbol", type: "string" },
|
|
1126
1358
|
{ name: "feeRecipient", type: "address" }
|
|
@@ -1134,13 +1366,6 @@ var VAULT_FACTORY_ABI = [
|
|
|
1134
1366
|
inputs: [],
|
|
1135
1367
|
outputs: [{ type: "uint256" }],
|
|
1136
1368
|
stateMutability: "view"
|
|
1137
|
-
},
|
|
1138
|
-
{
|
|
1139
|
-
type: "function",
|
|
1140
|
-
name: "eXABurnFee",
|
|
1141
|
-
inputs: [],
|
|
1142
|
-
outputs: [{ type: "uint256" }],
|
|
1143
|
-
stateMutability: "view"
|
|
1144
1369
|
}
|
|
1145
1370
|
];
|
|
1146
1371
|
var VAULT_ABI = [
|
|
@@ -1180,6 +1405,7 @@ var VaultManager = class {
|
|
|
1180
1405
|
lastVaultCheck = 0;
|
|
1181
1406
|
VAULT_CACHE_TTL = 6e4;
|
|
1182
1407
|
// 1 minute
|
|
1408
|
+
enabled = true;
|
|
1183
1409
|
constructor(config) {
|
|
1184
1410
|
this.config = config;
|
|
1185
1411
|
this.addresses = ADDRESSES[config.network];
|
|
@@ -1195,6 +1421,10 @@ var VaultManager = class {
|
|
|
1195
1421
|
chain: this.chain,
|
|
1196
1422
|
transport: (0, import_viem2.http)(rpcUrl)
|
|
1197
1423
|
});
|
|
1424
|
+
if (this.addresses.vaultFactory === "0x0000000000000000000000000000000000000000") {
|
|
1425
|
+
console.warn("VaultFactory address is zero \u2014 vault operations will be disabled");
|
|
1426
|
+
this.enabled = false;
|
|
1427
|
+
}
|
|
1198
1428
|
}
|
|
1199
1429
|
/**
|
|
1200
1430
|
* Get the agent's vault policy
|
|
@@ -1212,6 +1442,17 @@ var VaultManager = class {
|
|
|
1212
1442
|
* Get comprehensive vault status
|
|
1213
1443
|
*/
|
|
1214
1444
|
async getVaultStatus() {
|
|
1445
|
+
if (!this.enabled) {
|
|
1446
|
+
return {
|
|
1447
|
+
hasVault: false,
|
|
1448
|
+
vaultAddress: null,
|
|
1449
|
+
totalAssets: BigInt(0),
|
|
1450
|
+
canCreateVault: false,
|
|
1451
|
+
cannotCreateReason: "Vault operations disabled (contract address not set)",
|
|
1452
|
+
requirementsMet: false,
|
|
1453
|
+
requirements: { veXARequired: BigInt(0), isBypassed: false }
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1215
1456
|
const vaultAddress = await this.getVaultAddress();
|
|
1216
1457
|
const hasVault = vaultAddress !== null;
|
|
1217
1458
|
let totalAssets = BigInt(0);
|
|
@@ -1246,22 +1487,16 @@ var VaultManager = class {
|
|
|
1246
1487
|
}
|
|
1247
1488
|
/**
|
|
1248
1489
|
* Get vault creation requirements
|
|
1490
|
+
* Note: No burnFee on mainnet — vault creation requires USDC seed instead
|
|
1249
1491
|
*/
|
|
1250
1492
|
async getRequirements() {
|
|
1251
|
-
const
|
|
1252
|
-
this.
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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 };
|
|
1493
|
+
const veXARequired = await this.publicClient.readContract({
|
|
1494
|
+
address: this.addresses.vaultFactory,
|
|
1495
|
+
abi: VAULT_FACTORY_ABI,
|
|
1496
|
+
functionName: "minimumVeEXARequired"
|
|
1497
|
+
});
|
|
1498
|
+
const isBypassed = veXARequired === BigInt(0);
|
|
1499
|
+
return { veXARequired, isBypassed };
|
|
1265
1500
|
}
|
|
1266
1501
|
/**
|
|
1267
1502
|
* Get the agent's vault address (cached)
|
|
@@ -1285,30 +1520,15 @@ var VaultManager = class {
|
|
|
1285
1520
|
this.cachedVaultAddress = vaultAddress;
|
|
1286
1521
|
return vaultAddress;
|
|
1287
1522
|
}
|
|
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
1523
|
/**
|
|
1308
1524
|
* Create a vault for the agent
|
|
1525
|
+
* @param seedAmount - USDC seed amount in raw units (default: 100e6 = 100 USDC)
|
|
1309
1526
|
* @returns Vault address if successful
|
|
1310
1527
|
*/
|
|
1311
|
-
async createVault() {
|
|
1528
|
+
async createVault(seedAmount) {
|
|
1529
|
+
if (!this.enabled) {
|
|
1530
|
+
return { success: false, error: "Vault operations disabled (contract address not set)" };
|
|
1531
|
+
}
|
|
1312
1532
|
if (this.policy === "disabled") {
|
|
1313
1533
|
return { success: false, error: "Vault creation disabled by policy" };
|
|
1314
1534
|
}
|
|
@@ -1320,6 +1540,7 @@ var VaultManager = class {
|
|
|
1320
1540
|
if (!status.canCreateVault) {
|
|
1321
1541
|
return { success: false, error: status.cannotCreateReason || "Requirements not met" };
|
|
1322
1542
|
}
|
|
1543
|
+
const seed = seedAmount || BigInt(1e8);
|
|
1323
1544
|
const vaultName = this.config.vaultConfig.defaultName || `${this.config.agentName} Trading Vault`;
|
|
1324
1545
|
const vaultSymbol = this.config.vaultConfig.defaultSymbol || `ex${this.config.agentName.replace(/[^a-zA-Z]/g, "").slice(0, 4).toUpperCase()}`;
|
|
1325
1546
|
const feeRecipient = this.config.vaultConfig.feeRecipient || this.account.address;
|
|
@@ -1331,6 +1552,7 @@ var VaultManager = class {
|
|
|
1331
1552
|
args: [
|
|
1332
1553
|
this.config.agentId,
|
|
1333
1554
|
this.addresses.usdc,
|
|
1555
|
+
seed,
|
|
1334
1556
|
vaultName,
|
|
1335
1557
|
vaultSymbol,
|
|
1336
1558
|
feeRecipient
|
|
@@ -1394,37 +1616,12 @@ var VaultManager = class {
|
|
|
1394
1616
|
};
|
|
1395
1617
|
}
|
|
1396
1618
|
}
|
|
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
1619
|
};
|
|
1424
1620
|
|
|
1425
1621
|
// src/relay.ts
|
|
1426
1622
|
var import_ws = __toESM(require("ws"));
|
|
1427
1623
|
var import_accounts2 = require("viem/accounts");
|
|
1624
|
+
var import_sdk = require("@exagent/sdk");
|
|
1428
1625
|
var RelayClient = class {
|
|
1429
1626
|
config;
|
|
1430
1627
|
ws = null;
|
|
@@ -1527,7 +1724,8 @@ var RelayClient = class {
|
|
|
1527
1724
|
agentId: this.config.agentId,
|
|
1528
1725
|
wallet: account.address,
|
|
1529
1726
|
timestamp,
|
|
1530
|
-
signature
|
|
1727
|
+
signature,
|
|
1728
|
+
sdkVersion: import_sdk.SDK_VERSION
|
|
1531
1729
|
});
|
|
1532
1730
|
}
|
|
1533
1731
|
/**
|
|
@@ -1689,6 +1887,7 @@ function openBrowser(url) {
|
|
|
1689
1887
|
|
|
1690
1888
|
// src/runtime.ts
|
|
1691
1889
|
var FUNDS_LOW_THRESHOLD = 5e-3;
|
|
1890
|
+
var FUNDS_CRITICAL_THRESHOLD = 1e-3;
|
|
1692
1891
|
var AgentRuntime = class {
|
|
1693
1892
|
config;
|
|
1694
1893
|
client;
|
|
@@ -1702,14 +1901,13 @@ var AgentRuntime = class {
|
|
|
1702
1901
|
isRunning = false;
|
|
1703
1902
|
mode = "idle";
|
|
1704
1903
|
configHash;
|
|
1705
|
-
lastVaultCheck = 0;
|
|
1706
1904
|
cycleCount = 0;
|
|
1707
1905
|
lastCycleAt = 0;
|
|
1708
1906
|
lastPortfolioValue = 0;
|
|
1709
1907
|
lastEthBalance = "0";
|
|
1710
1908
|
processAlive = true;
|
|
1711
|
-
|
|
1712
|
-
|
|
1909
|
+
riskUniverse = 0;
|
|
1910
|
+
allowedTokens = /* @__PURE__ */ new Set();
|
|
1713
1911
|
constructor(config) {
|
|
1714
1912
|
this.config = config;
|
|
1715
1913
|
}
|
|
@@ -1718,7 +1916,7 @@ var AgentRuntime = class {
|
|
|
1718
1916
|
*/
|
|
1719
1917
|
async initialize() {
|
|
1720
1918
|
console.log(`Initializing agent: ${this.config.name} (ID: ${this.config.agentId})`);
|
|
1721
|
-
this.client = new
|
|
1919
|
+
this.client = new import_sdk2.ExagentClient({
|
|
1722
1920
|
privateKey: this.config.privateKey,
|
|
1723
1921
|
network: this.config.network
|
|
1724
1922
|
});
|
|
@@ -1729,6 +1927,7 @@ var AgentRuntime = class {
|
|
|
1729
1927
|
}
|
|
1730
1928
|
console.log(`Agent verified: ${agent.name}`);
|
|
1731
1929
|
await this.ensureWalletLinked();
|
|
1930
|
+
await this.loadTradingRestrictions();
|
|
1732
1931
|
console.log(`Initializing LLM: ${this.config.llm.provider}`);
|
|
1733
1932
|
this.llm = await createLLMAdapter(this.config.llm);
|
|
1734
1933
|
const llmMeta = this.llm.getMetadata();
|
|
@@ -1796,12 +1995,8 @@ var AgentRuntime = class {
|
|
|
1796
1995
|
console.log(`Vault TVL: ${Number(status.totalAssets) / 1e6} USDC`);
|
|
1797
1996
|
} else {
|
|
1798
1997
|
console.log("No vault exists for this agent");
|
|
1799
|
-
if (vaultConfig.policy === "
|
|
1800
|
-
|
|
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
|
-
}
|
|
1998
|
+
if (vaultConfig.policy === "manual") {
|
|
1999
|
+
console.log("Vault creation is manual \u2014 use the command center to create one");
|
|
1805
2000
|
}
|
|
1806
2001
|
}
|
|
1807
2002
|
}
|
|
@@ -1820,7 +2015,7 @@ var AgentRuntime = class {
|
|
|
1820
2015
|
if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
|
|
1821
2016
|
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
1822
2017
|
const nonce = await this.client.registry.getNonce(address);
|
|
1823
|
-
const linkMessage =
|
|
2018
|
+
const linkMessage = import_sdk2.ExagentRegistry.generateLinkMessage(
|
|
1824
2019
|
address,
|
|
1825
2020
|
agentId,
|
|
1826
2021
|
nonce
|
|
@@ -1860,6 +2055,49 @@ var AgentRuntime = class {
|
|
|
1860
2055
|
console.log("Wallet already linked");
|
|
1861
2056
|
}
|
|
1862
2057
|
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Load risk universe and allowed tokens from on-chain registry.
|
|
2060
|
+
* This prevents the agent from wasting gas on trades that will revert.
|
|
2061
|
+
*/
|
|
2062
|
+
async loadTradingRestrictions() {
|
|
2063
|
+
const agentId = BigInt(this.config.agentId);
|
|
2064
|
+
const RISK_UNIVERSE_NAMES = ["Core", "Established", "Derivatives", "Emerging", "Frontier"];
|
|
2065
|
+
try {
|
|
2066
|
+
this.riskUniverse = await this.client.registry.getRiskUniverse(agentId);
|
|
2067
|
+
console.log(`Risk universe: ${RISK_UNIVERSE_NAMES[this.riskUniverse] || this.riskUniverse}`);
|
|
2068
|
+
const configTokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
2069
|
+
const verified = [];
|
|
2070
|
+
for (const token of configTokens) {
|
|
2071
|
+
try {
|
|
2072
|
+
const allowed = await this.client.registry.isTradeAllowed(
|
|
2073
|
+
agentId,
|
|
2074
|
+
token,
|
|
2075
|
+
"0x0000000000000000000000000000000000000000"
|
|
2076
|
+
// zero = check token only
|
|
2077
|
+
);
|
|
2078
|
+
if (allowed) {
|
|
2079
|
+
this.allowedTokens.add(token.toLowerCase());
|
|
2080
|
+
verified.push(token);
|
|
2081
|
+
} else {
|
|
2082
|
+
console.warn(`Token ${token} not allowed for this agent's risk universe \u2014 excluded`);
|
|
2083
|
+
}
|
|
2084
|
+
} catch {
|
|
2085
|
+
this.allowedTokens.add(token.toLowerCase());
|
|
2086
|
+
verified.push(token);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
this.config.allowedTokens = verified;
|
|
2090
|
+
console.log(`Allowed tokens loaded: ${verified.length} tokens verified`);
|
|
2091
|
+
if (this.riskUniverse === 4) {
|
|
2092
|
+
console.warn("Frontier risk universe: vault creation is disabled");
|
|
2093
|
+
}
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
console.warn(
|
|
2096
|
+
"Could not load trading restrictions from registry (using defaults):",
|
|
2097
|
+
error instanceof Error ? error.message : error
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
1863
2101
|
/**
|
|
1864
2102
|
* Sync the LLM config hash to chain for epoch tracking.
|
|
1865
2103
|
* If the wallet has insufficient gas, enters a recovery loop
|
|
@@ -1868,7 +2106,7 @@ var AgentRuntime = class {
|
|
|
1868
2106
|
async syncConfigHash() {
|
|
1869
2107
|
const agentId = BigInt(this.config.agentId);
|
|
1870
2108
|
const llmMeta = this.llm.getMetadata();
|
|
1871
|
-
this.configHash =
|
|
2109
|
+
this.configHash = import_sdk2.ExagentRegistry.calculateConfigHash(llmMeta.provider, llmMeta.model);
|
|
1872
2110
|
console.log(`Config hash: ${this.configHash}`);
|
|
1873
2111
|
const onChainHash = await this.client.registry.getConfigHash(agentId);
|
|
1874
2112
|
if (onChainHash !== this.configHash) {
|
|
@@ -2032,21 +2270,38 @@ var AgentRuntime = class {
|
|
|
2032
2270
|
break;
|
|
2033
2271
|
case "update_risk_params": {
|
|
2034
2272
|
const params = cmd.params || {};
|
|
2273
|
+
let updated = false;
|
|
2035
2274
|
if (params.maxPositionSizeBps !== void 0) {
|
|
2036
|
-
|
|
2275
|
+
const value = Number(params.maxPositionSizeBps);
|
|
2276
|
+
if (isNaN(value) || value < 100 || value > 1e4) {
|
|
2277
|
+
this.relay?.sendCommandResult(cmd.id, false, "maxPositionSizeBps must be 100-10000");
|
|
2278
|
+
break;
|
|
2279
|
+
}
|
|
2280
|
+
this.config.trading.maxPositionSizeBps = value;
|
|
2281
|
+
updated = true;
|
|
2037
2282
|
}
|
|
2038
2283
|
if (params.maxDailyLossBps !== void 0) {
|
|
2039
|
-
|
|
2284
|
+
const value = Number(params.maxDailyLossBps);
|
|
2285
|
+
if (isNaN(value) || value < 50 || value > 5e3) {
|
|
2286
|
+
this.relay?.sendCommandResult(cmd.id, false, "maxDailyLossBps must be 50-5000");
|
|
2287
|
+
break;
|
|
2288
|
+
}
|
|
2289
|
+
this.config.trading.maxDailyLossBps = value;
|
|
2290
|
+
updated = true;
|
|
2291
|
+
}
|
|
2292
|
+
if (updated) {
|
|
2293
|
+
this.riskManager = new RiskManager(this.config.trading);
|
|
2294
|
+
console.log("Risk params updated via command center");
|
|
2295
|
+
this.relay?.sendCommandResult(cmd.id, true, "Risk parameters updated");
|
|
2296
|
+
this.relay?.sendMessage(
|
|
2297
|
+
"config_updated",
|
|
2298
|
+
"info",
|
|
2299
|
+
"Risk Parameters Updated",
|
|
2300
|
+
`Max position: ${this.config.trading.maxPositionSizeBps / 100}%, Max daily loss: ${this.config.trading.maxDailyLossBps / 100}%`
|
|
2301
|
+
);
|
|
2302
|
+
} else {
|
|
2303
|
+
this.relay?.sendCommandResult(cmd.id, false, "No valid parameters provided");
|
|
2040
2304
|
}
|
|
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
2305
|
break;
|
|
2051
2306
|
}
|
|
2052
2307
|
case "update_trading_interval": {
|
|
@@ -2123,7 +2378,7 @@ var AgentRuntime = class {
|
|
|
2123
2378
|
provider: this.config.llm.provider,
|
|
2124
2379
|
model: this.config.llm.model || "default"
|
|
2125
2380
|
},
|
|
2126
|
-
risk: this.riskManager?.getStatus() || {
|
|
2381
|
+
risk: this.riskManager?.getStatus(this.lastPortfolioValue) || {
|
|
2127
2382
|
dailyPnL: 0,
|
|
2128
2383
|
dailyLossLimit: 0,
|
|
2129
2384
|
isLimitHit: false
|
|
@@ -2144,14 +2399,18 @@ var AgentRuntime = class {
|
|
|
2144
2399
|
--- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
|
|
2145
2400
|
this.cycleCount++;
|
|
2146
2401
|
this.lastCycleAt = Date.now();
|
|
2147
|
-
await this.checkVaultAutoCreation();
|
|
2148
2402
|
const tokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
2149
2403
|
const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
|
|
2150
2404
|
console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
|
|
2151
2405
|
this.lastPortfolioValue = marketData.portfolioValue;
|
|
2152
2406
|
const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
2153
2407
|
this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
|
|
2154
|
-
this.checkFundsLow(marketData);
|
|
2408
|
+
const fundsOk = this.checkFundsLow(marketData);
|
|
2409
|
+
if (!fundsOk) {
|
|
2410
|
+
console.warn("Skipping trading cycle \u2014 ETH balance critically low");
|
|
2411
|
+
this.sendRelayStatus();
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2155
2414
|
let signals;
|
|
2156
2415
|
try {
|
|
2157
2416
|
signals = await this.strategy(marketData, this.llm, this.config);
|
|
@@ -2169,19 +2428,31 @@ var AgentRuntime = class {
|
|
|
2169
2428
|
console.log(`Strategy generated ${signals.length} signals`);
|
|
2170
2429
|
const filteredSignals = this.riskManager.filterSignals(signals, marketData);
|
|
2171
2430
|
console.log(`${filteredSignals.length} signals passed risk checks`);
|
|
2172
|
-
if (this.riskManager.getStatus().isLimitHit) {
|
|
2431
|
+
if (this.riskManager.getStatus(marketData.portfolioValue).isLimitHit) {
|
|
2173
2432
|
this.relay?.sendMessage(
|
|
2174
2433
|
"risk_limit_hit",
|
|
2175
2434
|
"warning",
|
|
2176
2435
|
"Risk Limit Hit",
|
|
2177
|
-
`Daily loss limit reached:
|
|
2436
|
+
`Daily loss limit reached: $${this.riskManager.getStatus(marketData.portfolioValue).dailyPnL.toFixed(2)}`
|
|
2178
2437
|
);
|
|
2179
2438
|
}
|
|
2180
2439
|
if (filteredSignals.length > 0) {
|
|
2440
|
+
const vaultStatus = await this.vaultManager?.getVaultStatus();
|
|
2441
|
+
if (vaultStatus?.hasVault && this.vaultManager?.preferVaultTrading) {
|
|
2442
|
+
console.log(`Trading through vault: ${vaultStatus.vaultAddress}`);
|
|
2443
|
+
}
|
|
2444
|
+
const preTradePortfolioValue = marketData.portfolioValue;
|
|
2181
2445
|
const results = await this.executor.executeAll(filteredSignals);
|
|
2446
|
+
let totalFeesUSD = 0;
|
|
2182
2447
|
for (const result of results) {
|
|
2183
2448
|
if (result.success) {
|
|
2184
2449
|
console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
|
|
2450
|
+
const feeCostBps = 20;
|
|
2451
|
+
const signalPrice = marketData.prices[result.signal.tokenIn.toLowerCase()] || 0;
|
|
2452
|
+
const amountUSD = Number(result.signal.amountIn) / Math.pow(10, getTokenDecimals(result.signal.tokenIn)) * signalPrice;
|
|
2453
|
+
const feeCostUSD = amountUSD * feeCostBps / 1e4;
|
|
2454
|
+
totalFeesUSD += feeCostUSD;
|
|
2455
|
+
this.riskManager.updateFees(feeCostUSD);
|
|
2185
2456
|
this.relay?.sendMessage(
|
|
2186
2457
|
"trade_executed",
|
|
2187
2458
|
"success",
|
|
@@ -2205,18 +2476,43 @@ var AgentRuntime = class {
|
|
|
2205
2476
|
);
|
|
2206
2477
|
}
|
|
2207
2478
|
}
|
|
2479
|
+
const postTokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
2480
|
+
const postTradeData = await this.marketData.fetchMarketData(this.client.address, postTokens);
|
|
2481
|
+
const marketPnL = postTradeData.portfolioValue - preTradePortfolioValue + totalFeesUSD;
|
|
2482
|
+
this.riskManager.updatePnL(marketPnL);
|
|
2483
|
+
if (marketPnL !== 0) {
|
|
2484
|
+
console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
|
|
2485
|
+
}
|
|
2486
|
+
this.lastPortfolioValue = postTradeData.portfolioValue;
|
|
2487
|
+
const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
2488
|
+
this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
|
|
2208
2489
|
}
|
|
2209
2490
|
this.sendRelayStatus();
|
|
2210
2491
|
}
|
|
2211
2492
|
/**
|
|
2212
|
-
* Check if ETH balance is below threshold and notify
|
|
2493
|
+
* Check if ETH balance is below threshold and notify.
|
|
2494
|
+
* Returns true if trading should continue, false if ETH is critically low.
|
|
2213
2495
|
*/
|
|
2214
2496
|
checkFundsLow(marketData) {
|
|
2215
|
-
if (!this.relay) return;
|
|
2216
2497
|
const ethBalance = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
|
|
2217
2498
|
const ethAmount = Number(ethBalance) / 1e18;
|
|
2499
|
+
if (ethAmount < FUNDS_CRITICAL_THRESHOLD) {
|
|
2500
|
+
console.error(`ETH balance critically low: ${ethAmount.toFixed(6)} ETH \u2014 halting trading`);
|
|
2501
|
+
this.relay?.sendMessage(
|
|
2502
|
+
"funds_low",
|
|
2503
|
+
"error",
|
|
2504
|
+
"Funds Critical",
|
|
2505
|
+
`ETH balance is ${ethAmount.toFixed(6)} ETH (below ${FUNDS_CRITICAL_THRESHOLD} ETH minimum). Trading halted \u2014 fund your wallet.`,
|
|
2506
|
+
{
|
|
2507
|
+
ethBalance: ethAmount.toFixed(6),
|
|
2508
|
+
wallet: this.client.address,
|
|
2509
|
+
threshold: FUNDS_CRITICAL_THRESHOLD
|
|
2510
|
+
}
|
|
2511
|
+
);
|
|
2512
|
+
return false;
|
|
2513
|
+
}
|
|
2218
2514
|
if (ethAmount < FUNDS_LOW_THRESHOLD) {
|
|
2219
|
-
this.relay
|
|
2515
|
+
this.relay?.sendMessage(
|
|
2220
2516
|
"funds_low",
|
|
2221
2517
|
"warning",
|
|
2222
2518
|
"Low Funds",
|
|
@@ -2228,36 +2524,7 @@ var AgentRuntime = class {
|
|
|
2228
2524
|
}
|
|
2229
2525
|
);
|
|
2230
2526
|
}
|
|
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
|
-
}
|
|
2527
|
+
return true;
|
|
2261
2528
|
}
|
|
2262
2529
|
/**
|
|
2263
2530
|
* Stop the agent process completely
|
|
@@ -2281,15 +2548,42 @@ var AgentRuntime = class {
|
|
|
2281
2548
|
return "https://sepolia.base.org";
|
|
2282
2549
|
}
|
|
2283
2550
|
/**
|
|
2284
|
-
* Default tokens to track
|
|
2551
|
+
* Default tokens to track.
|
|
2552
|
+
* These are validated against the on-chain registry's isTradeAllowed() during init —
|
|
2553
|
+
* agents in restricted risk universes will have ineligible tokens filtered out.
|
|
2285
2554
|
*/
|
|
2286
2555
|
getDefaultTokens() {
|
|
2287
2556
|
if (this.config.network === "mainnet") {
|
|
2288
2557
|
return [
|
|
2558
|
+
// Core
|
|
2289
2559
|
"0x4200000000000000000000000000000000000006",
|
|
2290
2560
|
// WETH
|
|
2291
|
-
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
2561
|
+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
2292
2562
|
// USDC
|
|
2563
|
+
"0x2Ae3F1Ec7F1F5012CFEab0185bFC7aa3cf0DEC22",
|
|
2564
|
+
// cbETH
|
|
2565
|
+
"0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA",
|
|
2566
|
+
// USDbC
|
|
2567
|
+
"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
|
|
2568
|
+
// DAI
|
|
2569
|
+
// Established
|
|
2570
|
+
"0x940181a94A35A4569E4529A3CDfB74e38FD98631",
|
|
2571
|
+
// AERO
|
|
2572
|
+
"0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
|
|
2573
|
+
// cbBTC
|
|
2574
|
+
"0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
|
|
2575
|
+
// wstETH
|
|
2576
|
+
"0x2416092f143378750bb29b79eD961ab195CcEea5",
|
|
2577
|
+
// ezETH
|
|
2578
|
+
// Emerging (filtered by risk universe at init)
|
|
2579
|
+
"0x532f27101965dd16442E59d40670FaF5eBB142E4",
|
|
2580
|
+
// BRETT
|
|
2581
|
+
"0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
|
|
2582
|
+
// DEGEN
|
|
2583
|
+
"0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b",
|
|
2584
|
+
// VIRTUAL
|
|
2585
|
+
"0xAC1Bd2486Aaf3B5C0fc3Fd868558b082a531B2B4"
|
|
2586
|
+
// TOSHI
|
|
2293
2587
|
];
|
|
2294
2588
|
}
|
|
2295
2589
|
return [
|
|
@@ -2315,7 +2609,7 @@ var AgentRuntime = class {
|
|
|
2315
2609
|
model: this.config.llm.model || "default"
|
|
2316
2610
|
},
|
|
2317
2611
|
configHash: this.configHash || "not initialized",
|
|
2318
|
-
risk: this.riskManager?.getStatus() || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false },
|
|
2612
|
+
risk: this.riskManager?.getStatus(this.lastPortfolioValue) || { dailyPnL: 0, dailyLossLimit: 0, isLimitHit: false },
|
|
2319
2613
|
vault: {
|
|
2320
2614
|
policy: vaultConfig.policy,
|
|
2321
2615
|
hasVault: false,
|
|
@@ -2377,16 +2671,18 @@ var TradingConfigSchema = import_zod.z.object({
|
|
|
2377
2671
|
maxDailyLossBps: import_zod.z.number().min(0).max(1e4).default(500),
|
|
2378
2672
|
// 0-100%
|
|
2379
2673
|
maxConcurrentPositions: import_zod.z.number().min(1).max(100).default(5),
|
|
2380
|
-
tradingIntervalMs: import_zod.z.number().min(1e3).default(6e4)
|
|
2674
|
+
tradingIntervalMs: import_zod.z.number().min(1e3).default(6e4),
|
|
2381
2675
|
// minimum 1 second
|
|
2676
|
+
maxSlippageBps: import_zod.z.number().min(10).max(1e3).default(100),
|
|
2677
|
+
// 0.1-10%, default 1%
|
|
2678
|
+
minTradeValueUSD: import_zod.z.number().min(0).default(1)
|
|
2679
|
+
// minimum trade value in USD
|
|
2382
2680
|
});
|
|
2383
2681
|
var VaultPolicySchema = import_zod.z.enum([
|
|
2384
2682
|
"disabled",
|
|
2385
2683
|
// Never create a vault - trade with agent's own capital only
|
|
2386
|
-
"manual"
|
|
2684
|
+
"manual"
|
|
2387
2685
|
// Only create vault when explicitly directed by owner
|
|
2388
|
-
"auto_when_qualified"
|
|
2389
|
-
// Automatically create vault when requirements are met
|
|
2390
2686
|
]);
|
|
2391
2687
|
var VaultConfigSchema = import_zod.z.object({
|
|
2392
2688
|
// Policy for vault creation (asked during deployment)
|