@exagent/agent 0.1.41 → 0.1.43

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
@@ -3435,16 +3435,20 @@ var TOKEN_SYMBOLS = {
3435
3435
  var KEY_POSITIONS = "__positions";
3436
3436
  var KEY_TRADE_HISTORY = "__trade_history";
3437
3437
  var KEY_RISK_STATE = "__risk_state";
3438
+ var KEY_SELL_FAILURES = "__sell_failures";
3439
+ var STUCK_FAILURE_THRESHOLD = 3;
3438
3440
  var PositionTracker = class {
3439
3441
  store;
3440
3442
  positions;
3441
3443
  tradeHistory;
3444
+ sellFailures;
3442
3445
  maxTradeHistory;
3443
3446
  constructor(store, options) {
3444
3447
  this.store = store;
3445
3448
  this.maxTradeHistory = options?.maxTradeHistory ?? 50;
3446
3449
  this.positions = store.get(KEY_POSITIONS) || {};
3447
3450
  this.tradeHistory = store.get(KEY_TRADE_HISTORY) || [];
3451
+ this.sellFailures = store.get(KEY_SELL_FAILURES) || {};
3448
3452
  const posCount = Object.keys(this.positions).length;
3449
3453
  if (posCount > 0 || this.tradeHistory.length > 0) {
3450
3454
  console.log(`Position tracker loaded: ${posCount} positions, ${this.tradeHistory.length} trade records`);
@@ -3480,10 +3484,10 @@ var PositionTracker = class {
3480
3484
  const realizedPnL = this.handleSell(tokenIn.toLowerCase(), tradeValueUSD);
3481
3485
  record.realizedPnL = realizedPnL;
3482
3486
  }
3483
- }
3484
- this.tradeHistory.unshift(record);
3485
- if (this.tradeHistory.length > this.maxTradeHistory) {
3486
- this.tradeHistory = this.tradeHistory.slice(0, this.maxTradeHistory);
3487
+ this.tradeHistory.unshift(record);
3488
+ if (this.tradeHistory.length > this.maxTradeHistory) {
3489
+ this.tradeHistory = this.tradeHistory.slice(0, this.maxTradeHistory);
3490
+ }
3487
3491
  }
3488
3492
  this.persist();
3489
3493
  }
@@ -3544,26 +3548,58 @@ var PositionTracker = class {
3544
3548
  // ============================================================
3545
3549
  /**
3546
3550
  * Sync tracked positions with on-chain balances.
3547
- * Updates currentAmount, detects new tokens (airdrops), removes zeroed positions.
3551
+ * Updates currentAmount, detects new tokens (airdrops), and handles position exits.
3552
+ *
3553
+ * CRITICAL: When a tracked position's balance decreases without a corresponding
3554
+ * sell trade, we infer a manual exit and record it with realized PnL calculated
3555
+ * at current market price. This prevents PnL manipulation via off-router sells.
3556
+ *
3557
+ * Returns array of inferred exits for the runtime to report to Command Center.
3548
3558
  */
3549
3559
  syncBalances(balances, prices) {
3550
3560
  let changed = false;
3561
+ const inferredExits = [];
3551
3562
  for (const [address, balance] of Object.entries(balances)) {
3552
3563
  const token = address.toLowerCase();
3553
3564
  if (BASE_ASSETS.has(token)) continue;
3554
3565
  const decimals = getTokenDecimals(token);
3555
3566
  const amount = Number(balance) / Math.pow(10, decimals);
3567
+ const position = this.positions[token];
3556
3568
  if (amount > 0) {
3557
- if (this.positions[token]) {
3558
- if (this.positions[token].currentAmount !== amount) {
3559
- this.positions[token].currentAmount = amount;
3560
- this.positions[token].lastUpdateTimestamp = Date.now();
3569
+ if (position) {
3570
+ const previousAmount = position.currentAmount;
3571
+ if (amount < previousAmount && position.totalAmountAcquired > 0) {
3572
+ const exitedAmount = previousAmount - amount;
3573
+ const currentPrice = prices[token] || position.averageEntryPrice || 0;
3574
+ const exitValueUSD = exitedAmount * currentPrice;
3575
+ const costBasisOfExited = exitedAmount * position.averageEntryPrice;
3576
+ const realizedPnL = exitValueUSD - costBasisOfExited;
3577
+ const symbol = position.symbol || TOKEN_SYMBOLS[token] || getTokenSymbol(token) || token.slice(0, 10);
3578
+ this.recordInferredExit(token, exitedAmount, exitValueUSD, realizedPnL, symbol);
3579
+ inferredExits.push({
3580
+ token,
3581
+ symbol,
3582
+ amountExited: exitedAmount,
3583
+ exitValueUSD,
3584
+ realizedPnL,
3585
+ costBasis: costBasisOfExited
3586
+ });
3587
+ const exitRatio = exitedAmount / position.totalAmountAcquired;
3588
+ position.totalCostBasis = Math.max(0, position.totalCostBasis * (1 - exitRatio));
3589
+ position.totalAmountAcquired = Math.max(0, position.totalAmountAcquired - exitedAmount);
3590
+ console.log(
3591
+ `Position tracker: inferred exit of ${exitedAmount.toFixed(4)} ${symbol} ($${exitValueUSD.toFixed(2)} value, PnL: $${realizedPnL >= 0 ? "+" : ""}${realizedPnL.toFixed(2)})`
3592
+ );
3593
+ }
3594
+ if (position.currentAmount !== amount) {
3595
+ position.currentAmount = amount;
3596
+ position.lastUpdateTimestamp = Date.now();
3561
3597
  changed = true;
3562
3598
  }
3563
- if (!this.positions[token].symbol) {
3599
+ if (!position.symbol) {
3564
3600
  const resolved = TOKEN_SYMBOLS[token] || getTokenSymbol(token);
3565
3601
  if (resolved) {
3566
- this.positions[token].symbol = resolved;
3602
+ position.symbol = resolved;
3567
3603
  changed = true;
3568
3604
  }
3569
3605
  }
@@ -3586,14 +3622,60 @@ var PositionTracker = class {
3586
3622
  }
3587
3623
  changed = true;
3588
3624
  }
3589
- } else if (this.positions[token]) {
3625
+ } else if (position) {
3626
+ if (position.totalAmountAcquired > 0 && position.currentAmount > 0) {
3627
+ const currentPrice = prices[token] || position.averageEntryPrice || 0;
3628
+ const exitValueUSD = position.currentAmount * currentPrice;
3629
+ const costBasisOfExited = position.currentAmount * position.averageEntryPrice;
3630
+ const realizedPnL = exitValueUSD - costBasisOfExited;
3631
+ const symbol = position.symbol || TOKEN_SYMBOLS[token] || getTokenSymbol(token) || token.slice(0, 10);
3632
+ this.recordInferredExit(token, position.currentAmount, exitValueUSD, realizedPnL, symbol);
3633
+ inferredExits.push({
3634
+ token,
3635
+ symbol,
3636
+ amountExited: position.currentAmount,
3637
+ exitValueUSD,
3638
+ realizedPnL,
3639
+ costBasis: costBasisOfExited
3640
+ });
3641
+ console.log(
3642
+ `Position tracker: inferred FULL exit of ${position.currentAmount.toFixed(4)} ${symbol} ($${exitValueUSD.toFixed(2)} value, PnL: $${realizedPnL >= 0 ? "+" : ""}${realizedPnL.toFixed(2)})`
3643
+ );
3644
+ }
3590
3645
  delete this.positions[token];
3646
+ if (this.sellFailures[token]) {
3647
+ delete this.sellFailures[token];
3648
+ }
3591
3649
  changed = true;
3592
3650
  }
3593
3651
  }
3594
- if (changed) {
3652
+ if (changed || inferredExits.length > 0) {
3595
3653
  this.persist();
3596
3654
  }
3655
+ return inferredExits;
3656
+ }
3657
+ /**
3658
+ * Record an inferred exit in trade history.
3659
+ * These are exits detected from balance changes, not from tracked sell executions.
3660
+ */
3661
+ recordInferredExit(token, amount, valueUSD, realizedPnL, symbol) {
3662
+ const record = {
3663
+ timestamp: Date.now(),
3664
+ action: "sell",
3665
+ tokenIn: token,
3666
+ tokenOut: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
3667
+ // USDC (assumed exit to stablecoin)
3668
+ amountIn: amount.toString(),
3669
+ priceUSD: valueUSD,
3670
+ reasoning: `Inferred exit: balance decreased without tracked sell. Exited ${amount.toFixed(4)} ${symbol} at ~$${(valueUSD / amount).toFixed(4)}/token.`,
3671
+ realizedPnL,
3672
+ success: true,
3673
+ inferred: true
3674
+ };
3675
+ this.tradeHistory.unshift(record);
3676
+ if (this.tradeHistory.length > this.maxTradeHistory) {
3677
+ this.tradeHistory = this.tradeHistory.slice(0, this.maxTradeHistory);
3678
+ }
3597
3679
  }
3598
3680
  // ============================================================
3599
3681
  // QUERY METHODS (for strategies)
@@ -3634,6 +3716,86 @@ var PositionTracker = class {
3634
3716
  return Object.values(pnl).reduce((sum, v) => sum + v, 0);
3635
3717
  }
3636
3718
  // ============================================================
3719
+ // STUCK POSITION DETECTION
3720
+ // ============================================================
3721
+ /**
3722
+ * Record a sell failure for a token. Called when a sell attempt fails
3723
+ * due to liquidity issues, route simulation failure, or excessive price impact.
3724
+ */
3725
+ recordSellFailure(token, reason) {
3726
+ const key = token.toLowerCase();
3727
+ const existing = this.sellFailures[key];
3728
+ if (existing) {
3729
+ existing.consecutiveFailures += 1;
3730
+ existing.lastFailureReason = reason;
3731
+ existing.lastFailureTimestamp = Date.now();
3732
+ } else {
3733
+ this.sellFailures[key] = {
3734
+ consecutiveFailures: 1,
3735
+ lastFailureReason: reason,
3736
+ lastFailureTimestamp: Date.now()
3737
+ };
3738
+ }
3739
+ this.store.set(KEY_SELL_FAILURES, this.sellFailures);
3740
+ }
3741
+ /**
3742
+ * Clear sell failure tracking for a token (called after successful sell).
3743
+ */
3744
+ clearSellFailure(token) {
3745
+ const key = token.toLowerCase();
3746
+ if (this.sellFailures[key]) {
3747
+ delete this.sellFailures[key];
3748
+ this.store.set(KEY_SELL_FAILURES, this.sellFailures);
3749
+ }
3750
+ }
3751
+ /**
3752
+ * Detect positions that are "stuck" — we've tried and failed to sell them
3753
+ * multiple times, likely due to insufficient liquidity.
3754
+ *
3755
+ * Returns positions that have failed to sell >= STUCK_FAILURE_THRESHOLD times.
3756
+ */
3757
+ getStuckPositions(prices) {
3758
+ const stuck = [];
3759
+ const now = Date.now();
3760
+ for (const [token, failure] of Object.entries(this.sellFailures)) {
3761
+ if (failure.consecutiveFailures < STUCK_FAILURE_THRESHOLD) continue;
3762
+ const position = this.positions[token];
3763
+ if (!position || position.currentAmount <= 0) continue;
3764
+ const price = prices[token] || position.averageEntryPrice || 0;
3765
+ const estimatedValueUSD = position.currentAmount * price;
3766
+ stuck.push({
3767
+ token,
3768
+ symbol: position.symbol || TOKEN_SYMBOLS[token] || token.slice(0, 10),
3769
+ consecutiveFailures: failure.consecutiveFailures,
3770
+ lastFailureReason: failure.lastFailureReason,
3771
+ holdingDurationMs: now - position.entryTimestamp,
3772
+ estimatedValueUSD
3773
+ });
3774
+ }
3775
+ return stuck;
3776
+ }
3777
+ /**
3778
+ * Check if a specific position is stuck.
3779
+ */
3780
+ isPositionStuck(token) {
3781
+ const failure = this.sellFailures[token.toLowerCase()];
3782
+ return !!failure && failure.consecutiveFailures >= STUCK_FAILURE_THRESHOLD;
3783
+ }
3784
+ /**
3785
+ * Check if we should alert about a stuck position (rate-limited to once per 24h).
3786
+ * Returns true if we should alert, and marks the alert as sent.
3787
+ */
3788
+ shouldAlertStuckPosition(token) {
3789
+ const alertKey = `__stuck_alert_${token.toLowerCase()}`;
3790
+ const lastAlert = this.store.get(alertKey) || 0;
3791
+ const hoursSinceAlert = (Date.now() - lastAlert) / (1e3 * 60 * 60);
3792
+ if (hoursSinceAlert >= 24) {
3793
+ this.store.set(alertKey, Date.now());
3794
+ return true;
3795
+ }
3796
+ return false;
3797
+ }
3798
+ // ============================================================
3637
3799
  // RISK STATE PERSISTENCE
3638
3800
  // ============================================================
3639
3801
  /** Load persisted risk state */
@@ -4132,7 +4294,9 @@ function validateConfig(config) {
4132
4294
  throw new Error("Endpoint required for custom LLM provider");
4133
4295
  }
4134
4296
  if (!config.agentId || Number(config.agentId) <= 0) {
4135
- throw new Error("Valid agent ID required");
4297
+ throw new Error(
4298
+ 'Valid agent ID required in agent-config.json.\n\n Register your agent at https://exagent.io first:\n 1. Connect your wallet\n 2. Click "Launch Agent" to register\n 3. Copy your agent ID into agent-config.json as "agentId"\n'
4299
+ );
4136
4300
  }
4137
4301
  }
4138
4302
  var DEFAULT_RPC_URL = "https://mainnet.base.org";
@@ -5391,7 +5555,7 @@ var HyperliquidWebSocket = class {
5391
5555
  var import_viem5 = require("viem");
5392
5556
  var import_chains3 = require("viem/chains");
5393
5557
  var import_accounts2 = require("viem/accounts");
5394
- var ROUTER_ADDRESS = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
5558
+ var ROUTER_ADDRESS = "0x20feB3054750970773Fe7370c391732EE0559743";
5395
5559
  var ROUTER_ABI = [
5396
5560
  {
5397
5561
  type: "function",
@@ -6451,7 +6615,7 @@ var PredictionPositionManager = class {
6451
6615
  var import_viem8 = require("viem");
6452
6616
  var import_chains5 = require("viem/chains");
6453
6617
  var import_accounts4 = require("viem/accounts");
6454
- var ROUTER_ADDRESS2 = "0x1BCFa13f677fDCf697D8b7d5120f544817F1de1A";
6618
+ var ROUTER_ADDRESS2 = "0x20feB3054750970773Fe7370c391732EE0559743";
6455
6619
  var ROUTER_ABI2 = [
6456
6620
  {
6457
6621
  type: "function",
@@ -7761,7 +7925,7 @@ function loadSecureEnv(basePath, passphrase) {
7761
7925
  }
7762
7926
 
7763
7927
  // src/index.ts
7764
- var AGENT_VERSION = "0.1.41";
7928
+ var AGENT_VERSION = "0.1.43";
7765
7929
 
7766
7930
  // src/relay.ts
7767
7931
  var RelayClient = class {
@@ -8129,7 +8293,18 @@ var AgentRuntime = class {
8129
8293
  console.log(`Wallet: ${this.client.address}`);
8130
8294
  const agent = await this.client.registry.getAgent(BigInt(this.config.agentId));
8131
8295
  if (!agent) {
8132
- throw new Error(`Agent ID ${this.config.agentId} not found on-chain. Please register first.`);
8296
+ throw new Error(
8297
+ `Agent ID ${this.config.agentId} not found on-chain.
8298
+
8299
+ Register your agent at https://exagent.io first:
8300
+ 1. Connect your wallet at https://exagent.io
8301
+ 2. Click "Launch Agent" to register (sets name, risk universe, limits)
8302
+ 3. Copy your agent ID into agent-config.json
8303
+ 4. Run your agent again
8304
+
8305
+ The website handles the full registration flow including risk parameters.
8306
+ Your agent ID will appear on your agent's profile page after registration.`
8307
+ );
8133
8308
  }
8134
8309
  console.log(`Agent verified: ${agent.name}`);
8135
8310
  await this.ensureWalletLinked();
@@ -8653,7 +8828,7 @@ var AgentRuntime = class {
8653
8828
  try {
8654
8829
  const tokens = this.getTokensToTrack();
8655
8830
  const initData = await this.marketData.fetchMarketData(this.client.address, tokens);
8656
- this.positionTracker.syncBalances(initData.balances, initData.prices);
8831
+ void this.positionTracker.syncBalances(initData.balances, initData.prices);
8657
8832
  this.lastPortfolioValue = initData.portfolioValue;
8658
8833
  this.lastPrices = initData.prices;
8659
8834
  const nativeEthBal = initData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
@@ -8814,6 +8989,9 @@ var AgentRuntime = class {
8814
8989
  }
8815
8990
  case "create_vault": {
8816
8991
  const result = await this.createVault();
8992
+ if (!result.success) {
8993
+ console.warn(`[vault] create_vault failed: ${result.error}`);
8994
+ }
8817
8995
  this.relay?.sendCommandResult(
8818
8996
  cmd.id,
8819
8997
  result.success,
@@ -9505,7 +9683,31 @@ var AgentRuntime = class {
9505
9683
  this.lastPrices = marketData.prices;
9506
9684
  const nativeEthBal = marketData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
9507
9685
  this.lastEthBalance = (Number(nativeEthBal) / 1e18).toFixed(6);
9508
- this.positionTracker.syncBalances(marketData.balances, marketData.prices);
9686
+ const inferredExits = this.positionTracker.syncBalances(marketData.balances, marketData.prices);
9687
+ for (const exit of inferredExits) {
9688
+ const pnlSign = exit.realizedPnL >= 0 ? "+" : "";
9689
+ this.relay?.sendMessage(
9690
+ "position_inferred_exit",
9691
+ exit.realizedPnL >= 0 ? "info" : "warning",
9692
+ `Inferred Exit: ${exit.symbol}`,
9693
+ `Detected manual sell of ${exit.amountExited.toFixed(4)} ${exit.symbol} outside the ExagentRouter.
9694
+
9695
+ Exit value: $${exit.exitValueUSD.toFixed(2)}
9696
+ Cost basis: $${exit.costBasis.toFixed(2)}
9697
+ Realized PnL: ${pnlSign}$${exit.realizedPnL.toFixed(2)}
9698
+
9699
+ This exit has been recorded in your trade history and counts toward your total PnL.`,
9700
+ {
9701
+ token: exit.token,
9702
+ symbol: exit.symbol,
9703
+ amountExited: exit.amountExited,
9704
+ exitValueUSD: exit.exitValueUSD,
9705
+ realizedPnL: exit.realizedPnL,
9706
+ costBasis: exit.costBasis,
9707
+ inferred: true
9708
+ }
9709
+ );
9710
+ }
9509
9711
  if (!isPaper) {
9510
9712
  const fundsOk = this.checkFundsLow(marketData);
9511
9713
  if (!fundsOk) {
@@ -9598,7 +9800,7 @@ var AgentRuntime = class {
9598
9800
  console.log(` [PAPER] Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
9599
9801
  }
9600
9802
  this.paperPortfolio.recordEquityPoint(marketData.prices);
9601
- this.positionTracker.syncBalances(this.paperPortfolio.getBalances(), marketData.prices);
9803
+ void this.positionTracker.syncBalances(this.paperPortfolio.getBalances(), marketData.prices);
9602
9804
  this.lastPortfolioValue = postValue;
9603
9805
  this.paperPortfolio.save();
9604
9806
  } else {
@@ -9629,6 +9831,9 @@ var AgentRuntime = class {
9629
9831
  tokenOut: result.signal.tokenOut
9630
9832
  }
9631
9833
  );
9834
+ if (result.signal.action === "sell") {
9835
+ this.positionTracker.clearSellFailure(result.signal.tokenIn);
9836
+ }
9632
9837
  } else {
9633
9838
  console.warn(`Trade failed: ${result.error}`);
9634
9839
  this.relay?.sendMessage(
@@ -9638,6 +9843,41 @@ var AgentRuntime = class {
9638
9843
  result.error || "Unknown error",
9639
9844
  { action: result.signal.action }
9640
9845
  );
9846
+ if (result.signal.action === "sell") {
9847
+ const failedToken = result.signal.tokenIn.toLowerCase();
9848
+ this.positionTracker.recordSellFailure(failedToken, result.error || "Unknown error");
9849
+ }
9850
+ }
9851
+ }
9852
+ const stuckPositions = this.positionTracker.getStuckPositions(marketData.prices);
9853
+ for (const stuck of stuckPositions) {
9854
+ if (this.positionTracker.shouldAlertStuckPosition(stuck.token)) {
9855
+ const holdingDays = Math.floor(stuck.holdingDurationMs / (1e3 * 60 * 60 * 24));
9856
+ this.relay?.sendMessage(
9857
+ "position_stuck",
9858
+ "warning",
9859
+ `Position Stuck: ${stuck.symbol}`,
9860
+ `Unable to sell ${stuck.symbol} after ${stuck.consecutiveFailures} attempts. This position has been held for ${holdingDays} days with ~$${stuck.estimatedValueUSD.toFixed(2)} value.
9861
+
9862
+ Last error: ${stuck.lastFailureReason}
9863
+
9864
+ MANUAL EXIT REQUIRED:
9865
+ 1. Export your wallet key: npx @exagent/agent export-key
9866
+ 2. Import into MetaMask or Rabby
9867
+ 3. Go to Uniswap/Aerodrome and swap ${stuck.symbol} \u2192 ETH directly
9868
+ 4. Use high slippage (10-20%) for illiquid tokens
9869
+
9870
+ Note: Manual sells won't be tracked in your agent's PnL. The position will be removed from tracking once your balance is synced.
9871
+
9872
+ See: https://exagent.io/docs/guides/manual-exit`,
9873
+ {
9874
+ token: stuck.token,
9875
+ symbol: stuck.symbol,
9876
+ consecutiveFailures: stuck.consecutiveFailures,
9877
+ estimatedValueUSD: stuck.estimatedValueUSD,
9878
+ holdingDurationMs: stuck.holdingDurationMs
9879
+ }
9880
+ );
9641
9881
  }
9642
9882
  }
9643
9883
  for (const result of results) {
@@ -9663,7 +9903,7 @@ var AgentRuntime = class {
9663
9903
  if (marketPnL !== 0) {
9664
9904
  console.log(`Cycle PnL: $${marketPnL.toFixed(2)} (market), -$${totalFeesUSD.toFixed(2)} (fees)`);
9665
9905
  }
9666
- this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
9906
+ void this.positionTracker.syncBalances(postTradeData.balances, postTradeData.prices);
9667
9907
  this.lastPortfolioValue = postTradeData.portfolioValue;
9668
9908
  const postNativeEthBal = postTradeData.balances[NATIVE_ETH.toLowerCase()] || BigInt(0);
9669
9909
  this.lastEthBalance = (Number(postNativeEthBal) / 1e18).toFixed(6);
package/dist/cli.mjs CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  results_exports,
30
30
  saveSessionResult,
31
31
  validateConfig
32
- } from "./chunk-34JBZQO6.mjs";
32
+ } from "./chunk-FOYVWN7Q.mjs";
33
33
 
34
34
  // src/backtest/data-loader.ts
35
35
  import * as fs from "fs";
package/dist/index.d.mts CHANGED
@@ -9,6 +9,24 @@ interface RiskState {
9
9
  dailyFees: number;
10
10
  lastResetDate: string;
11
11
  }
12
+ /** Stuck position detection result */
13
+ interface StuckPosition {
14
+ token: string;
15
+ symbol: string;
16
+ consecutiveFailures: number;
17
+ lastFailureReason: string;
18
+ holdingDurationMs: number;
19
+ estimatedValueUSD: number;
20
+ }
21
+ /** Result of an inferred exit detected during balance sync */
22
+ interface InferredExit {
23
+ token: string;
24
+ symbol: string;
25
+ amountExited: number;
26
+ exitValueUSD: number;
27
+ realizedPnL: number;
28
+ costBasis: number;
29
+ }
12
30
  /**
13
31
  * PositionTracker — automatic position tracking with persistence.
14
32
  *
@@ -25,6 +43,7 @@ declare class PositionTracker {
25
43
  private store;
26
44
  private positions;
27
45
  private tradeHistory;
46
+ private sellFailures;
28
47
  private maxTradeHistory;
29
48
  constructor(store: StrategyStore, options?: {
30
49
  maxTradeHistory?: number;
@@ -55,9 +74,20 @@ declare class PositionTracker {
55
74
  private handleSell;
56
75
  /**
57
76
  * Sync tracked positions with on-chain balances.
58
- * Updates currentAmount, detects new tokens (airdrops), removes zeroed positions.
77
+ * Updates currentAmount, detects new tokens (airdrops), and handles position exits.
78
+ *
79
+ * CRITICAL: When a tracked position's balance decreases without a corresponding
80
+ * sell trade, we infer a manual exit and record it with realized PnL calculated
81
+ * at current market price. This prevents PnL manipulation via off-router sells.
82
+ *
83
+ * Returns array of inferred exits for the runtime to report to Command Center.
59
84
  */
60
- syncBalances(balances: Record<string, bigint>, prices: Record<string, number>): void;
85
+ syncBalances(balances: Record<string, bigint>, prices: Record<string, number>): InferredExit[];
86
+ /**
87
+ * Record an inferred exit in trade history.
88
+ * These are exits detected from balance changes, not from tracked sell executions.
89
+ */
90
+ private recordInferredExit;
61
91
  /** Get all tracked positions (backfills missing symbols from resolver) */
62
92
  getPositions(): TrackedPosition[];
63
93
  /** Get a single position by token address */
@@ -68,6 +98,31 @@ declare class PositionTracker {
68
98
  getUnrealizedPnL(prices: Record<string, number>): Record<string, number>;
69
99
  /** Get total unrealized PnL across all positions */
70
100
  getTotalUnrealizedPnL(prices: Record<string, number>): number;
101
+ /**
102
+ * Record a sell failure for a token. Called when a sell attempt fails
103
+ * due to liquidity issues, route simulation failure, or excessive price impact.
104
+ */
105
+ recordSellFailure(token: string, reason: string): void;
106
+ /**
107
+ * Clear sell failure tracking for a token (called after successful sell).
108
+ */
109
+ clearSellFailure(token: string): void;
110
+ /**
111
+ * Detect positions that are "stuck" — we've tried and failed to sell them
112
+ * multiple times, likely due to insufficient liquidity.
113
+ *
114
+ * Returns positions that have failed to sell >= STUCK_FAILURE_THRESHOLD times.
115
+ */
116
+ getStuckPositions(prices: Record<string, number>): StuckPosition[];
117
+ /**
118
+ * Check if a specific position is stuck.
119
+ */
120
+ isPositionStuck(token: string): boolean;
121
+ /**
122
+ * Check if we should alert about a stuck position (rate-limited to once per 24h).
123
+ * Returns true if we should alert, and marks the alert as sent.
124
+ */
125
+ shouldAlertStuckPosition(token: string): boolean;
71
126
  /** Load persisted risk state */
72
127
  getRiskState(): RiskState;
73
128
  /** Save risk state to persistent store */
@@ -783,6 +838,13 @@ interface TradeRecord {
783
838
  /** Realized PnL in USD (only for sells) */
784
839
  realizedPnL?: number;
785
840
  success: boolean;
841
+ /**
842
+ * True if this trade was inferred from balance changes, not from a tracked execution.
843
+ * Inferred exits occur when a position's balance decreases without a corresponding
844
+ * sell trade — typically from manual swaps outside the ExagentRouter.
845
+ * PnL is calculated at current market price and counts toward total realized PnL.
846
+ */
847
+ inferred?: boolean;
786
848
  }
787
849
  /**
788
850
  * Compact position summary for relay heartbeats.
@@ -936,7 +998,7 @@ interface RelayCommand {
936
998
  type: CommandType;
937
999
  params?: Record<string, unknown>;
938
1000
  }
939
- type MessageType = 'trade_executed' | 'trade_failed' | 'paper_trade_executed' | 'paper_trade_failed' | 'perp_fill' | 'perp_liquidation_warning' | 'perp_funding' | 'prediction_fill' | 'prediction_market_resolved' | 'prediction_vault_created' | 'prediction_vault_deposit' | 'prediction_vault_withdraw' | 'funds_low' | 'risk_limit_hit' | 'vault_created' | 'config_updated' | 'llm_error' | 'command_result' | 'system';
1001
+ type MessageType = 'trade_executed' | 'trade_failed' | 'paper_trade_executed' | 'paper_trade_failed' | 'perp_fill' | 'perp_liquidation_warning' | 'perp_funding' | 'prediction_fill' | 'prediction_market_resolved' | 'prediction_vault_created' | 'prediction_vault_deposit' | 'prediction_vault_withdraw' | 'funds_low' | 'risk_limit_hit' | 'position_stuck' | 'position_inferred_exit' | 'vault_created' | 'config_updated' | 'llm_error' | 'command_result' | 'system';
940
1002
  type MessageLevel = 'info' | 'warning' | 'error' | 'success';
941
1003
  interface AgentStatusPayload {
942
1004
  mode: AgentMode;
@@ -3283,6 +3345,6 @@ declare function decryptEnvFile(encPath: string, passphrase: string): Record<str
3283
3345
  declare function loadSecureEnv(basePath: string, passphrase?: string): boolean;
3284
3346
 
3285
3347
  /** @exagent/agent package version — update alongside package.json */
3286
- declare const AGENT_VERSION = "0.1.41";
3348
+ declare const AGENT_VERSION = "0.1.43";
3287
3349
 
3288
- export { AGENT_VERSION, type AccountSummary, type AgentConfig, AgentConfigSchema, type AgentMode, AgentRuntime, type AgentStatusPayload, AnthropicAdapter, BaseLLMAdapter, type BridgeResult, type BridgeStep, type CommandType, DeepSeekAdapter, FileStore, type FillCallback, type FundingCallback, type FundingPayment, GoogleAdapter, GroqAdapter, HYPERLIQUID_DOMAIN, HyperliquidClient, HyperliquidSigner, HyperliquidWebSocket, type LLMAdapter, type LLMConfig, LLMConfigSchema, type LLMMessage, type LLMMetadata, type LLMProvider, LLMProviderSchema, type LLMResponse, type LiquidationCallback, type LocalPosition, MARKET_CATEGORIES, MarketBrowser, type MarketCategory, type MarketData, MarketDataService, type MessageLevel, type MessageType, MistralAdapter, OllamaAdapter, type OnboardingStatus, OpenAIAdapter, OrderManager, type OrderResult, type PerpAction, type PerpConfig$1 as PerpConfig, PerpConfigSchema, type PerpFill, type PerpMarketData, PerpOnboarding, type PerpPosition, type PerpStrategyFunction, PerpTradeRecorder, type PerpTradeSignal, type PerpConfig as PerpTradingConfig, PolymarketClient, PositionManager, type PositionSummary, PositionTracker, type PredictionAccountSummary, type PredictionAction, type PredictionConfig$1 as PredictionConfig, PredictionConfigSchema, type PredictionFill, PredictionFunding, type PredictionFundingConfig, type PredictionMarket, PredictionOrderManager, type PredictionPosition, PredictionPositionManager, type PredictionStrategyFunction, PredictionTradeRecorder, type PredictionTradeSignal, type PredictionConfig as PredictionTradingConfig, type RecordPerpTradeParams, type RecordPredictionTradeParams, RelayClient, type RelayCommand, type RelayConfig$1 as RelayConfig, RelayConfigSchema, RiskManager, type RiskState, type RiskUniverse, RiskUniverseSchema, type RuntimeConfig, STRATEGY_TEMPLATES, type StrategyContext, type StrategyFunction, type StrategyStore, type StrategyTemplate, TogetherAdapter, type TrackedPosition, TradeExecutor, type TradeRecord, type TradeSignal, type TradingConfig, TradingConfigSchema, type VaultConfig, VaultConfigSchema, VaultManager, type VaultManagerConfig, type VaultPolicy, VaultPolicySchema, type VaultStatus, calculatePredictionFee, createLLMAdapter, createSampleConfig, decodePredictionInstrument, decryptEnvFile, encodePredictionInstrument, encryptEnvFile, fillHashToBytes32, fillOidToBytes32, getAllStrategyTemplates, getNextNonce, getStrategyTemplate, loadConfig, loadSecureEnv, loadStrategy, orderIdToBytes32, tradeIdToBytes32, validateConfig, validateStrategy };
3350
+ export { AGENT_VERSION, type AccountSummary, type AgentConfig, AgentConfigSchema, type AgentMode, AgentRuntime, type AgentStatusPayload, AnthropicAdapter, BaseLLMAdapter, type BridgeResult, type BridgeStep, type CommandType, DeepSeekAdapter, FileStore, type FillCallback, type FundingCallback, type FundingPayment, GoogleAdapter, GroqAdapter, HYPERLIQUID_DOMAIN, HyperliquidClient, HyperliquidSigner, HyperliquidWebSocket, type InferredExit, type LLMAdapter, type LLMConfig, LLMConfigSchema, type LLMMessage, type LLMMetadata, type LLMProvider, LLMProviderSchema, type LLMResponse, type LiquidationCallback, type LocalPosition, MARKET_CATEGORIES, MarketBrowser, type MarketCategory, type MarketData, MarketDataService, type MessageLevel, type MessageType, MistralAdapter, OllamaAdapter, type OnboardingStatus, OpenAIAdapter, OrderManager, type OrderResult, type PerpAction, type PerpConfig$1 as PerpConfig, PerpConfigSchema, type PerpFill, type PerpMarketData, PerpOnboarding, type PerpPosition, type PerpStrategyFunction, PerpTradeRecorder, type PerpTradeSignal, type PerpConfig as PerpTradingConfig, PolymarketClient, PositionManager, type PositionSummary, PositionTracker, type PredictionAccountSummary, type PredictionAction, type PredictionConfig$1 as PredictionConfig, PredictionConfigSchema, type PredictionFill, PredictionFunding, type PredictionFundingConfig, type PredictionMarket, PredictionOrderManager, type PredictionPosition, PredictionPositionManager, type PredictionStrategyFunction, PredictionTradeRecorder, type PredictionTradeSignal, type PredictionConfig as PredictionTradingConfig, type RecordPerpTradeParams, type RecordPredictionTradeParams, RelayClient, type RelayCommand, type RelayConfig$1 as RelayConfig, RelayConfigSchema, RiskManager, type RiskState, type RiskUniverse, RiskUniverseSchema, type RuntimeConfig, STRATEGY_TEMPLATES, type StrategyContext, type StrategyFunction, type StrategyStore, type StrategyTemplate, type StuckPosition, TogetherAdapter, type TrackedPosition, TradeExecutor, type TradeRecord, type TradeSignal, type TradingConfig, TradingConfigSchema, type VaultConfig, VaultConfigSchema, VaultManager, type VaultManagerConfig, type VaultPolicy, VaultPolicySchema, type VaultStatus, calculatePredictionFee, createLLMAdapter, createSampleConfig, decodePredictionInstrument, decryptEnvFile, encodePredictionInstrument, encryptEnvFile, fillHashToBytes32, fillOidToBytes32, getAllStrategyTemplates, getNextNonce, getStrategyTemplate, loadConfig, loadSecureEnv, loadStrategy, orderIdToBytes32, tradeIdToBytes32, validateConfig, validateStrategy };