@agentlayer.tech/wallet 0.1.43 → 0.1.47

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.
Files changed (37) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.openclaw/extensions/agent-wallet/index.ts +99 -0
  3. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +6 -1
  4. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  5. package/CHANGELOG.md +79 -0
  6. package/README.md +16 -2
  7. package/VERSION +1 -1
  8. package/agent-wallet/agent_wallet/__init__.py +1 -1
  9. package/agent-wallet/agent_wallet/openclaw_adapter.py +686 -0
  10. package/agent-wallet/agent_wallet/openclaw_cli.py +22 -0
  11. package/agent-wallet/agent_wallet/providers/jupiter.py +5 -0
  12. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +6 -0
  13. package/agent-wallet/agent_wallet/telemetry.py +306 -0
  14. package/agent-wallet/agent_wallet/wallet_layer/base.py +79 -0
  15. package/agent-wallet/agent_wallet/wallet_layer/solana.py +14 -0
  16. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +404 -0
  17. package/agent-wallet/openclaw.plugin.json +1 -1
  18. package/agent-wallet/pyproject.toml +1 -1
  19. package/agent-wallet/scripts/install_agent_wallet.py +33 -8
  20. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  21. package/claude-code/plugins/agent-wallet/README.md +30 -1
  22. package/claude-code/plugins/agent-wallet/commands/wallet-setup.md +24 -0
  23. package/claude-code/plugins/agent-wallet/hooks/hooks.json +16 -0
  24. package/claude-code/plugins/agent-wallet/scripts/bootstrap_backend.sh +136 -0
  25. package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +6 -1
  26. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  27. package/codex/plugins/agent-wallet/scripts/run_mcp.sh +5 -0
  28. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  29. package/hermes/plugins/agent_wallet/tools.py +3 -0
  30. package/package.json +2 -1
  31. package/wdk-btc-wallet/package.json +1 -1
  32. package/wdk-evm-wallet/README.md +31 -0
  33. package/wdk-evm-wallet/package-lock.json +268 -64
  34. package/wdk-evm-wallet/package.json +4 -1
  35. package/wdk-evm-wallet/src/config.js +2 -0
  36. package/wdk-evm-wallet/src/server.js +66 -0
  37. package/wdk-evm-wallet/src/wdk_evm_wallet.js +2725 -939
@@ -11,6 +11,12 @@ from typing import Any
11
11
 
12
12
  from agent_wallet.wallet_layer.base import WalletBackendError
13
13
 
14
+ try: # telemetry is optional and must never break the CLI
15
+ from agent_wallet.telemetry import record as _telemetry_record
16
+ except Exception: # pragma: no cover - defensive
17
+ def _telemetry_record(*_args: Any, **_kwargs: Any) -> None:
18
+ return None
19
+
14
20
 
15
21
  def _parse_bool(value: Any) -> str:
16
22
  if value is None:
@@ -610,6 +616,15 @@ def main() -> int:
610
616
  )
611
617
  )
612
618
  except Exception as exc:
619
+ # Anonymous adoption telemetry for tool invocations only (never for
620
+ # onboard/wallet-create/unlock/import — those touch secrets). Records
621
+ # just the tool name + backend family + failure flag; never raises.
622
+ if getattr(args, "command", "") == "invoke":
623
+ _telemetry_record(
624
+ getattr(args, "tool", ""),
625
+ backend=str(locals().get("config", {}).get("backend", "") or ""),
626
+ ok=False,
627
+ )
613
628
  error_payload: dict[str, Any] = {"ok": False, "error": str(exc)}
614
629
  if isinstance(exc, WalletBackendError):
615
630
  if exc.code:
@@ -619,6 +634,13 @@ def main() -> int:
619
634
  print(json.dumps(error_payload), file=sys.stderr)
620
635
  return 1
621
636
 
637
+ if getattr(args, "command", "") == "invoke":
638
+ _telemetry_record(
639
+ getattr(args, "tool", ""),
640
+ backend=str(config.get("backend", "") or ""),
641
+ ok=True,
642
+ )
643
+
622
644
  print(json.dumps(payload))
623
645
  return 0
624
646
 
@@ -117,6 +117,7 @@ async def fetch_quote(
117
117
  restrict_intermediate_tokens: bool = True,
118
118
  only_direct_routes: bool = False,
119
119
  swap_mode: str = "ExactIn",
120
+ exclude_dexes: list[str] | None = None,
120
121
  ) -> dict[str, Any]:
121
122
  """Fetch a Jupiter quote for an exact-in swap.
122
123
 
@@ -133,6 +134,7 @@ async def fetch_quote(
133
134
  restrict_intermediate_tokens=restrict_intermediate_tokens,
134
135
  only_direct_routes=only_direct_routes,
135
136
  swap_mode=swap_mode,
137
+ exclude_dexes=exclude_dexes,
136
138
  )
137
139
  except ProviderError as exc:
138
140
  error_msg = str(exc).lower()
@@ -168,6 +170,7 @@ async def _fetch_quote_direct(
168
170
  restrict_intermediate_tokens: bool = True,
169
171
  only_direct_routes: bool = False,
170
172
  swap_mode: str = "ExactIn",
173
+ exclude_dexes: list[str] | None = None,
171
174
  ) -> dict[str, Any]:
172
175
  """Fetch a Jupiter quote directly from Jupiter API."""
173
176
  client = get_client()
@@ -180,6 +183,8 @@ async def _fetch_quote_direct(
180
183
  "restrictIntermediateTokens": str(restrict_intermediate_tokens).lower(),
181
184
  "onlyDirectRoutes": str(only_direct_routes).lower(),
182
185
  }
186
+ if exclude_dexes:
187
+ params["excludeDexes"] = ",".join(str(d).strip() for d in exclude_dexes if str(d).strip())
183
188
  response = await client.get(
184
189
  f"{settings.jupiter_api_base_url.rstrip('/')}/quote",
185
190
  params=params,
@@ -18,6 +18,12 @@ LONG_RUNNING_POST_PATHS = {
18
18
  "/v1/evm/aave/withdraw/send",
19
19
  "/v1/evm/aave/borrow/send",
20
20
  "/v1/evm/aave/repay/send",
21
+ "/v1/evm/morpho/vault/supply/send",
22
+ "/v1/evm/morpho/vault/withdraw/send",
23
+ "/v1/evm/morpho/market/supply_collateral/send",
24
+ "/v1/evm/morpho/market/borrow/send",
25
+ "/v1/evm/morpho/market/repay/send",
26
+ "/v1/evm/morpho/market/withdraw_collateral/send",
21
27
  "/v1/evm/lido/stake_eth_for_wsteth/send",
22
28
  "/v1/evm/lido/wrap_steth/send",
23
29
  "/v1/evm/lido/unwrap_wsteth/send",
@@ -0,0 +1,306 @@
1
+ """Anonymous, privacy-first adoption telemetry for the AgentLayer wallet.
2
+
3
+ Why this exists: we want to know how many people use the wallet and through which
4
+ host (Claude Code / Codex / Hermes / OpenClaw) — nothing more. It deliberately
5
+ records NO PII: no wallet addresses, balances, amounts, tx hashes, tool
6
+ arguments, or secrets. Only a random local install id, the host name, the
7
+ invoked tool's registered name, the backend family, the plugin version, and a
8
+ success flag.
9
+
10
+ Design for a short-lived CLI subprocess:
11
+ - `record()` appends one JSON line to a local spool file. This is instant and
12
+ durable: a single small append is atomic on POSIX, and the event survives
13
+ even if this process exits before any network call completes.
14
+ - A throttled, best-effort flush claims the spool (atomic rename), POSTs each
15
+ event to the provider-gateway, and re-spools anything that failed. Because
16
+ durability lives in the spool, a killed flush never loses data — the next
17
+ invocation (or a longer-lived process) ships it.
18
+
19
+ Everything here swallows its own errors. Telemetry must never slow down or break
20
+ a wallet operation. Users opt out with AGENT_WALLET_NO_TELEMETRY=1.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import os
27
+ import subprocess
28
+ import sys
29
+ import time
30
+ import urllib.request
31
+ import uuid
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from . import __version__
36
+ from .config import DEFAULT_PROVIDER_GATEWAY_URL, resolve_openclaw_home
37
+
38
+ ALLOWED_HOSTS = {"claude-code", "codex", "hermes", "openclaw"}
39
+
40
+ SPOOL_NAME = "telemetry_spool.jsonl"
41
+ ID_NAME = "telemetry_id"
42
+ LAST_FLUSH_NAME = "telemetry_last_flush"
43
+
44
+ # Keep the spool bounded if the gateway is unreachable for a long time.
45
+ MAX_SPOOL_LINES = 500
46
+ # Don't attempt a network flush more than this often (seconds), unless the spool
47
+ # has grown past the soft cap below.
48
+ FLUSH_THROTTLE_SECONDS = 20
49
+ FLUSH_FORCE_LINES = 50
50
+ # Tight network bounds: telemetry never blocks meaningfully.
51
+ HTTP_TIMEOUT_SECONDS = 1.5
52
+ MAX_EVENTS_PER_FLUSH = 100
53
+
54
+
55
+ def _enabled() -> bool:
56
+ raw = os.getenv("AGENT_WALLET_NO_TELEMETRY", "").strip().lower()
57
+ return raw not in ("1", "true", "yes", "on")
58
+
59
+
60
+ def _home() -> Path:
61
+ return resolve_openclaw_home()
62
+
63
+
64
+ def _gateway_url() -> str:
65
+ url = os.getenv("PROVIDER_GATEWAY_URL", "").strip() or DEFAULT_PROVIDER_GATEWAY_URL
66
+ return url.rstrip("/")
67
+
68
+
69
+ def _host() -> str:
70
+ host = os.getenv("AGENT_WALLET_HOST", "").strip().lower()
71
+ return host if host in ALLOWED_HOSTS else "unknown"
72
+
73
+
74
+ def _install_id() -> str:
75
+ """Stable, random, non-identifying id for this install. Created once."""
76
+ path = _home() / ID_NAME
77
+ try:
78
+ existing = path.read_text(encoding="utf-8").strip()
79
+ if existing:
80
+ return existing
81
+ except OSError:
82
+ pass
83
+ new_id = uuid.uuid4().hex
84
+ try:
85
+ path.parent.mkdir(parents=True, exist_ok=True)
86
+ path.write_text(new_id, encoding="utf-8")
87
+ except OSError:
88
+ pass
89
+ return new_id
90
+
91
+
92
+ def record(
93
+ tool: str,
94
+ *,
95
+ backend: str = "",
96
+ ok: bool = True,
97
+ event: str = "tool_invoke",
98
+ ) -> None:
99
+ """Append an anonymous event to the spool and best-effort flush. Never raises."""
100
+ try:
101
+ if not _enabled():
102
+ return
103
+ payload = {
104
+ "event": event,
105
+ "install_id": _install_id(),
106
+ "host": _host(),
107
+ "tool": tool or "",
108
+ "backend": backend or "",
109
+ "plugin_version": __version__,
110
+ "ok": bool(ok),
111
+ "ts": int(time.time()),
112
+ }
113
+ _append_spool(payload)
114
+ _maybe_spawn_flush()
115
+ except Exception:
116
+ # Telemetry is never allowed to affect the wallet call.
117
+ pass
118
+
119
+
120
+ # --- spool I/O --------------------------------------------------------------
121
+
122
+
123
+ def _spool_path() -> Path:
124
+ return _home() / SPOOL_NAME
125
+
126
+
127
+ def _append_spool(payload: dict[str, Any]) -> None:
128
+ path = _spool_path()
129
+ line = json.dumps(payload, separators=(",", ":")) + "\n"
130
+ path.parent.mkdir(parents=True, exist_ok=True)
131
+ # A single write of a short line is atomic across processes on POSIX.
132
+ with open(path, "a", encoding="utf-8") as fh:
133
+ fh.write(line)
134
+
135
+
136
+ def _spool_line_count() -> int:
137
+ try:
138
+ with open(_spool_path(), "r", encoding="utf-8") as fh:
139
+ return sum(1 for _ in fh)
140
+ except OSError:
141
+ return 0
142
+
143
+
144
+ def _should_flush_now() -> bool:
145
+ count = _spool_line_count()
146
+ if count == 0:
147
+ return False
148
+ if count >= FLUSH_FORCE_LINES:
149
+ return True
150
+ last_path = _home() / LAST_FLUSH_NAME
151
+ try:
152
+ last = float(last_path.read_text(encoding="utf-8").strip() or "0")
153
+ except (OSError, ValueError):
154
+ last = 0.0
155
+ return (time.time() - last) >= FLUSH_THROTTLE_SECONDS
156
+
157
+
158
+ def _mark_flush_attempt() -> None:
159
+ try:
160
+ (_home() / LAST_FLUSH_NAME).write_text(str(time.time()), encoding="utf-8")
161
+ except OSError:
162
+ pass
163
+
164
+
165
+ def _maybe_spawn_flush() -> None:
166
+ """Trigger a flush in a DETACHED subprocess that outlives this process.
167
+
168
+ A per-tool CLI invocation exits microseconds after recording, so a thread
169
+ would be killed mid-flush. A detached child (its own session, no wait) runs
170
+ the network call on its own time and re-spools failures reliably. Throttling
171
+ keeps this to roughly one spawn per FLUSH_THROTTLE_SECONDS, so most calls
172
+ just append to the spool and return.
173
+ """
174
+ if not _should_flush_now():
175
+ return
176
+ _mark_flush_attempt()
177
+ try:
178
+ subprocess.Popen(
179
+ [sys.executable, "-m", "agent_wallet.telemetry", "--flush"],
180
+ stdout=subprocess.DEVNULL,
181
+ stderr=subprocess.DEVNULL,
182
+ stdin=subprocess.DEVNULL,
183
+ start_new_session=True, # detach so parent exit doesn't kill it
184
+ )
185
+ except Exception:
186
+ pass
187
+
188
+
189
+ def _pid_alive(pid: int) -> bool:
190
+ try:
191
+ os.kill(pid, 0)
192
+ except ProcessLookupError:
193
+ return False
194
+ except PermissionError:
195
+ return True # exists but owned by another user
196
+ except OSError:
197
+ return True
198
+ return True
199
+
200
+
201
+ def _claim_suffix_pid(path: Path) -> int | None:
202
+ # ...telemetry_spool.jsonl.flushing.<pid>
203
+ try:
204
+ return int(path.name.rsplit(".flushing.", 1)[1])
205
+ except (IndexError, ValueError):
206
+ return None
207
+
208
+
209
+ def _flush() -> None:
210
+ """Claim the spool atomically, adopt dead-PID orphans, POST, re-spool failures."""
211
+ spool = _spool_path()
212
+ home = _home()
213
+ claim = spool.with_suffix(spool.suffix + f".flushing.{os.getpid()}")
214
+
215
+ claims: list[Path] = []
216
+ try:
217
+ os.rename(spool, claim) # atomic; only one process wins the live spool
218
+ claims.append(claim)
219
+ except OSError:
220
+ pass # nothing to flush, or another process claimed it
221
+
222
+ # Recover orphans left by a flush that died mid-run (crash/exit after the
223
+ # rename but before re-spooling). Only adopt claims whose PID is gone so we
224
+ # never steal a batch a live sibling is still working.
225
+ try:
226
+ for orphan in home.glob(SPOOL_NAME + ".flushing.*"):
227
+ if orphan == claim:
228
+ continue
229
+ pid = _claim_suffix_pid(orphan)
230
+ if pid is None or not _pid_alive(pid):
231
+ claims.append(orphan)
232
+ except OSError:
233
+ pass
234
+
235
+ if not claims:
236
+ return
237
+
238
+ lines: list[str] = []
239
+ for c in claims:
240
+ try:
241
+ lines.extend(c.read_text(encoding="utf-8").splitlines())
242
+ except OSError:
243
+ pass
244
+
245
+ # Bound a long backlog (gateway down for a while): keep the most recent.
246
+ if len(lines) > MAX_SPOOL_LINES:
247
+ lines = lines[-MAX_SPOOL_LINES:]
248
+
249
+ url = _gateway_url() + "/v1/telemetry"
250
+ failed: list[str] = []
251
+ sent = 0
252
+ for line in lines:
253
+ line = line.strip()
254
+ if not line:
255
+ continue
256
+ if sent >= MAX_EVENTS_PER_FLUSH:
257
+ failed.append(line) # defer the rest to the next flush
258
+ continue
259
+ if _post(url, line):
260
+ sent += 1
261
+ else:
262
+ failed.append(line)
263
+
264
+ # Re-spool failures BEFORE removing claims: favors at-least-once delivery
265
+ # (a crash here re-sends, never drops). Successes are gone from the claims.
266
+ if failed:
267
+ try:
268
+ with open(spool, "a", encoding="utf-8") as fh:
269
+ fh.write("\n".join(failed) + "\n")
270
+ except OSError:
271
+ pass
272
+
273
+ for c in claims:
274
+ try:
275
+ c.unlink()
276
+ except OSError:
277
+ pass
278
+
279
+
280
+ def _post(url: str, body: str) -> bool:
281
+ try:
282
+ req = urllib.request.Request(
283
+ url,
284
+ data=body.encode("utf-8"),
285
+ headers={"Content-Type": "application/json"},
286
+ method="POST",
287
+ )
288
+ with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp:
289
+ return 200 <= resp.status < 300
290
+ except Exception:
291
+ return False
292
+
293
+
294
+ def _flush_main() -> int:
295
+ """Entry point for the detached flush subprocess (`-m agent_wallet.telemetry --flush`)."""
296
+ try:
297
+ _flush()
298
+ except Exception:
299
+ pass
300
+ return 0
301
+
302
+
303
+ if __name__ == "__main__":
304
+ if "--flush" in sys.argv[1:]:
305
+ raise SystemExit(_flush_main())
306
+ raise SystemExit(0)
@@ -120,6 +120,35 @@ class AgentWalletBackend(ABC):
120
120
  async def get_evm_lido_withdrawal_requests(self) -> dict[str, Any]:
121
121
  raise WalletBackendError(f"{self.name} does not support EVM Lido withdrawal lookup.")
122
122
 
123
+ async def get_evm_morpho_vaults(
124
+ self,
125
+ *,
126
+ vault_address: str | None = None,
127
+ limit: int | None = None,
128
+ listed_only: bool = True,
129
+ asset_address: str | None = None,
130
+ order_by: str | None = None,
131
+ order_direction: str | None = None,
132
+ ) -> dict[str, Any]:
133
+ raise WalletBackendError(f"{self.name} does not support EVM Morpho vault lookup.")
134
+
135
+ async def get_evm_morpho_markets(
136
+ self,
137
+ *,
138
+ market_id: str | None = None,
139
+ limit: int | None = None,
140
+ listed_only: bool = True,
141
+ search: str | None = None,
142
+ collateral_asset_address: str | None = None,
143
+ loan_asset_address: str | None = None,
144
+ order_by: str | None = None,
145
+ order_direction: str | None = None,
146
+ ) -> dict[str, Any]:
147
+ raise WalletBackendError(f"{self.name} does not support EVM Morpho market lookup.")
148
+
149
+ async def get_evm_morpho_positions(self) -> dict[str, Any]:
150
+ raise WalletBackendError(f"{self.name} does not support EVM Morpho positions lookup.")
151
+
123
152
  async def preview_evm_aave_operation(
124
153
  self,
125
154
  *,
@@ -175,6 +204,56 @@ class AgentWalletBackend(ABC):
175
204
  ) -> dict[str, Any]:
176
205
  raise WalletBackendError(f"{self.name} does not support EVM Lido withdrawals.")
177
206
 
207
+ async def preview_evm_morpho_vault_operation(
208
+ self,
209
+ *,
210
+ operation: str,
211
+ token_address: str,
212
+ vault_address: str | None = None,
213
+ vault_preset: str | None = None,
214
+ amount_raw: str | None = None,
215
+ native_amount_raw: str | None = None,
216
+ ) -> dict[str, Any]:
217
+ raise WalletBackendError(f"{self.name} does not support EVM Morpho vault previews.")
218
+
219
+ async def send_evm_morpho_vault_operation(
220
+ self,
221
+ *,
222
+ operation: str,
223
+ token_address: str,
224
+ vault_address: str | None = None,
225
+ vault_preset: str | None = None,
226
+ amount_raw: str | None = None,
227
+ native_amount_raw: str | None = None,
228
+ expected_quote_fingerprint: str | None = None,
229
+ ) -> dict[str, Any]:
230
+ raise WalletBackendError(f"{self.name} does not support EVM Morpho vault operations.")
231
+
232
+ async def preview_evm_morpho_market_operation(
233
+ self,
234
+ *,
235
+ operation: str,
236
+ token_address: str,
237
+ market_id: str | None = None,
238
+ market_preset: str | None = None,
239
+ amount_raw: str | None = None,
240
+ native_amount_raw: str | None = None,
241
+ ) -> dict[str, Any]:
242
+ raise WalletBackendError(f"{self.name} does not support EVM Morpho market previews.")
243
+
244
+ async def send_evm_morpho_market_operation(
245
+ self,
246
+ *,
247
+ operation: str,
248
+ token_address: str,
249
+ market_id: str | None = None,
250
+ market_preset: str | None = None,
251
+ amount_raw: str | None = None,
252
+ native_amount_raw: str | None = None,
253
+ expected_quote_fingerprint: str | None = None,
254
+ ) -> dict[str, Any]:
255
+ raise WalletBackendError(f"{self.name} does not support EVM Morpho market operations.")
256
+
178
257
  async def preview_evm_swap(
179
258
  self,
180
259
  *,
@@ -4704,6 +4704,7 @@ class SolanaWalletBackend(AgentWalletBackend):
4704
4704
  amount_ui: float,
4705
4705
  slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
4706
4706
  exclude_routers: list[str] | None = None,
4707
+ exclude_dexes: list[str] | None = None,
4707
4708
  ) -> dict[str, Any]:
4708
4709
  if self.network != "mainnet":
4709
4710
  raise WalletBackendError("Provider-routed swaps are only enabled for Solana mainnet.")
@@ -4749,6 +4750,7 @@ class SolanaWalletBackend(AgentWalletBackend):
4749
4750
  output_mint=output_mint,
4750
4751
  amount_raw=raw_amount,
4751
4752
  slippage_bps=slippage_bps,
4753
+ exclude_dexes=exclude_dexes,
4752
4754
  )
4753
4755
  quote_source = "jupiter-metis"
4754
4756
 
@@ -5006,17 +5008,27 @@ class SolanaWalletBackend(AgentWalletBackend):
5006
5008
 
5007
5009
  attempts: list[dict[str, Any]] = []
5008
5010
  last_error: str | None = None
5011
+ _simulation_failed = False
5009
5012
  for attempt_index in range(max_attempts):
5010
5013
  if valid_until_epoch_seconds is not None and int(time.time()) > int(valid_until_epoch_seconds):
5011
5014
  break
5012
5015
  try:
5013
5016
  exclude_routers = ["jupiterz"] if attempt_index > 0 else None
5017
+ # On retries after a simulation failure, exclude DEXes known to fail
5018
+ # simulation for Token-2022 tokens with extensions such as
5019
+ # scaledUiAmountConfig, pausableConfig, or permanentDelegate
5020
+ # (e.g. Backpack xStock tokens). GoonFi V2 is the primary offender;
5021
+ # ZeroFi handles these tokens correctly.
5022
+ exclude_dexes: list[str] | None = None
5023
+ if _simulation_failed and attempt_index > 0:
5024
+ exclude_dexes = ["GoonFi V2"]
5014
5025
  preview = await self.preview_swap(
5015
5026
  input_mint=input_mint,
5016
5027
  output_mint=output_mint,
5017
5028
  amount_ui=amount_ui,
5018
5029
  slippage_bps=slippage_bps,
5019
5030
  exclude_routers=exclude_routers,
5031
+ exclude_dexes=exclude_dexes,
5020
5032
  )
5021
5033
  estimated_output_raw = int(preview.get("estimated_output_amount_raw") or 0)
5022
5034
  if (
@@ -5073,6 +5085,8 @@ class SolanaWalletBackend(AgentWalletBackend):
5073
5085
  return result
5074
5086
  except (WalletBackendError, ProviderError) as exc:
5075
5087
  last_error = str(exc)
5088
+ if "simulation failed" in str(exc).lower():
5089
+ _simulation_failed = True
5076
5090
  attempts.append(
5077
5091
  {
5078
5092
  "attempt": attempt_index + 1,