@exagent/agent 0.1.47 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -332,7 +332,7 @@ function getTokenDecimals(address) {
332
332
  function getTokenSymbol(address) {
333
333
  return _globalResolver?.getSymbol(address.toLowerCase());
334
334
  }
335
- var import_viem2, import_chains, NATIVE_ETH, _globalResolver, TOKEN_DECIMALS, TOKEN_TO_COINGECKO, STABLECOIN_IDS, PRICE_STALENESS_MS, MarketDataService;
335
+ var import_viem2, import_chains, NATIVE_ETH, _globalResolver, TOKEN_DECIMALS, TOKEN_TO_COINGECKO, STABLECOIN_IDS, PRICE_STALENESS_MS, HISTORY_STALENESS_MS, MarketDataService;
336
336
  var init_market = __esm({
337
337
  "src/trading/market.ts"() {
338
338
  "use strict";
@@ -498,6 +498,7 @@ var init_market = __esm({
498
498
  };
499
499
  STABLECOIN_IDS = /* @__PURE__ */ new Set(["usd-coin", "dai", "tether"]);
500
500
  PRICE_STALENESS_MS = 6e4;
501
+ HISTORY_STALENESS_MS = 60 * 6e4;
501
502
  MarketDataService = class {
502
503
  rpcUrl;
503
504
  client;
@@ -510,6 +511,10 @@ var init_market = __esm({
510
511
  cachedVolume24h = {};
511
512
  /** Cached price change data */
512
513
  cachedPriceChange24h = {};
514
+ /** Cached 24h hourly price history per token address */
515
+ cachedPriceHistory = {};
516
+ /** Timestamp of last successful price history fetch */
517
+ lastHistoryFetchAt = 0;
513
518
  constructor(rpcUrl, store) {
514
519
  this.rpcUrl = rpcUrl;
515
520
  this.client = (0, import_viem2.createPublicClient)({
@@ -530,11 +535,10 @@ var init_market = __esm({
530
535
  const prices = await this.fetchPrices(tokenAddresses);
531
536
  const balances = await this.fetchBalances(walletAddress, tokenAddresses);
532
537
  const portfolioValue = this.calculatePortfolioValue(balances, prices);
533
- let gasPrice;
534
- try {
535
- gasPrice = await this.client.getGasPrice();
536
- } catch {
537
- }
538
+ const [priceHistory, gasPrice] = await Promise.all([
539
+ this.fetchPriceHistory(tokenAddresses).catch(() => void 0),
540
+ this.client.getGasPrice().catch(() => void 0)
541
+ ]);
538
542
  return {
539
543
  timestamp: Date.now(),
540
544
  prices,
@@ -545,7 +549,8 @@ var init_market = __esm({
545
549
  gasPrice,
546
550
  network: {
547
551
  chainId: this.client.chain?.id ?? 8453
548
- }
552
+ },
553
+ priceHistory: priceHistory && Object.keys(priceHistory).length > 0 ? priceHistory : void 0
549
554
  };
550
555
  }
551
556
  /**
@@ -718,6 +723,58 @@ var init_market = __esm({
718
723
  }
719
724
  return prices;
720
725
  }
726
+ /**
727
+ * Fetch 24h hourly price history from CoinGecko.
728
+ * Returns cached data if still fresh (< 60 min old).
729
+ * Only fetches for tokens with known CoinGecko IDs.
730
+ */
731
+ async fetchPriceHistory(tokenAddresses) {
732
+ if (Date.now() - this.lastHistoryFetchAt < HISTORY_STALENESS_MS && Object.keys(this.cachedPriceHistory).length > 0) {
733
+ return { ...this.cachedPriceHistory };
734
+ }
735
+ const history = {};
736
+ const idToAddrs = {};
737
+ for (const addr of tokenAddresses) {
738
+ const cgId = TOKEN_TO_COINGECKO[addr.toLowerCase()];
739
+ if (cgId && !STABLECOIN_IDS.has(cgId)) {
740
+ if (!idToAddrs[cgId]) idToAddrs[cgId] = [];
741
+ idToAddrs[cgId].push(addr.toLowerCase());
742
+ }
743
+ }
744
+ const ids = Object.keys(idToAddrs);
745
+ for (let i = 0; i < ids.length; i++) {
746
+ const cgId = ids[i];
747
+ try {
748
+ const resp = await fetch(
749
+ `https://api.coingecko.com/api/v3/coins/${cgId}/market_chart?vs_currency=usd&days=1`,
750
+ { signal: AbortSignal.timeout(5e3) }
751
+ );
752
+ if (!resp.ok) {
753
+ if (resp.status === 429) {
754
+ console.warn("CoinGecko rate limit hit during price history fetch \u2014 using partial data");
755
+ break;
756
+ }
757
+ continue;
758
+ }
759
+ const data = await resp.json();
760
+ if (data.prices && Array.isArray(data.prices)) {
761
+ const candles = data.prices.map(([ts, price]) => ({ timestamp: ts, price }));
762
+ for (const addr of idToAddrs[cgId]) {
763
+ history[addr] = candles;
764
+ }
765
+ }
766
+ } catch {
767
+ }
768
+ if (i < ids.length - 1) {
769
+ await new Promise((r) => setTimeout(r, 2500));
770
+ }
771
+ }
772
+ if (Object.keys(history).length > 0) {
773
+ this.cachedPriceHistory = { ...this.cachedPriceHistory, ...history };
774
+ this.lastHistoryFetchAt = Date.now();
775
+ }
776
+ return { ...this.cachedPriceHistory };
777
+ }
721
778
  /**
722
779
  * Fetch real on-chain balances: native ETH + ERC-20 tokens.
723
780
  * Uses Multicall3 to batch all balanceOf calls into a single RPC request.
@@ -1643,6 +1700,10 @@ var init_executor = __esm({
1643
1700
  * Validate a signal against config limits and token restrictions
1644
1701
  */
1645
1702
  validateSignal(signal) {
1703
+ if (signal.amountIn <= 0n) {
1704
+ console.warn(`Signal amountIn is ${signal.amountIn} \u2014 skipping (must be positive)`);
1705
+ return false;
1706
+ }
1646
1707
  if (signal.confidence < 0.5) {
1647
1708
  console.warn(`Signal confidence ${signal.confidence} below threshold (0.5)`);
1648
1709
  return false;
@@ -2587,6 +2648,33 @@ function formatSessionReport(result) {
2587
2648
  }
2588
2649
  lines.push("");
2589
2650
  }
2651
+ if (result.trades.length > 0) {
2652
+ lines.push(` ${thinDivider}`);
2653
+ lines.push(" TRADE LOG");
2654
+ lines.push(` ${thinDivider}`);
2655
+ lines.push("");
2656
+ const recentTrades = result.trades.slice(-50);
2657
+ for (const t of recentTrades) {
2658
+ const time = new Date(t.timestamp).toLocaleString(void 0, {
2659
+ month: "short",
2660
+ day: "2-digit",
2661
+ hour: "2-digit",
2662
+ minute: "2-digit"
2663
+ });
2664
+ const tag = t.action === "buy" ? "BUY " : "SELL";
2665
+ const tokenIn = t.tokenIn.slice(0, 6) + "\u2026";
2666
+ const tokenOut = t.tokenOut.slice(0, 6) + "\u2026";
2667
+ const value = `$${t.valueInUSD.toFixed(2)}`;
2668
+ const pnl = t.valueOutUSD - t.valueInUSD - t.feeUSD - t.gasUSD;
2669
+ const pnlStr = pnl >= 0 ? `+$${pnl.toFixed(2)}` : `-$${Math.abs(pnl).toFixed(2)}`;
2670
+ const reason = t.reasoning ? ` "${t.reasoning.slice(0, 60)}"` : "";
2671
+ lines.push(` ${time} ${tag} ${tokenIn}\u2192${tokenOut} ${value} ${pnlStr}${reason}`);
2672
+ }
2673
+ if (result.trades.length > 50) {
2674
+ lines.push(` ... and ${result.trades.length - 50} earlier trades`);
2675
+ }
2676
+ lines.push("");
2677
+ }
2590
2678
  if (result.equityCurve.length >= 3) {
2591
2679
  lines.push(` ${thinDivider}`);
2592
2680
  lines.push(" EQUITY CURVE");
@@ -3920,7 +4008,7 @@ var STRATEGY_TEMPLATES = [
3920
4008
  {
3921
4009
  id: "momentum",
3922
4010
  name: "Momentum Trader",
3923
- description: "Follows price trends and momentum indicators. Buys assets with strong upward momentum.",
4011
+ description: "Follows price trends and momentum indicators. Buys assets with strong upward momentum, sells when momentum fades.",
3924
4012
  riskLevel: "medium",
3925
4013
  riskWarnings: [
3926
4014
  "Momentum strategies can suffer significant losses during trend reversals",
@@ -3928,60 +4016,248 @@ var STRATEGY_TEMPLATES = [
3928
4016
  "Past performance does not guarantee future results",
3929
4017
  "This strategy may underperform in sideways markets"
3930
4018
  ],
3931
- systemPrompt: `You are an AI trading analyst specializing in momentum trading strategies.
4019
+ systemPrompt: `You are an AI trading analyst specializing in momentum trading on Base network.
3932
4020
 
3933
- Your role is to analyze market data and identify momentum-based trading opportunities.
4021
+ Analyze the provided market data and identify momentum-based trading opportunities.
3934
4022
 
3935
- IMPORTANT CONSTRAINTS:
4023
+ RULES:
3936
4024
  - Only recommend trades when there is clear momentum evidence
3937
- - Always consider risk/reward ratios
3938
- - Never recommend more than the configured position size limits
3939
- - Be conservative with confidence scores
4025
+ - Use priceTrends (1h, 4h, 12h, 24h changes) as your primary momentum signal
4026
+ - Fall back to priceChange24h if priceTrends is not available
4027
+ - Be conservative with confidence \u2014 only use > 0.6 for strong signals
4028
+ - Never recommend buying a token you already hold significant amounts of
4029
+ - Always consider selling positions that have lost momentum
4030
+ - Return "hold" signals when uncertain \u2014 doing nothing is often the best trade
3940
4031
 
3941
- When analyzing data, look for:
3942
- 1. Price trends (higher highs, higher lows for uptrends)
3943
- 2. Volume confirmation (increasing volume on moves)
3944
- 3. Relative strength vs market benchmarks
4032
+ ANALYZE:
4033
+ 1. Multi-timeframe momentum: use priceTrends (1h, 4h, 12h, 24h) to confirm trend direction
4034
+ 2. Strong momentum = positive across all timeframes. Divergence (e.g., 1h negative, 12h positive) = weakening
4035
+ 3. Volume confirmation: prefer tokens with > $10K daily volume
4036
+ 4. Portfolio concentration: avoid putting > 20% in any single token
4037
+ 5. Exit signals: sell when short-term trend (1h, 4h) turns negative on a held position
3945
4038
 
3946
- Respond with JSON in this format:
4039
+ Respond with ONLY valid JSON (no markdown, no code fences):
3947
4040
  {
3948
- "analysis": "Brief market analysis",
4041
+ "analysis": "Brief market analysis (1-2 sentences)",
3949
4042
  "signals": [
3950
4043
  {
3951
4044
  "action": "buy" | "sell" | "hold",
3952
- "tokenIn": "0x...",
3953
- "tokenOut": "0x...",
3954
- "percentage": 0-100,
3955
- "confidence": 0-1,
3956
- "reasoning": "Why this trade"
4045
+ "token": "0x... (token address to buy/sell)",
4046
+ "percentage": 5-25,
4047
+ "confidence": 0.0-1.0,
4048
+ "reasoning": "Why this trade (1 sentence)"
3957
4049
  }
3958
4050
  ]
3959
- }`,
3960
- exampleCode: `import { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig } from '@exagent/agent';
4051
+ }
4052
+
4053
+ If no good opportunities exist, return: { "analysis": "No clear momentum signals", "signals": [] }`,
4054
+ exampleCode: `import type { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig, StrategyContext } from '@exagent/agent';
4055
+
4056
+ // Well-known token addresses on Base
4057
+ const WETH = '0x4200000000000000000000000000000000000006';
4058
+ const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
4059
+
4060
+ // Token decimals lookup (extend as needed)
4061
+ const DECIMALS: Record<string, number> = {
4062
+ [WETH]: 18,
4063
+ [USDC]: 6,
4064
+ };
4065
+
4066
+ /**
4067
+ * Format balances for LLM consumption (human-readable amounts)
4068
+ */
4069
+ function formatBalances(balances: Record<string, bigint>, prices: Record<string, number>): Record<string, { amount: string; usdValue: string }> {
4070
+ const result: Record<string, { amount: string; usdValue: string }> = {};
4071
+ for (const [token, balance] of Object.entries(balances)) {
4072
+ if (balance === 0n) continue;
4073
+ const decimals = DECIMALS[token.toLowerCase()] ?? 18;
4074
+ const amount = Number(balance) / (10 ** decimals);
4075
+ const price = prices[token.toLowerCase()] ?? 0;
4076
+ result[token] = {
4077
+ amount: amount.toFixed(decimals <= 8 ? decimals : 6),
4078
+ usdValue: (amount * price).toFixed(2),
4079
+ };
4080
+ }
4081
+ return result;
4082
+ }
4083
+
4084
+ /**
4085
+ * Convert LLM percentage signal to a TradeSignal with bigint amountIn
4086
+ *
4087
+ * The LLM says "buy token X with 10% of my ETH" \u2014 this function converts
4088
+ * that percentage into the actual wei amount to trade.
4089
+ */
4090
+ function toTradeSignal(
4091
+ signal: { action: string; token: string; percentage: number; confidence: number; reasoning?: string },
4092
+ balances: Record<string, bigint>,
4093
+ ): TradeSignal | null {
4094
+ const { action, token, percentage, confidence, reasoning } = signal;
4095
+
4096
+ // Skip hold signals and low confidence
4097
+ if (action === 'hold' || confidence < 0.5) return null;
4098
+
4099
+ // Clamp percentage to 1-50 range for safety
4100
+ const pct = Math.max(1, Math.min(50, percentage));
4101
+
4102
+ if (action === 'buy') {
4103
+ // Buying: spend ETH (or USDC) to acquire the target token
4104
+ // Use ETH balance as the source
4105
+ const ethBalance = balances[WETH] ?? 0n;
4106
+ if (ethBalance === 0n) return null;
4107
+ const amountIn = (ethBalance * BigInt(Math.round(pct * 100))) / 10000n;
4108
+ if (amountIn === 0n) return null;
4109
+ return { action: 'buy', tokenIn: WETH, tokenOut: token, amountIn, confidence, reasoning };
4110
+ }
3961
4111
 
4112
+ if (action === 'sell') {
4113
+ // Selling: sell the target token for ETH
4114
+ const tokenBalance = balances[token.toLowerCase()] ?? balances[token] ?? 0n;
4115
+ if (tokenBalance === 0n) return null;
4116
+ const amountIn = (tokenBalance * BigInt(Math.round(pct * 100))) / 10000n;
4117
+ if (amountIn === 0n) return null;
4118
+ return { action: 'sell', tokenIn: token, tokenOut: WETH, amountIn, confidence, reasoning };
4119
+ }
4120
+
4121
+ return null;
4122
+ }
4123
+
4124
+ /**
4125
+ * Momentum Trading Strategy
4126
+ *
4127
+ * Sends market data to the LLM, parses the response, and converts
4128
+ * percentage-based signals into executable TradeSignal objects.
4129
+ */
3962
4130
  export const generateSignals: StrategyFunction = async (
3963
4131
  marketData: MarketData,
3964
4132
  llm: LLMAdapter,
3965
- config: AgentConfig
4133
+ config: AgentConfig,
4134
+ context?: StrategyContext,
3966
4135
  ): Promise<TradeSignal[]> => {
4136
+ // Summarize price history into compact trend data for the LLM
4137
+ // Instead of sending 24 raw candles per token, compute useful signals:
4138
+ // - 1h, 4h, 12h, 24h price changes (momentum at different timeframes)
4139
+ const priceTrends: Record<string, { '1h': string; '4h': string; '12h': string; '24h': string }> = {};
4140
+ if (marketData.priceHistory) {
4141
+ for (const [addr, candles] of Object.entries(marketData.priceHistory)) {
4142
+ if (candles.length < 2) continue;
4143
+ const latest = candles[candles.length - 1].price;
4144
+ const getChangeAt = (hoursAgo: number): string => {
4145
+ const target = Date.now() - hoursAgo * 3600_000;
4146
+ // Find the candle closest to the target time
4147
+ let closest = candles[0];
4148
+ for (const c of candles) {
4149
+ if (Math.abs(c.timestamp - target) < Math.abs(closest.timestamp - target)) closest = c;
4150
+ }
4151
+ if (closest.price === 0) return '0.0%';
4152
+ const pct = ((latest - closest.price) / closest.price) * 100;
4153
+ return (pct >= 0 ? '+' : '') + pct.toFixed(1) + '%';
4154
+ };
4155
+ priceTrends[addr] = {
4156
+ '1h': getChangeAt(1),
4157
+ '4h': getChangeAt(4),
4158
+ '12h': getChangeAt(12),
4159
+ '24h': getChangeAt(24),
4160
+ };
4161
+ }
4162
+ }
4163
+
4164
+ // Build the data payload for the LLM
4165
+ const payload: Record<string, unknown> = {
4166
+ prices: marketData.prices,
4167
+ balances: formatBalances(marketData.balances, marketData.prices),
4168
+ portfolioValueUSD: marketData.portfolioValue.toFixed(2),
4169
+ priceChange24h: marketData.priceChange24h ?? {},
4170
+ volume24h: marketData.volume24h ?? {},
4171
+ recentTrades: (context?.tradeHistory ?? []).slice(0, 5).map(t => ({
4172
+ action: t.action,
4173
+ token: t.action === 'buy' ? t.tokenOut : t.tokenIn,
4174
+ reasoning: t.reasoning,
4175
+ timestamp: new Date(t.timestamp).toISOString(),
4176
+ })),
4177
+ };
4178
+
4179
+ // Include price trends if available (more useful than raw candles for LLM)
4180
+ if (Object.keys(priceTrends).length > 0) {
4181
+ payload.priceTrends = priceTrends;
4182
+ }
4183
+
4184
+ // Call the LLM
3967
4185
  const response = await llm.chat([
3968
- { role: 'system', content: MOMENTUM_SYSTEM_PROMPT },
3969
- { role: 'user', content: JSON.stringify({
3970
- prices: marketData.prices,
3971
- balances: formatBalances(marketData.balances),
3972
- portfolioValue: marketData.portfolioValue,
3973
- })}
4186
+ { role: 'system', content: config.strategyPrompt ?? MOMENTUM_SYSTEM_PROMPT },
4187
+ { role: 'user', content: JSON.stringify(payload) },
3974
4188
  ]);
3975
4189
 
3976
- // Parse LLM response and convert to TradeSignals
3977
- const parsed = JSON.parse(response.content);
3978
- return parsed.signals.map(convertToTradeSignal);
3979
- };`
4190
+ // Parse the response \u2014 handle markdown code fences if the LLM wraps its output
4191
+ let content = response.content.trim();
4192
+ if (content.startsWith('\`\`\`')) {
4193
+ content = content.replace(/^\`\`\`(?:json)?\\n?/, '').replace(/\\n?\`\`\`$/, '').trim();
4194
+ }
4195
+
4196
+ let parsed: { signals?: Array<{ action: string; token: string; percentage: number; confidence: number; reasoning?: string }> };
4197
+ try {
4198
+ parsed = JSON.parse(content);
4199
+ } catch (e) {
4200
+ console.error('[strategy] Failed to parse LLM response:', (e as Error).message);
4201
+ console.error('[strategy] Raw response:', content.slice(0, 200));
4202
+ return []; // Safe fallback: no trades
4203
+ }
4204
+
4205
+ if (!parsed.signals || !Array.isArray(parsed.signals)) {
4206
+ return [];
4207
+ }
4208
+
4209
+ // Convert each signal to a TradeSignal with proper bigint amountIn
4210
+ const signals: TradeSignal[] = [];
4211
+ for (const raw of parsed.signals) {
4212
+ const signal = toTradeSignal(raw, marketData.balances);
4213
+ if (signal) signals.push(signal);
4214
+ }
4215
+
4216
+ return signals;
4217
+ };
4218
+
4219
+ // Default system prompt \u2014 used when config.strategyPrompt is not set
4220
+ const MOMENTUM_SYSTEM_PROMPT = \`You are an AI trading analyst specializing in momentum trading on Base network.
4221
+
4222
+ Analyze the provided market data and identify momentum-based trading opportunities.
4223
+
4224
+ RULES:
4225
+ - Only recommend trades when there is clear momentum evidence
4226
+ - Use priceTrends (1h, 4h, 12h, 24h changes) as your primary momentum signal
4227
+ - Fall back to priceChange24h if priceTrends is not available
4228
+ - Be conservative with confidence \u2014 only use > 0.6 for strong signals
4229
+ - Never recommend buying a token you already hold significant amounts of
4230
+ - Always consider selling positions that have lost momentum
4231
+ - Return "hold" signals when uncertain \u2014 doing nothing is often the best trade
4232
+
4233
+ ANALYZE:
4234
+ 1. Multi-timeframe momentum: use priceTrends (1h, 4h, 12h, 24h) to confirm trend direction
4235
+ 2. Strong momentum = positive across all timeframes. Divergence (e.g., 1h negative, 12h positive) = weakening
4236
+ 3. Volume confirmation: prefer tokens with > $10K daily volume
4237
+ 4. Portfolio concentration: avoid putting > 20% in any single token
4238
+ 5. Exit signals: sell when short-term trend (1h, 4h) turns negative on a held position
4239
+
4240
+ Respond with ONLY valid JSON (no markdown, no code fences):
4241
+ {
4242
+ "analysis": "Brief market analysis (1-2 sentences)",
4243
+ "signals": [
4244
+ {
4245
+ "action": "buy" | "sell" | "hold",
4246
+ "token": "0x... (token address to buy/sell)",
4247
+ "percentage": 5-25,
4248
+ "confidence": 0.0-1.0,
4249
+ "reasoning": "Why this trade (1 sentence)"
4250
+ }
4251
+ ]
4252
+ }
4253
+
4254
+ If no good opportunities exist, return: { "analysis": "No clear momentum signals", "signals": [] }\`;
4255
+ `
3980
4256
  },
3981
4257
  {
3982
4258
  id: "value",
3983
4259
  name: "Value Investor",
3984
- description: "Looks for undervalued assets based on fundamentals. Takes long-term positions.",
4260
+ description: "Looks for undervalued assets based on fundamentals. Takes long-term positions with lower turnover.",
3985
4261
  riskLevel: "low",
3986
4262
  riskWarnings: [
3987
4263
  "Value traps can result in prolonged losses",
@@ -3989,47 +4265,135 @@ export const generateSignals: StrategyFunction = async (
3989
4265
  "Fundamental analysis may not apply well to all crypto assets",
3990
4266
  "Market sentiment can override fundamentals for long periods"
3991
4267
  ],
3992
- systemPrompt: `You are an AI trading analyst specializing in value investing.
4268
+ systemPrompt: `You are an AI trading analyst specializing in value investing on Base network.
3993
4269
 
3994
4270
  Your role is to identify undervalued assets with strong fundamentals.
3995
4271
 
3996
- IMPORTANT CONSTRAINTS:
4272
+ RULES:
3997
4273
  - Focus on long-term value, not short-term price movements
3998
4274
  - Only recommend assets with clear value propositions
3999
4275
  - Consider protocol revenue, TVL, active users, developer activity
4000
- - Be very selective - quality over quantity
4276
+ - Be very selective \u2014 quality over quantity
4277
+ - Prefer established tokens (AERO, WELL, MORPHO, COMP, CRV) over memecoins
4278
+ - Hold positions for days/weeks, not hours
4001
4279
 
4002
- When analyzing, consider:
4003
- 1. Protocol fundamentals (revenue, TVL, user growth)
4004
- 2. Token economics (supply schedule, utility)
4005
- 3. Competitive positioning
4006
- 4. Valuation relative to peers
4007
-
4008
- Respond with JSON in this format:
4280
+ Respond with ONLY valid JSON:
4009
4281
  {
4010
4282
  "analysis": "Brief fundamental analysis",
4011
4283
  "signals": [
4012
4284
  {
4013
4285
  "action": "buy" | "sell" | "hold",
4014
- "tokenIn": "0x...",
4015
- "tokenOut": "0x...",
4016
- "percentage": 0-100,
4017
- "confidence": 0-1,
4286
+ "token": "0x... (token address)",
4287
+ "percentage": 5-20,
4288
+ "confidence": 0.0-1.0,
4018
4289
  "reasoning": "Fundamental thesis"
4019
4290
  }
4020
4291
  ]
4021
4292
  }`,
4022
- exampleCode: `import { StrategyFunction } from '@exagent/agent';
4293
+ exampleCode: `import type { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig, StrategyContext } from '@exagent/agent';
4294
+
4295
+ const WETH = '0x4200000000000000000000000000000000000006';
4296
+ const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
4297
+ const DECIMALS: Record<string, number> = { [WETH]: 18, [USDC]: 6 };
4298
+
4299
+ function formatBalances(balances: Record<string, bigint>, prices: Record<string, number>): Record<string, { amount: string; usdValue: string }> {
4300
+ const result: Record<string, { amount: string; usdValue: string }> = {};
4301
+ for (const [token, balance] of Object.entries(balances)) {
4302
+ if (balance === 0n) continue;
4303
+ const decimals = DECIMALS[token.toLowerCase()] ?? 18;
4304
+ const amount = Number(balance) / (10 ** decimals);
4305
+ const price = prices[token.toLowerCase()] ?? 0;
4306
+ result[token] = { amount: amount.toFixed(6), usdValue: (amount * price).toFixed(2) };
4307
+ }
4308
+ return result;
4309
+ }
4310
+
4311
+ function toTradeSignal(
4312
+ signal: { action: string; token: string; percentage: number; confidence: number; reasoning?: string },
4313
+ balances: Record<string, bigint>,
4314
+ ): TradeSignal | null {
4315
+ if (signal.action === 'hold' || signal.confidence < 0.5) return null;
4316
+ const pct = Math.max(1, Math.min(50, signal.percentage));
4317
+ if (signal.action === 'buy') {
4318
+ const ethBalance = balances[WETH] ?? 0n;
4319
+ if (ethBalance === 0n) return null;
4320
+ const amountIn = (ethBalance * BigInt(Math.round(pct * 100))) / 10000n;
4321
+ if (amountIn === 0n) return null;
4322
+ return { action: 'buy', tokenIn: WETH, tokenOut: signal.token, amountIn, confidence: signal.confidence, reasoning: signal.reasoning };
4323
+ }
4324
+ if (signal.action === 'sell') {
4325
+ const tokenBalance = balances[signal.token.toLowerCase()] ?? balances[signal.token] ?? 0n;
4326
+ if (tokenBalance === 0n) return null;
4327
+ const amountIn = (tokenBalance * BigInt(Math.round(pct * 100))) / 10000n;
4328
+ if (amountIn === 0n) return null;
4329
+ return { action: 'sell', tokenIn: signal.token, tokenOut: WETH, amountIn, confidence: signal.confidence, reasoning: signal.reasoning };
4330
+ }
4331
+ return null;
4332
+ }
4333
+
4334
+ export const generateSignals: StrategyFunction = async (
4335
+ marketData: MarketData,
4336
+ llm: LLMAdapter,
4337
+ config: AgentConfig,
4338
+ context?: StrategyContext,
4339
+ ): Promise<TradeSignal[]> => {
4340
+ const payload = {
4341
+ prices: marketData.prices,
4342
+ balances: formatBalances(marketData.balances, marketData.prices),
4343
+ portfolioValueUSD: marketData.portfolioValue.toFixed(2),
4344
+ priceChange24h: marketData.priceChange24h ?? {},
4345
+ positions: (context?.positions ?? []).map(p => ({
4346
+ token: p.tokenAddress,
4347
+ entryPrice: p.averageEntryPrice,
4348
+ currentAmount: p.currentAmount.toString(),
4349
+ })),
4350
+ };
4023
4351
 
4024
- export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
4025
- // Value strategy runs less frequently
4026
4352
  const response = await llm.chat([
4027
- { role: 'system', content: VALUE_SYSTEM_PROMPT },
4028
- { role: 'user', content: JSON.stringify(marketData) }
4353
+ { role: 'system', content: config.strategyPrompt ?? VALUE_SYSTEM_PROMPT },
4354
+ { role: 'user', content: JSON.stringify(payload) },
4029
4355
  ]);
4030
4356
 
4031
- return parseSignals(response.content);
4032
- };`
4357
+ let content = response.content.trim();
4358
+ if (content.startsWith('\\\`\\\`\\\`')) {
4359
+ content = content.replace(/^\\\`\\\`\\\`(?:json)?\\n?/, '').replace(/\\n?\\\`\\\`\\\`$/, '').trim();
4360
+ }
4361
+
4362
+ try {
4363
+ const parsed = JSON.parse(content);
4364
+ if (!parsed.signals || !Array.isArray(parsed.signals)) return [];
4365
+ return parsed.signals.map((s: any) => toTradeSignal(s, marketData.balances)).filter(Boolean) as TradeSignal[];
4366
+ } catch (e) {
4367
+ console.error('[strategy] Failed to parse LLM response:', (e as Error).message);
4368
+ return [];
4369
+ }
4370
+ };
4371
+
4372
+ const VALUE_SYSTEM_PROMPT = \`You are an AI trading analyst specializing in value investing on Base network.
4373
+
4374
+ Your role is to identify undervalued assets with strong fundamentals.
4375
+
4376
+ RULES:
4377
+ - Focus on long-term value, not short-term price movements
4378
+ - Only recommend assets with clear value propositions
4379
+ - Be very selective \u2014 quality over quantity
4380
+ - Prefer established tokens over memecoins
4381
+ - Hold positions for days/weeks, not hours
4382
+
4383
+ Respond with ONLY valid JSON:
4384
+ {
4385
+ "analysis": "Brief fundamental analysis",
4386
+ "signals": [
4387
+ {
4388
+ "action": "buy" | "sell" | "hold",
4389
+ "token": "0x... (token address)",
4390
+ "percentage": 5-20,
4391
+ "confidence": 0.0-1.0,
4392
+ "reasoning": "Fundamental thesis"
4393
+ }
4394
+ ]
4395
+ }\`;
4396
+ `
4033
4397
  },
4034
4398
  {
4035
4399
  id: "arbitrage",
@@ -4069,7 +4433,7 @@ Respond with JSON in this format:
4069
4433
  exampleCode: `// Note: Pure arbitrage requires specialized infrastructure
4070
4434
  // This template is for educational purposes
4071
4435
 
4072
- import { StrategyFunction } from '@exagent/agent';
4436
+ import type { StrategyFunction } from '@exagent/agent';
4073
4437
 
4074
4438
  export const generateSignals: StrategyFunction = async (marketData, llm, config) => {
4075
4439
  // Arbitrage requires real-time price feeds from multiple sources
@@ -4101,18 +4465,21 @@ Respond with JSON:
4101
4465
  {
4102
4466
  "signals": []
4103
4467
  }`,
4104
- exampleCode: `import { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig } from '@exagent/agent';
4468
+ exampleCode: `import type { StrategyFunction, MarketData, TradeSignal, LLMAdapter, AgentConfig, StrategyContext } from '@exagent/agent';
4469
+
4470
+ const WETH = '0x4200000000000000000000000000000000000006';
4105
4471
 
4106
4472
  /**
4107
4473
  * Custom Strategy Template
4108
4474
  *
4109
4475
  * Customize this file with your own trading logic and prompts.
4110
- * Your prompts are YOUR intellectual property - we don't store them.
4476
+ * Your prompts are YOUR intellectual property \u2014 we don't store them.
4111
4477
  */
4112
4478
  export const generateSignals: StrategyFunction = async (
4113
4479
  marketData: MarketData,
4114
4480
  llm: LLMAdapter,
4115
- config: AgentConfig
4481
+ config: AgentConfig,
4482
+ context?: StrategyContext,
4116
4483
  ): Promise<TradeSignal[]> => {
4117
4484
  // Your custom system prompt (this is your secret sauce)
4118
4485
  const systemPrompt = \`
@@ -4122,13 +4489,26 @@ export const generateSignals: StrategyFunction = async (
4122
4489
  // Call the LLM with your prompt
4123
4490
  const response = await llm.chat([
4124
4491
  { role: 'system', content: systemPrompt },
4125
- { role: 'user', content: JSON.stringify(marketData) }
4492
+ { role: 'user', content: JSON.stringify({
4493
+ prices: marketData.prices,
4494
+ balances: Object.fromEntries(
4495
+ Object.entries(marketData.balances)
4496
+ .filter(([, v]) => v > 0n)
4497
+ .map(([k, v]) => [k, v.toString()])
4498
+ ),
4499
+ portfolioValue: marketData.portfolioValue,
4500
+ })},
4126
4501
  ]);
4127
4502
 
4128
4503
  // Parse and return signals
4129
4504
  // IMPORTANT: Validate LLM output before using
4505
+ let content = response.content.trim();
4506
+ if (content.startsWith('\\\`\\\`\\\`')) {
4507
+ content = content.replace(/^\\\`\\\`\\\`(?:json)?\\n?/, '').replace(/\\n?\\\`\\\`\\\`$/, '').trim();
4508
+ }
4509
+
4130
4510
  try {
4131
- const parsed = JSON.parse(response.content);
4511
+ const parsed = JSON.parse(content);
4132
4512
  return parsed.signals || [];
4133
4513
  } catch (e) {
4134
4514
  console.error('Failed to parse LLM response:', e);
@@ -4137,6 +4517,9 @@ export const generateSignals: StrategyFunction = async (
4137
4517
  };`
4138
4518
  }
4139
4519
  ];
4520
+ function getStrategyTemplate(id) {
4521
+ return STRATEGY_TEMPLATES.find((t) => t.id === id);
4522
+ }
4140
4523
  function getAllStrategyTemplates() {
4141
4524
  return STRATEGY_TEMPLATES;
4142
4525
  }
@@ -4284,6 +4667,8 @@ var AgentConfigSchema = import_zod.z.object({
4284
4667
  perp: PerpConfigSchema,
4285
4668
  // Prediction market configuration (Polymarket)
4286
4669
  prediction: PredictionConfigSchema,
4670
+ // Custom strategy system prompt (overrides built-in template prompt)
4671
+ strategyPrompt: import_zod.z.string().optional(),
4287
4672
  // Allowed tokens (addresses)
4288
4673
  allowedTokens: import_zod.z.array(import_zod.z.string()).optional()
4289
4674
  });
@@ -8071,7 +8456,7 @@ function loadSecureEnv(basePath, passphrase) {
8071
8456
  }
8072
8457
 
8073
8458
  // src/index.ts
8074
- var AGENT_VERSION = "0.1.47";
8459
+ var AGENT_VERSION = "0.1.48";
8075
8460
 
8076
8461
  // src/relay.ts
8077
8462
  var RelayClient = class {
@@ -10788,6 +11173,95 @@ ${llmEnvVar}EXAGENT_LLM_MODEL=${config.llm?.model || ""}
10788
11173
  (0, import_dotenv2.config)({ path: envPath, override: true });
10789
11174
  }
10790
11175
  }
11176
+ program.command("init").description("Scaffold a new agent project with strategy template and config").option("-t, --template <template>", "Strategy template (momentum, value, arbitrage, custom)", "momentum").option("--force", "Overwrite existing files without prompting").action(async (options) => {
11177
+ try {
11178
+ const templateId = options.template;
11179
+ const template = getStrategyTemplate(templateId);
11180
+ if (!template) {
11181
+ const templates = getAllStrategyTemplates();
11182
+ console.error(`Unknown template: ${templateId}`);
11183
+ console.error(`Available templates: ${templates.map((t) => t.id).join(", ")}`);
11184
+ process.exit(1);
11185
+ }
11186
+ console.log("");
11187
+ console.log("=".repeat(50));
11188
+ console.log(" EXAGENT INIT");
11189
+ console.log("=".repeat(50));
11190
+ console.log("");
11191
+ console.log(` Template: ${template.name}`);
11192
+ console.log(` Risk: ${template.riskLevel}`);
11193
+ console.log("");
11194
+ const cwd = process.cwd();
11195
+ const strategyPath = path7.join(cwd, "strategy.ts");
11196
+ const configPath = path7.join(cwd, "agent-config.json");
11197
+ const existingFiles = [];
11198
+ if (fs6.existsSync(strategyPath)) existingFiles.push("strategy.ts");
11199
+ if (fs6.existsSync(configPath)) existingFiles.push("agent-config.json");
11200
+ if (existingFiles.length > 0 && !options.force) {
11201
+ console.log(` Files already exist: ${existingFiles.join(", ")}`);
11202
+ const answer = await prompt(" Overwrite? (y/n): ");
11203
+ if (answer.toLowerCase() !== "y") {
11204
+ console.log(" Aborted.");
11205
+ process.exit(0);
11206
+ }
11207
+ console.log("");
11208
+ }
11209
+ fs6.writeFileSync(strategyPath, template.exampleCode.trim() + "\n");
11210
+ console.log(" Created: strategy.ts");
11211
+ const defaultConfig = {
11212
+ agentId: 0,
11213
+ name: "my-agent",
11214
+ network: "mainnet",
11215
+ llm: {
11216
+ provider: "openai",
11217
+ model: "gpt-4o",
11218
+ temperature: 0.7,
11219
+ maxTokens: 4096
11220
+ },
11221
+ riskUniverse: "established",
11222
+ trading: {
11223
+ timeHorizon: "swing",
11224
+ maxPositionSizeBps: 1e3,
11225
+ maxDailyLossBps: 500,
11226
+ maxConcurrentPositions: 5,
11227
+ tradingIntervalMs: 6e4,
11228
+ maxSlippageBps: 100,
11229
+ minTradeValueUSD: 1
11230
+ },
11231
+ wallet: {
11232
+ setup: "generate"
11233
+ },
11234
+ relay: {
11235
+ enabled: true,
11236
+ apiUrl: "https://exagent-api.onrender.com"
11237
+ }
11238
+ };
11239
+ if (!fs6.existsSync(configPath) || options.force || existingFiles.includes("agent-config.json")) {
11240
+ fs6.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n");
11241
+ console.log(" Created: agent-config.json");
11242
+ }
11243
+ console.log("");
11244
+ console.log("=".repeat(50));
11245
+ console.log(" NEXT STEPS");
11246
+ console.log("=".repeat(50));
11247
+ console.log("");
11248
+ console.log(" 1. Register your agent at https://exagent.io/deploy");
11249
+ console.log(" 2. Copy your agent ID into agent-config.json");
11250
+ console.log(" 3. Update agent-config.json with your name and LLM provider");
11251
+ console.log(" 4. Run: npx @exagent/agent paper");
11252
+ console.log(" (paper trade first to validate your strategy)");
11253
+ console.log("");
11254
+ console.log(" 5. When ready for real trading:");
11255
+ console.log(" npx @exagent/agent run");
11256
+ console.log("");
11257
+ console.log("=".repeat(50));
11258
+ console.log("");
11259
+ process.exit(0);
11260
+ } catch (error) {
11261
+ console.error("Error:", error instanceof Error ? error.message : error);
11262
+ process.exit(1);
11263
+ }
11264
+ });
10791
11265
  program.command("run").description("Start the trading agent").option("-c, --config <path>", "Path to agent-config.json", "agent-config.json").option("-p, --passphrase <passphrase>", "Passphrase to decrypt .env.enc").action(async (options) => {
10792
11266
  try {
10793
11267
  await checkFirstRunSetup(options.config);