@agentlayer.tech/wallet 0.1.53 → 0.1.58

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 (31) hide show
  1. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
  2. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  3. package/CHANGELOG.md +63 -0
  4. package/README.md +14 -12
  5. package/VERSION +1 -1
  6. package/agent-wallet/.env.example +1 -0
  7. package/agent-wallet/agent_wallet/__init__.py +1 -1
  8. package/agent-wallet/agent_wallet/config.py +9 -0
  9. package/agent-wallet/agent_wallet/openclaw_cli.py +95 -39
  10. package/agent-wallet/agent_wallet/openclaw_runtime.py +8 -3
  11. package/agent-wallet/agent_wallet/providers/jupiter.py +34 -0
  12. package/agent-wallet/agent_wallet/user_wallets.py +46 -3
  13. package/agent-wallet/agent_wallet/wallet_layer/solana.py +34 -8
  14. package/agent-wallet/openclaw.plugin.json +1 -1
  15. package/agent-wallet/pyproject.toml +1 -1
  16. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  17. package/claude-code/plugins/agent-wallet/README.md +9 -3
  18. package/claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-approve.md +17 -2
  19. package/claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-revoke.md +1 -0
  20. package/claude-code/plugins/agent-wallet/commands/wallet-base.md +37 -0
  21. package/claude-code/plugins/agent-wallet/commands/wallet-ethereum.md +37 -0
  22. package/claude-code/plugins/agent-wallet/commands/wallet-evm.md +38 -0
  23. package/claude-code/plugins/agent-wallet/commands/wallet-sol.md +30 -0
  24. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  25. package/codex/plugins/agent-wallet/README.md +9 -0
  26. package/codex/plugins/agent-wallet/server.py +427 -6
  27. package/codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md +29 -0
  28. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  29. package/package.json +1 -1
  30. package/wdk-btc-wallet/package.json +1 -1
  31. package/wdk-evm-wallet/package.json +1 -1
@@ -2,12 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import atexit
5
6
  import asyncio
6
7
  import copy
7
8
  import base64
8
9
  import hashlib
9
10
  import json
10
11
  import os
12
+ import selectors
13
+ import signal
11
14
  import subprocess
12
15
  import sys
13
16
  import threading
@@ -75,15 +78,21 @@ APPROVAL_CONTEXT_MISSING_MESSAGE = (
75
78
  "operation again, wait for explicit user confirmation, then retry execute. Do not ask the "
76
79
  "user for a manual approval token."
77
80
  )
81
+ RESIDENT_READ_ONLY_TOOLS = {
82
+ "get_wallet_balance",
83
+ "get_wallet_portfolio",
84
+ }
78
85
 
79
86
  selected_wallet_backend: str | None = None
80
87
  selected_solana_network: str | None = None
81
88
  selected_evm_network: str | None = None
82
89
  selected_btc_network: str | None = None
83
90
  approval_preview_cache: dict[str, dict[str, Any]] = {}
91
+ resident_read_workers: dict[str, "_ResidentReadWorker"] = {}
84
92
  # Guards approval_preview_cache against races once wallet calls run concurrently
85
93
  # via asyncio.to_thread. Reentrant so prune helpers can be nested under writers.
86
94
  _approval_cache_lock = threading.RLock()
95
+ _resident_worker_lock = threading.RLock()
87
96
 
88
97
 
89
98
  class WalletCliError(RuntimeError):
@@ -93,6 +102,10 @@ class WalletCliError(RuntimeError):
93
102
  self.details = details or {}
94
103
 
95
104
 
105
+ class ResidentReadWorkerTransportError(RuntimeError):
106
+ """Raised when the long-lived read worker transport fails."""
107
+
108
+
96
109
  def _plugin_root() -> Path:
97
110
  return Path(__file__).resolve().parent
98
111
 
@@ -361,6 +374,15 @@ def _normalize_selectable_evm_network(value: Any) -> str:
361
374
  return network
362
375
 
363
376
 
377
+ def _implied_evm_network_from_backend_alias(value: Any) -> str | None:
378
+ normalized = str(value or "").strip().lower()
379
+ if normalized in {"base", "base-mainnet"}:
380
+ return "base"
381
+ if normalized in {"ethereum", "eth", "mainnet", "eth-mainnet"}:
382
+ return "ethereum"
383
+ return None
384
+
385
+
364
386
  def _normalize_solana_network(value: Any) -> str | None:
365
387
  network = str(value or "").strip().lower()
366
388
  if not network:
@@ -492,12 +514,17 @@ def _reject_secret_config_json(config: dict[str, Any]) -> None:
492
514
 
493
515
 
494
516
  def _base_config(args: dict[str, Any], *, tool_name: str = "") -> dict[str, Any]:
517
+ requested_backend = args.get("backend")
495
518
  backend = (
496
- _normalize_wallet_backend(args.get("backend"))
497
- if args.get("backend") is not None
519
+ _normalize_wallet_backend(requested_backend)
520
+ if requested_backend is not None
498
521
  else _active_backend_for_tool(tool_name)
499
522
  )
500
523
  config = _effective_config_for_backend(backend)
524
+ if backend == "wdk_evm_local":
525
+ implied_evm_network = _implied_evm_network_from_backend_alias(requested_backend)
526
+ if implied_evm_network and args.get("network") is None:
527
+ config["network"] = implied_evm_network
501
528
  if "config" in args:
502
529
  extra = args.get("config")
503
530
  if not isinstance(extra, dict):
@@ -611,6 +638,311 @@ def _invoke_tool(tool_name: str, arguments: dict[str, Any], config: dict[str, An
611
638
  )
612
639
 
613
640
 
641
+ class _ResidentReadWorker:
642
+ def __init__(self, *, user_id: str, config: dict[str, Any]):
643
+ self.user_id = user_id
644
+ self.config = copy.deepcopy(config)
645
+ self.package_root = _resolve_package_root()
646
+ self._process: subprocess.Popen[str] | None = None
647
+ self._lock = threading.RLock()
648
+ self._request_id = 0
649
+ self._stderr_lines: list[str] = []
650
+ self._stderr_thread: threading.Thread | None = None
651
+ self._last_used = time.monotonic()
652
+
653
+ def touch(self) -> None:
654
+ self._last_used = time.monotonic()
655
+
656
+ def idle_seconds(self) -> float:
657
+ return time.monotonic() - self._last_used
658
+
659
+ def _command(self) -> list[str]:
660
+ return [
661
+ _python_bin(self.package_root),
662
+ "-m",
663
+ "agent_wallet.openclaw_cli",
664
+ "read-worker",
665
+ "--user-id",
666
+ self.user_id,
667
+ "--config-json",
668
+ json.dumps(self.config),
669
+ ]
670
+
671
+ def _drain_stderr(self) -> None:
672
+ process = self._process
673
+ if process is None or process.stderr is None:
674
+ return
675
+ try:
676
+ for raw_line in process.stderr:
677
+ line = raw_line.strip()
678
+ if not line:
679
+ continue
680
+ with self._lock:
681
+ self._stderr_lines.append(line)
682
+ if len(self._stderr_lines) > 20:
683
+ self._stderr_lines = self._stderr_lines[-20:]
684
+ except Exception:
685
+ return
686
+
687
+ def _stderr_summary(self) -> str:
688
+ with self._lock:
689
+ if not self._stderr_lines:
690
+ return ""
691
+ return " | ".join(self._stderr_lines[-5:])
692
+
693
+ def _ensure_started(self) -> subprocess.Popen[str]:
694
+ with self._lock:
695
+ process = self._process
696
+ if process is not None and process.poll() is None:
697
+ return process
698
+ try:
699
+ process = subprocess.Popen(
700
+ self._command(),
701
+ cwd=str(self.package_root),
702
+ env=_cli_env(self.package_root),
703
+ text=True,
704
+ stdin=subprocess.PIPE,
705
+ stdout=subprocess.PIPE,
706
+ stderr=subprocess.PIPE,
707
+ bufsize=1,
708
+ )
709
+ except Exception as exc:
710
+ raise ResidentReadWorkerTransportError(
711
+ f"Could not start resident read worker: {exc}"
712
+ ) from exc
713
+ self._process = process
714
+ self._stderr_lines = []
715
+ self._stderr_thread = threading.Thread(
716
+ target=self._drain_stderr,
717
+ name="agent-wallet-read-worker-stderr",
718
+ daemon=True,
719
+ )
720
+ self._stderr_thread.start()
721
+ return process
722
+
723
+ def warm(self) -> None:
724
+ """Spawn the worker eagerly, without waiting for a request/response.
725
+
726
+ Lets the interpreter boot, module imports, and read-only onboarding
727
+ happen off the critical path of the first real read-only tool call.
728
+ """
729
+ try:
730
+ self._ensure_started()
731
+ except ResidentReadWorkerTransportError:
732
+ pass
733
+
734
+ def close(self) -> None:
735
+ with self._lock:
736
+ process = self._process
737
+ self._process = None
738
+ if process is None:
739
+ return
740
+ try:
741
+ if process.poll() is None and process.stdin is not None:
742
+ process.stdin.write(json.dumps({"op": "shutdown"}) + "\n")
743
+ process.stdin.flush()
744
+ except Exception:
745
+ pass
746
+ try:
747
+ process.wait(timeout=1)
748
+ except Exception:
749
+ try:
750
+ process.terminate()
751
+ process.wait(timeout=1)
752
+ except Exception:
753
+ try:
754
+ process.kill()
755
+ except Exception:
756
+ pass
757
+
758
+ def invoke(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
759
+ with self._lock:
760
+ self.touch()
761
+ process = self._ensure_started()
762
+ if process.stdin is None or process.stdout is None:
763
+ self.close()
764
+ raise ResidentReadWorkerTransportError("Resident read worker stdio is unavailable.")
765
+ self._request_id += 1
766
+ request_id = str(self._request_id)
767
+ request = {
768
+ "id": request_id,
769
+ "tool": tool_name,
770
+ "arguments": arguments,
771
+ }
772
+ try:
773
+ process.stdin.write(json.dumps(request) + "\n")
774
+ process.stdin.flush()
775
+ except Exception as exc:
776
+ self.close()
777
+ raise ResidentReadWorkerTransportError(
778
+ f"Could not write to resident read worker: {exc}"
779
+ ) from exc
780
+
781
+ selector = selectors.DefaultSelector()
782
+ selector.register(process.stdout, selectors.EVENT_READ)
783
+ events = selector.select(timeout=_cli_timeout_seconds())
784
+ selector.close()
785
+ if not events:
786
+ self.close()
787
+ raise ResidentReadWorkerTransportError(
788
+ f"Resident read worker timed out after {_cli_timeout_seconds():g}s."
789
+ )
790
+ response_line = process.stdout.readline()
791
+ if not response_line:
792
+ self.close()
793
+ stderr_summary = self._stderr_summary()
794
+ detail = f" Worker stderr: {stderr_summary}" if stderr_summary else ""
795
+ raise ResidentReadWorkerTransportError(
796
+ "Resident read worker exited without a response." + detail
797
+ )
798
+ try:
799
+ response = json.loads(response_line)
800
+ except json.JSONDecodeError as exc:
801
+ self.close()
802
+ raise ResidentReadWorkerTransportError(
803
+ f"Resident read worker returned invalid JSON: {exc}"
804
+ ) from exc
805
+ if str(response.get("id") or "") != request_id:
806
+ self.close()
807
+ raise ResidentReadWorkerTransportError(
808
+ "Resident read worker response id did not match the request."
809
+ )
810
+ if response.get("ok") is False:
811
+ raise WalletCliError(
812
+ str(response.get("error") or "resident read worker failed."),
813
+ code=str(response.get("code") or ""),
814
+ details=response.get("details")
815
+ if isinstance(response.get("details"), dict)
816
+ else {},
817
+ )
818
+ payload = response.get("payload")
819
+ if not isinstance(payload, dict):
820
+ raise ResidentReadWorkerTransportError(
821
+ "Resident read worker returned a non-object payload."
822
+ )
823
+ return payload
824
+
825
+
826
+ def _resident_worker_cache_key(user_id: str, config: dict[str, Any]) -> str:
827
+ return _canonical_json_text(
828
+ {
829
+ "user_id": user_id,
830
+ "config": config,
831
+ }
832
+ )
833
+
834
+
835
+ def _resident_worker_idle_seconds() -> float:
836
+ """Idle threshold (seconds) after which an unused resident worker for a
837
+ config that is no longer the active one gets reaped. Falls back to 10
838
+ minutes on bad values."""
839
+ raw = os.getenv("AGENT_WALLET_READ_WORKER_IDLE_SECONDS", "600")
840
+ try:
841
+ value = float(raw)
842
+ except (TypeError, ValueError):
843
+ return 600.0
844
+ return value if value > 0 else 600.0
845
+
846
+
847
+ def _evict_idle_resident_read_workers(*, keep_key: str) -> None:
848
+ """Close resident workers other than `keep_key` that have been idle past
849
+ the configured threshold.
850
+
851
+ Each distinct (user_id, config) pair (e.g. switching Solana network or
852
+ wallet backend mid-session) keeps its own resident subprocess alive
853
+ indefinitely otherwise, since nothing previously evicted them. This
854
+ bounds that growth without adding a background timer thread: eviction
855
+ piggybacks on the next lookup instead.
856
+ """
857
+ idle_limit = _resident_worker_idle_seconds()
858
+ with _resident_worker_lock:
859
+ stale_keys = [
860
+ key
861
+ for key, worker in resident_read_workers.items()
862
+ if key != keep_key and worker.idle_seconds() > idle_limit
863
+ ]
864
+ stale_workers = [resident_read_workers.pop(key) for key in stale_keys]
865
+ for worker in stale_workers:
866
+ worker.close()
867
+
868
+
869
+ def _resident_read_worker_for_config(user_id: str, config: dict[str, Any]) -> _ResidentReadWorker:
870
+ key = _resident_worker_cache_key(user_id, config)
871
+ _evict_idle_resident_read_workers(keep_key=key)
872
+ with _resident_worker_lock:
873
+ worker = resident_read_workers.get(key)
874
+ if worker is None:
875
+ worker = _ResidentReadWorker(user_id=user_id, config=config)
876
+ resident_read_workers[key] = worker
877
+ return worker
878
+
879
+
880
+ def _shutdown_resident_read_workers() -> None:
881
+ with _resident_worker_lock:
882
+ workers = list(resident_read_workers.values())
883
+ resident_read_workers.clear()
884
+ for worker in workers:
885
+ worker.close()
886
+
887
+
888
+ atexit.register(_shutdown_resident_read_workers)
889
+
890
+
891
+ def _handle_termination_signal(signum: int, frame: Any) -> None:
892
+ """Close resident read worker subprocesses on SIGTERM, then terminate
893
+ normally.
894
+
895
+ atexit handlers only run on normal interpreter shutdown; Python's
896
+ default SIGTERM disposition terminates the process immediately without
897
+ unwinding to atexit. Hosts that gracefully stop this MCP server (e.g.
898
+ closing a Claude Code / Codex session) send SIGTERM, so without this the
899
+ resident worker subprocesses leaked past every normal shutdown, not just
900
+ an abrupt kill -9. (SIGINT is not handled here: Python's default handler
901
+ raises KeyboardInterrupt, which unwinds to a normal interpreter
902
+ shutdown and already triggers the atexit hook above.)
903
+ """
904
+ _shutdown_resident_read_workers()
905
+ signal.signal(signum, signal.SIG_DFL)
906
+ os.kill(os.getpid(), signum)
907
+
908
+
909
+ def _install_termination_signal_handlers() -> None:
910
+ try:
911
+ signal.signal(signal.SIGTERM, _handle_termination_signal)
912
+ except (ValueError, OSError):
913
+ # e.g. signal.signal() called outside the main thread.
914
+ pass
915
+
916
+
917
+ def _prewarm_resident_read_worker() -> None:
918
+ """Best-effort background warm-up of the default resident read worker.
919
+
920
+ server.py is a persistent stdio MCP process kept alive for the whole
921
+ host session, but the resident worker itself only used to spawn lazily
922
+ on the first read-only tool call (get_wallet_balance /
923
+ get_wallet_portfolio, e.g. /wallet-sol), putting interpreter boot +
924
+ onboarding on the critical path of that first call. Kick it off in a
925
+ daemon thread instead so it overlaps with the user issuing the command.
926
+ Failures here are silently ignored: the lazy path in
927
+ _invoke_read_tool_blocking remains the source of truth and will retry.
928
+ """
929
+ if os.getenv("AGENT_WALLET_PREWARM_READ_WORKER", "1").strip().lower() in {"0", "false", "no"}:
930
+ return
931
+
932
+ def _run() -> None:
933
+ try:
934
+ config = _base_config({}, tool_name="get_wallet_portfolio")
935
+ _resident_read_worker_for_config(_user_id(), config).warm()
936
+ except Exception:
937
+ pass
938
+
939
+ threading.Thread(
940
+ target=_run,
941
+ name="agent-wallet-read-worker-prewarm",
942
+ daemon=True,
943
+ ).start()
944
+
945
+
614
946
  def _issue_approval_token(
615
947
  tool_name: str,
616
948
  config: dict[str, Any],
@@ -640,6 +972,15 @@ def _issue_approval_token(
640
972
  return token
641
973
 
642
974
 
975
+ def _invoke_resident_read_tool(tool_name: str, arguments: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]:
976
+ worker = _resident_read_worker_for_config(_user_id(), config)
977
+ try:
978
+ return worker.invoke(tool_name, arguments)
979
+ except ResidentReadWorkerTransportError:
980
+ worker.close()
981
+ raise
982
+
983
+
643
984
  def _is_solana_swap_intent_execute(params: dict[str, Any]) -> bool:
644
985
  return str(params.get("mode") or "") == "intent_execute"
645
986
 
@@ -807,6 +1148,33 @@ def _sanitize_schema(schema: dict[str, Any]) -> dict[str, Any]:
807
1148
 
808
1149
  def _manual_tool_definitions() -> list[dict[str, Any]]:
809
1150
  return [
1151
+ {
1152
+ "name": "get_wallet_overview",
1153
+ "description": (
1154
+ "Get a one-off wallet overview for a requested backend/network without changing the "
1155
+ "active Codex wallet session. Returns the same enriched balance payload used by "
1156
+ "get_wallet_balance."
1157
+ ),
1158
+ "input_schema": {
1159
+ "type": "object",
1160
+ "properties": {
1161
+ "backend": {
1162
+ "type": "string",
1163
+ "description": "solana, evm, base, ethereum, btc, or bitcoin.",
1164
+ },
1165
+ "network": {
1166
+ "type": "string",
1167
+ "description": "Optional network override. Use base or ethereum for EVM.",
1168
+ },
1169
+ "address": {
1170
+ "type": "string",
1171
+ "description": "Optional wallet address override.",
1172
+ },
1173
+ },
1174
+ "additionalProperties": False,
1175
+ },
1176
+ "read_only": True,
1177
+ },
810
1178
  {
811
1179
  "name": "get_active_wallet_backend",
812
1180
  "description": (
@@ -909,7 +1277,13 @@ async def _handle_set_wallet_backend(params: dict[str, Any]) -> dict[str, Any]:
909
1277
  # globals after the validating wallet call succeeds, so a failed switch never
910
1278
  # leaves a stale backend paired with a freshly mutated network selection.
911
1279
  if backend == "wdk_evm_local":
912
- implied = params.get("network") or selected_evm_network or _default_evm_network() or "ethereum"
1280
+ implied = (
1281
+ params.get("network")
1282
+ or _implied_evm_network_from_backend_alias(requested)
1283
+ or selected_evm_network
1284
+ or _default_evm_network()
1285
+ or "ethereum"
1286
+ )
913
1287
  resolved_network = _normalize_selectable_evm_network(implied)
914
1288
  elif backend == "wdk_btc_local":
915
1289
  resolved_network = _normalize_btc_network(
@@ -978,6 +1352,31 @@ async def _handle_set_evm_network(params: dict[str, Any]) -> dict[str, Any]:
978
1352
  }
979
1353
 
980
1354
 
1355
+ async def _handle_get_wallet_overview(params: dict[str, Any]) -> dict[str, Any]:
1356
+ config = _base_config(params, tool_name="get_wallet_balance")
1357
+ backend = _normalize_wallet_backend(config.get("backend"))
1358
+
1359
+ effective_params: dict[str, Any] = {}
1360
+ if params.get("address") is not None:
1361
+ effective_params["address"] = params.get("address")
1362
+
1363
+ payload = await asyncio.to_thread(
1364
+ _invoke_read_tool_blocking, "get_wallet_balance", effective_params, config
1365
+ )
1366
+ if payload.get("ok") is False:
1367
+ raise RuntimeError(str(payload.get("error") or "get_wallet_overview failed"))
1368
+ data = payload.get("data", {})
1369
+ if not isinstance(data, dict):
1370
+ return {
1371
+ "requested_backend": _backend_label(backend),
1372
+ "requested_network": config.get("network"),
1373
+ "data": data,
1374
+ }
1375
+ data.setdefault("requested_backend", _backend_label(backend))
1376
+ data.setdefault("requested_network", config.get("network"))
1377
+ return data
1378
+
1379
+
981
1380
  def _invoke_wallet_tool_blocking(
982
1381
  tool_name: str,
983
1382
  config: dict[str, Any],
@@ -1002,6 +1401,19 @@ def _invoke_wallet_tool_blocking(
1002
1401
  return payload
1003
1402
 
1004
1403
 
1404
+ def _invoke_read_tool_blocking(
1405
+ tool_name: str,
1406
+ effective_params: dict[str, Any],
1407
+ config: dict[str, Any],
1408
+ ) -> dict[str, Any]:
1409
+ if tool_name not in RESIDENT_READ_ONLY_TOOLS:
1410
+ return _invoke_tool(tool_name, effective_params, config)
1411
+ try:
1412
+ return _invoke_resident_read_tool(tool_name, effective_params, config)
1413
+ except ResidentReadWorkerTransportError:
1414
+ return _invoke_tool(tool_name, effective_params, config)
1415
+
1416
+
1005
1417
  async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[str, Any]:
1006
1418
  config = _base_config(params, tool_name=tool_name)
1007
1419
  backend = _normalize_wallet_backend(config.get("backend"))
@@ -1010,9 +1422,14 @@ async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[st
1010
1422
  config["network"] = selected_evm_network
1011
1423
 
1012
1424
  effective_params = dict(params)
1013
- payload = await asyncio.to_thread(
1014
- _invoke_wallet_tool_blocking, tool_name, config, effective_params
1015
- )
1425
+ if tool_name in RESIDENT_READ_ONLY_TOOLS:
1426
+ payload = await asyncio.to_thread(
1427
+ _invoke_read_tool_blocking, tool_name, effective_params, config
1428
+ )
1429
+ else:
1430
+ payload = await asyncio.to_thread(
1431
+ _invoke_wallet_tool_blocking, tool_name, config, effective_params
1432
+ )
1016
1433
 
1017
1434
  if payload.get("ok") is False:
1018
1435
  raise RuntimeError(str(payload.get("error") or f"{tool_name} failed"))
@@ -1068,6 +1485,8 @@ def build_server():
1068
1485
 
1069
1486
  async def _dispatch(tool_name: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
1070
1487
  params = params or {}
1488
+ if tool_name == "get_wallet_overview":
1489
+ return await _handle_get_wallet_overview(params)
1071
1490
  if tool_name == "get_active_wallet_backend":
1072
1491
  return await _handle_get_active_wallet_backend()
1073
1492
  if tool_name == "set_wallet_backend":
@@ -1101,6 +1520,8 @@ def build_server():
1101
1520
 
1102
1521
 
1103
1522
  def main() -> None:
1523
+ _install_termination_signal_handlers()
1524
+ _prewarm_resident_read_worker()
1104
1525
  build_server().run(show_banner=False)
1105
1526
 
1106
1527
 
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: "wallet-sol"
3
+ description: "Show the current Solana wallet portfolio from the local AgentLayer wallet in a compact chat table. Use when the user asks for /wallet-sol or wants the Solana wallet shown directly in chat."
4
+ ---
5
+
6
+ # Solana Wallet Portfolio
7
+
8
+ Use the local AgentLayer wallet MCP tools only.
9
+
10
+ Workflow:
11
+
12
+ 1. Call `set_wallet_backend` with `backend=solana`.
13
+ 2. Call `get_wallet_portfolio` with no address override.
14
+ 3. Render the result directly in chat in this shape:
15
+ - title: `Solana Wallet Portfolio`
16
+ - bullets for `Chain`, `Network`, `Address`, and `Total Value (USD)` when available
17
+ - one compact Markdown table with columns: `Asset | Type | Amount | USD Value`
18
+
19
+ Formatting rules:
20
+
21
+ - Use `assets` when present.
22
+ - For the asset label, prefer `symbol`, then `name`, then `mint`, then `token_address`, then `asset_type`.
23
+ - When the label came from `symbol`/`name` (not the mint/token_address itself) and a `mint` or `token_address` is present, append it shortened in parentheses next to the label as `first6…last4` (e.g. `USDC (EPjFWd…TDt1v)`) — keep the contract visible but compact, never show it twice.
24
+ - For the asset type, use `asset_type` when present; otherwise infer `native` for the native asset and `token` for the rest.
25
+ - For the amount, prefer `amount_ui`, then `balance_ui`, then `balance_native`, then `amount_raw`.
26
+ - For the USD value, prefer `value_usd`, then `balance_usd`.
27
+ - Do not include source metadata lines or extra footer fields such as `source`, `token_discovery_source`, `pricing_source`, or `pricing_errors`.
28
+ - Keep the response concise. Do not add strategy suggestions, swap ideas, or write actions unless the user explicitly asks for them.
29
+ - If the wallet tool fails, surface the tool error plainly and stop.
@@ -1,5 +1,5 @@
1
1
  name: agent-wallet
2
- version: 0.1.53
2
+ version: 0.1.58
3
3
  description: Thin Hermes Agent bridge to the existing AgentLayer/OpenClaw wallet backend
4
4
  provides_tools:
5
5
  - agent_wallet_tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.53",
3
+ "version": "0.1.58",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-btc-wallet",
3
- "version": "0.1.53",
3
+ "version": "0.1.58",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate BTC-only wallet service built on Tether WDK.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-evm-wallet",
3
- "version": "0.1.53",
3
+ "version": "0.1.58",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate EVM wallet service built on Tether WDK.",