@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,7 +2,7 @@
2
2
  "id": "agent-wallet",
3
3
  "name": "Agent Wallet",
4
4
  "description": "Official OpenClaw plugin bridge for the agent-wallet backends, including Solana, local BTC, and local EVM.",
5
- "version": "0.1.53",
5
+ "version": "0.1.58",
6
6
  "contracts": {
7
7
  "tools": [
8
8
  "agentlayer_autonomous_approve",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayertech/agent-wallet-plugin",
3
- "version": "0.1.53",
3
+ "version": "0.1.58",
4
4
  "description": "OpenClaw plugin bridge for the AgentLayer wallet runtime.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN ../../../LICENSE",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,69 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.1.58 - 2026-07-01
6
+
7
+ - Resolved SPL token symbol/name in `get_wallet_portfolio` via a batched,
8
+ concurrent Jupiter token-search lookup, so `/wallet-sol` shows tickers
9
+ (e.g. `USDC`) instead of raw mint addresses. The mint/token_address is
10
+ still returned in full and rendered shortened next to the label rather
11
+ than dropped. Falls back gracefully (symbol=None) when a mint isn't
12
+ indexed or the lookup provider is unavailable.
13
+ - `agent-wallet/agent_wallet/providers/jupiter.py`
14
+ - `agent-wallet/agent_wallet/wallet_layer/solana.py`
15
+ - `claude-code/plugins/agent-wallet/commands/wallet-sol.md`
16
+ - `codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md`
17
+ - Dropped the `/wallet-sol` source-metadata footer line (`source`,
18
+ `token_discovery_source`, `pricing_source`, `pricing_errors`) from the
19
+ rendered chat output for a more compact report.
20
+ - `claude-code/plugins/agent-wallet/commands/wallet-sol.md`
21
+ - `codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md`
22
+
23
+ ## v0.1.57 - 2026-07-01
24
+
25
+ - Added a bundled Codex `wallet-sol` skill so Codex users can render the
26
+ connected Solana wallet portfolio in chat the same way Claude Code's
27
+ `/wallet-sol` command does.
28
+ - `codex/plugins/agent-wallet/skills/wallet-sol/SKILL.md`
29
+ - `codex/plugins/agent-wallet/README.md`
30
+ - Fixed the resident read worker (used by `get_wallet_balance` /
31
+ `get_wallet_portfolio`, e.g. `/wallet-sol`) decrypting the wallet's private
32
+ key on every read-only cold start even though it never needed it. Read-only
33
+ onboarding now resolves the address from the existing plaintext wallet pin
34
+ file once a wallet has been provisioned, instead of unsealing secrets and
35
+ deriving a signer just to discard it.
36
+ - `agent-wallet/agent_wallet/user_wallets.py`
37
+ - Reduced `/wallet-sol` context payload by dropping the unused raw Jupiter
38
+ price blob (`price_raw`) from portfolio token entries.
39
+ - `agent-wallet/agent_wallet/wallet_layer/solana.py`
40
+ - Parallelized Jupiter price batch fetches in `get_portfolio` instead of
41
+ awaiting each 20-mint batch sequentially, speeding up portfolio lookups for
42
+ wallets with many SPL token accounts.
43
+ - `agent-wallet/agent_wallet/wallet_layer/solana.py`
44
+ - Added a background prewarm of the resident read worker at MCP server
45
+ startup so interpreter boot and onboarding overlap with the user issuing
46
+ the first read-only command instead of blocking it. Opt out with
47
+ `AGENT_WALLET_PREWARM_READ_WORKER=0`.
48
+ - `codex/plugins/agent-wallet/server.py`
49
+ - Added idle eviction for resident read workers left over from a stale
50
+ config (e.g. after switching Solana network or wallet backend), bounded by
51
+ `AGENT_WALLET_READ_WORKER_IDLE_SECONDS` (default 10 minutes), and closed
52
+ resident read workers on SIGTERM in addition to the existing `atexit`
53
+ cleanup, since Python does not run `atexit` hooks on signal termination.
54
+ - `codex/plugins/agent-wallet/server.py`
55
+
56
+ ## v0.1.54 - 2026-06-29
57
+
58
+ - Fixed Claude Code autonomous-mode slash command activation so
59
+ `/agentlayer-autonomous-approve` no longer relies on the model treating the
60
+ slash invocation itself as sufficient consent. The command now uses an
61
+ explicit in-command confirmation step before enabling the standing
62
+ permission, and both autonomous toggle commands are marked manual-only in the
63
+ Claude Code command metadata.
64
+ - `claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-approve.md`
65
+ - `claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-revoke.md`
66
+ - `claude-code/plugins/agent-wallet/README.md`
67
+
5
68
  ## v0.1.53 - 2026-06-26
6
69
 
7
70
  - Fixed Morpho vault and market quote (preview) requests timing out under the
package/README.md CHANGED
@@ -86,35 +86,37 @@ openclaw plugins install clawhub:@agentlayertech/agent-wallet-plugin
86
86
 
87
87
  The ClawHub package does not replace the npm installer. `npx @agentlayer.tech/wallet install --yes` installs the local runtime, Python backend, and helper services. ClawHub only installs the OpenClaw plugin surface that points at that runtime.
88
88
 
89
- Or install the CLI globally:
89
+ Or install the CLI globally for shorter commands:
90
90
 
91
91
  ```bash
92
92
  npm install -g @agentlayer.tech/wallet
93
93
  wallet install --yes
94
94
  ```
95
95
 
96
+ > **Why install globally?** Without the global install, `wallet` is not in your `PATH` and commands like `wallet update --yes` return `zsh: command not found: wallet`. The global install adds the `wallet` binary once; after that all `wallet <cmd>` shortcuts work. If you skip the global install, prefix every command with `npx @agentlayer.tech/wallet` instead (see the Updating section below).
97
+
96
98
  The CLI uses a versioned runtime layout:
97
99
 
98
100
  ```bash
99
- ~/.openclaw/agent-wallet-runtime/releases/<version>
100
- ~/.openclaw/agent-wallet-runtime/current
101
+ ~/.openclaw/agent-wallet-runtime/releases/<version> # immutable per-version snapshots
102
+ ~/.openclaw/agent-wallet-runtime/current # symlink → active release
101
103
  ```
102
104
 
103
105
  On first install, `--yes` generates local runtime secrets. The installer stores `master_key` and `approval_secret` in `~/.openclaw/sealed_keys.json`; only the boot key needed to unlock that sealed bundle is written to the runtime `.env`.
104
106
 
105
- Useful npm CLI commands:
107
+ Useful CLI commands (require the global install above):
106
108
 
107
109
  ```bash
108
- wallet status
109
- wallet doctor
110
- wallet hermes install --yes
111
- wallet codex install --yes
112
- wallet update --yes
113
- wallet update --yes --dry-run
114
- wallet rollback
110
+ wallet status # show active runtime version and health
111
+ wallet doctor # diagnose common config problems
112
+ wallet update --yes # upgrade to the latest published version
113
+ wallet update --yes --dry-run # preview the upgrade without applying it
114
+ wallet rollback # revert to the previous release
115
+ wallet hermes install --yes # (re)connect Hermes to the current runtime
116
+ wallet codex install --yes # (re)connect Codex to the current runtime
115
117
  ```
116
118
 
117
- `wallet update --yes` delegates to the latest published npm package and reuses shared Python and Node dependency snapshots when possible. Use `wallet update --yes --dry-run` to inspect the target version and dependency plan before switching `current`.
119
+ `wallet update --yes` fetches the latest npm package, installs it under `releases/<version>`, and flips the `current` symlink. Python and Node dependency snapshots are reused when the dependency fingerprint has not changed, so subsequent updates are fast. All frameworks that read from `current/` (Claude Code, Codex, Hermes) pick up the new code automatically on their next session start; OpenClaw requires a gateway restart to reload its TypeScript extension.
118
120
 
119
121
  ## Native OpenClaw plugin installs
120
122
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.53
1
+ 0.1.58
@@ -43,6 +43,7 @@ SOLANA_AGENT_KEYPAIR_PATH=
43
43
  JUPITER_API_BASE_URL=https://lite-api.jup.ag/swap/v1
44
44
  JUPITER_ULTRA_API_BASE_URL=https://lite-api.jup.ag/ultra/v1
45
45
  JUPITER_PRICE_API_BASE_URL=https://lite-api.jup.ag/price/v3
46
+ JUPITER_TOKEN_SEARCH_API_BASE_URL=https://lite-api.jup.ag/tokens/v2
46
47
  JUPITER_API_KEY=
47
48
 
48
49
  # LI.FI cross-chain routing. API key is optional for basic read-only quote/status calls.
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Keep in sync with package.json, pyproject.toml, and the npm installer version.
4
4
  # scripts/check_release_version.mjs enforces this on release.
5
- __version__ = "0.1.53"
5
+ __version__ = "0.1.58"
6
6
 
7
7
  __all__ = [
8
8
  "config",
@@ -48,6 +48,7 @@ class Settings(BaseSettings):
48
48
  jupiter_swap_v2_api_base_url: str = "https://api.jup.ag/swap/v2"
49
49
  jupiter_ultra_api_base_url: str = "https://lite-api.jup.ag/ultra/v1"
50
50
  jupiter_price_api_base_url: str = "https://lite-api.jup.ag/price/v3"
51
+ jupiter_token_search_api_base_url: str = "https://lite-api.jup.ag/tokens/v2"
51
52
  jupiter_api_key: str = ""
52
53
  lifi_api_base_url: str = "https://li.quest/v1"
53
54
  lifi_api_key: str = ""
@@ -74,6 +75,14 @@ class Settings(BaseSettings):
74
75
  settings = Settings()
75
76
 
76
77
 
78
+ def reload_settings() -> Settings:
79
+ """Reload settings from the current environment without changing object identity."""
80
+ refreshed = Settings()
81
+ for field_name in Settings.model_fields:
82
+ setattr(settings, field_name, getattr(refreshed, field_name))
83
+ return settings
84
+
85
+
77
86
  def normalize_solana_network(network: str | None) -> str:
78
87
  """Canonicalize supported Solana network names and reject test clusters."""
79
88
  normalized = str(network or "").strip().lower() or "mainnet"
@@ -33,6 +33,12 @@ def _parse_csv(value: Any) -> str:
33
33
 
34
34
 
35
35
  SECRET_CONFIG_KEYS = {"privateKey", "masterKey", "approvalSecret"}
36
+ READ_ONLY_WORKER_TOOLS = frozenset(
37
+ {
38
+ "get_wallet_balance",
39
+ "get_wallet_portfolio",
40
+ }
41
+ )
36
42
 
37
43
 
38
44
  def _reject_secret_config_json(config: dict[str, Any]) -> None:
@@ -117,6 +123,9 @@ def _apply_config_overrides(config: dict[str, Any]) -> None:
117
123
  if not allow_override and os.getenv(env_name, "").strip():
118
124
  continue
119
125
  os.environ[env_name] = text
126
+ from agent_wallet.config import reload_settings
127
+
128
+ reload_settings()
120
129
 
121
130
 
122
131
  def _load_json(raw: str | None, default: dict[str, Any] | None = None) -> dict[str, Any]:
@@ -138,10 +147,23 @@ def _read_stdin_secret(field_name: str) -> str:
138
147
  async def _run_onboard(user_id: str, config: dict[str, Any]) -> dict[str, Any]:
139
148
  from agent_wallet.openclaw_runtime import onboard_openclaw_user_wallet
140
149
 
141
- context = onboard_openclaw_user_wallet(
150
+ context = _build_runtime_context(user_id, config)
151
+ return context.serializable_bundle()
152
+
153
+
154
+ def _build_runtime_context(
155
+ user_id: str,
156
+ config: dict[str, Any],
157
+ *,
158
+ read_only: bool = False,
159
+ ) -> Any:
160
+ from agent_wallet.openclaw_runtime import onboard_openclaw_user_wallet
161
+
162
+ return onboard_openclaw_user_wallet(
142
163
  user_id,
143
164
  backend=config.get("backend"),
144
165
  sign_only=config.get("signOnly"),
166
+ read_only=read_only,
145
167
  network=config.get("network"),
146
168
  rpc_url=config.get("rpcUrl"),
147
169
  wdk_btc_service_url=config.get("wdkBtcServiceUrl"),
@@ -151,7 +173,6 @@ async def _run_onboard(user_id: str, config: dict[str, Any]) -> dict[str, Any]:
151
173
  wdk_evm_wallet_id=config.get("wdkEvmWalletId"),
152
174
  wdk_evm_account_index=config.get("wdkEvmAccountIndex"),
153
175
  )
154
- return context.serializable_bundle()
155
176
 
156
177
 
157
178
  async def _run_invoke(
@@ -160,21 +181,7 @@ async def _run_invoke(
160
181
  arguments: dict[str, Any],
161
182
  config: dict[str, Any],
162
183
  ) -> dict[str, Any]:
163
- from agent_wallet.openclaw_runtime import onboard_openclaw_user_wallet
164
-
165
- context = onboard_openclaw_user_wallet(
166
- user_id,
167
- backend=config.get("backend"),
168
- sign_only=config.get("signOnly"),
169
- network=config.get("network"),
170
- rpc_url=config.get("rpcUrl"),
171
- wdk_btc_service_url=config.get("wdkBtcServiceUrl"),
172
- wdk_btc_wallet_id=config.get("wdkBtcWalletId"),
173
- wdk_btc_account_index=config.get("wdkBtcAccountIndex"),
174
- wdk_evm_service_url=config.get("wdkEvmServiceUrl"),
175
- wdk_evm_wallet_id=config.get("wdkEvmWalletId"),
176
- wdk_evm_account_index=config.get("wdkEvmAccountIndex"),
177
- )
184
+ context = _build_runtime_context(user_id, config)
178
185
  result = await context.adapter.invoke(tool_name, arguments)
179
186
  return result.model_dump()
180
187
 
@@ -188,21 +195,7 @@ async def _run_issue_approval(
188
195
  mainnet_confirmed: bool = False,
189
196
  ttl_seconds: int | None = None,
190
197
  ) -> dict[str, Any]:
191
- from agent_wallet.openclaw_runtime import onboard_openclaw_user_wallet
192
-
193
- context = onboard_openclaw_user_wallet(
194
- user_id,
195
- backend=config.get("backend"),
196
- sign_only=config.get("signOnly"),
197
- network=config.get("network"),
198
- rpc_url=config.get("rpcUrl"),
199
- wdk_btc_service_url=config.get("wdkBtcServiceUrl"),
200
- wdk_btc_wallet_id=config.get("wdkBtcWalletId"),
201
- wdk_btc_account_index=config.get("wdkBtcAccountIndex"),
202
- wdk_evm_service_url=config.get("wdkEvmServiceUrl"),
203
- wdk_evm_wallet_id=config.get("wdkEvmWalletId"),
204
- wdk_evm_account_index=config.get("wdkEvmAccountIndex"),
205
- )
198
+ context = _build_runtime_context(user_id, config)
206
199
  token = context.issue_execute_approval(
207
200
  tool_name=tool_name,
208
201
  confirmation_summary=summary,
@@ -250,6 +243,69 @@ def _run_autonomous_permission(action: str, scope: str) -> dict[str, Any]:
250
243
  raise WalletBackendError("action must be approve, revoke, or status.")
251
244
 
252
245
 
246
+ def _error_payload_for_exception(exc: Exception) -> dict[str, Any]:
247
+ payload: dict[str, Any] = {"ok": False, "error": str(exc)}
248
+ if isinstance(exc, WalletBackendError):
249
+ if exc.code:
250
+ payload["code"] = exc.code
251
+ if exc.details is not None:
252
+ payload["details"] = exc.details
253
+ return payload
254
+
255
+
256
+ async def _run_read_worker_tool(context: Any, request: dict[str, Any]) -> dict[str, Any]:
257
+ tool_name = str(request.get("tool") or "").strip()
258
+ if tool_name not in READ_ONLY_WORKER_TOOLS:
259
+ raise WalletBackendError(
260
+ f"read-worker only allows read-only tools: {', '.join(sorted(READ_ONLY_WORKER_TOOLS))}."
261
+ )
262
+ arguments = request.get("arguments") or {}
263
+ if not isinstance(arguments, dict):
264
+ raise WalletBackendError("read-worker request arguments must be a JSON object.")
265
+ result = await context.adapter.invoke(tool_name, arguments)
266
+ return result.model_dump()
267
+
268
+
269
+ async def _serve_read_worker(user_id: str, config: dict[str, Any]) -> int:
270
+ context = _build_runtime_context(user_id, config, read_only=True)
271
+ while True:
272
+ line = sys.stdin.readline()
273
+ if not line:
274
+ break
275
+ text = line.strip()
276
+ if not text:
277
+ continue
278
+ request_id: str | None = None
279
+ try:
280
+ request = _load_json(text)
281
+ request_id = str(request.get("id") or "").strip() or None
282
+ if request.get("op") == "shutdown":
283
+ print(json.dumps({"id": request_id, "ok": True, "stopped": True}), flush=True)
284
+ break
285
+ payload = await _run_read_worker_tool(context, request)
286
+ print(
287
+ json.dumps(
288
+ {
289
+ "id": request_id,
290
+ "ok": True,
291
+ "payload": payload,
292
+ }
293
+ ),
294
+ flush=True,
295
+ )
296
+ except Exception as exc:
297
+ print(
298
+ json.dumps(
299
+ {
300
+ "id": request_id,
301
+ **_error_payload_for_exception(exc),
302
+ }
303
+ ),
304
+ flush=True,
305
+ )
306
+ return 0
307
+
308
+
253
309
  async def _run_btc_wallet_get(user_id: str, config: dict[str, Any]) -> dict[str, Any]:
254
310
  from agent_wallet.btc_user_wallets import get_user_btc_wallet_binding
255
311
 
@@ -522,6 +578,10 @@ def main() -> int:
522
578
  evm_lock_parser.add_argument("--user-id", required=True)
523
579
  evm_lock_parser.add_argument("--config-json", default="{}")
524
580
 
581
+ read_worker_parser = subparsers.add_parser("read-worker")
582
+ read_worker_parser.add_argument("--user-id", required=True)
583
+ read_worker_parser.add_argument("--config-json", default="{}")
584
+
525
585
  args = parser.parse_args()
526
586
 
527
587
  try:
@@ -643,6 +703,8 @@ def main() -> int:
643
703
  )
644
704
  elif args.command == "evm-wallet-lock":
645
705
  payload = asyncio.run(_run_evm_wallet_lock(args.user_id, config))
706
+ elif args.command == "read-worker":
707
+ return asyncio.run(_serve_read_worker(args.user_id, config))
646
708
  else:
647
709
  payload = asyncio.run(
648
710
  _run_invoke(
@@ -662,13 +724,7 @@ def main() -> int:
662
724
  backend=str(locals().get("config", {}).get("backend", "") or ""),
663
725
  ok=False,
664
726
  )
665
- error_payload: dict[str, Any] = {"ok": False, "error": str(exc)}
666
- if isinstance(exc, WalletBackendError):
667
- if exc.code:
668
- error_payload["code"] = exc.code
669
- if exc.details is not None:
670
- error_payload["details"] = exc.details
671
- print(json.dumps(error_payload), file=sys.stderr)
727
+ print(json.dumps(_error_payload_for_exception(exc)), file=sys.stderr)
672
728
  return 1
673
729
 
674
730
  if getattr(args, "command", "") == "invoke":
@@ -87,6 +87,7 @@ def onboard_openclaw_user_wallet(
87
87
  *,
88
88
  backend: str | None = None,
89
89
  sign_only: bool | None = None,
90
+ read_only: bool = False,
90
91
  network: str | None = None,
91
92
  rpc_url: str | None = None,
92
93
  wdk_btc_service_url: str | None = None,
@@ -98,6 +99,9 @@ def onboard_openclaw_user_wallet(
98
99
  ) -> OpenClawWalletRuntimeContext:
99
100
  """Provision and assemble a runtime-ready wallet context for one OpenClaw user."""
100
101
  backend_name = str(backend or settings.agent_wallet_backend).strip().lower()
102
+ effective_sign_only = True if read_only else (
103
+ settings.agent_wallet_sign_only if sign_only is None else sign_only
104
+ )
101
105
  if backend_name in {"wdk_btc_local", "wdk-btc-local", "btc_local", "btc-local"}:
102
106
  service_url = str(wdk_btc_service_url or settings.wdk_btc_service_url).strip()
103
107
  account_index = (
@@ -133,7 +137,7 @@ def onboard_openclaw_user_wallet(
133
137
  wallet_id=wallet_id,
134
138
  network=effective_network,
135
139
  account_index=account_index,
136
- sign_only=settings.agent_wallet_sign_only if sign_only is None else sign_only,
140
+ sign_only=effective_sign_only,
137
141
  address=str(address_payload.get("address") or "").strip() or None,
138
142
  )
139
143
  wallet_info = {
@@ -188,7 +192,7 @@ def onboard_openclaw_user_wallet(
188
192
  wallet_id=wallet_id,
189
193
  network=effective_network,
190
194
  account_index=account_index,
191
- sign_only=settings.agent_wallet_sign_only if sign_only is None else sign_only,
195
+ sign_only=effective_sign_only,
192
196
  address=resolved_address or None,
193
197
  )
194
198
  wallet_info = {
@@ -213,7 +217,8 @@ def onboard_openclaw_user_wallet(
213
217
 
214
218
  backend, wallet_info, created_now = create_openclaw_solana_backend(
215
219
  user_id,
216
- sign_only=sign_only,
220
+ sign_only=effective_sign_only,
221
+ read_only=read_only,
217
222
  network=network,
218
223
  rpc_url=rpc_url,
219
224
  )
@@ -483,3 +483,37 @@ async def fetch_prices(
483
483
  if not isinstance(data, dict):
484
484
  raise ProviderError("jupiter", "Unexpected price response from Jupiter.")
485
485
  return data
486
+
487
+
488
+ async def fetch_token_metadata(*, mints: list[str]) -> dict[str, dict[str, Any]]:
489
+ """Look up token symbol/name for a batch of mints via Jupiter's token
490
+ search API. Keyed by mint address; mints Jupiter doesn't index (very new
491
+ or illiquid tokens) are simply absent from the result rather than
492
+ raising, since this is display-only enrichment on top of the price data.
493
+ """
494
+ if not mints:
495
+ return {}
496
+ client = get_client()
497
+ params = {"query": ",".join(mints)}
498
+ response = await client.get(
499
+ f"{settings.jupiter_token_search_api_base_url.rstrip('/')}/search",
500
+ params=params,
501
+ headers=_headers(),
502
+ )
503
+ if response.status_code != 200:
504
+ raise ProviderError("jupiter", f"HTTP {response.status_code}: {response.text[:300]}")
505
+ data = response.json()
506
+ if not isinstance(data, list):
507
+ raise ProviderError("jupiter", "Unexpected token search response from Jupiter.")
508
+ result: dict[str, dict[str, Any]] = {}
509
+ for item in data:
510
+ if not isinstance(item, dict):
511
+ continue
512
+ mint = str(item.get("id") or "").strip()
513
+ if not mint:
514
+ continue
515
+ result[mint] = {
516
+ "symbol": item.get("symbol"),
517
+ "name": item.get("name"),
518
+ }
519
+ return result
@@ -147,10 +147,31 @@ def _load_user_wallet_secret_material(
147
147
  )
148
148
 
149
149
 
150
- def ensure_user_solana_wallet(user_id: str, network: str | None = None) -> dict[str, str]:
150
+ def ensure_user_solana_wallet(
151
+ user_id: str,
152
+ network: str | None = None,
153
+ *,
154
+ read_only: bool = False,
155
+ ) -> dict[str, str]:
151
156
  """Provision a per-user Solana wallet if it does not exist yet."""
152
157
  effective_network = _resolve_effective_network(network)
153
158
  path = resolve_user_wallet_path(user_id, network=effective_network)
159
+ if read_only and path.exists():
160
+ # Read-only callers only need the address. The pin file already stores
161
+ # it in plaintext, so skip decrypting the wallet secret material and
162
+ # deriving the signer entirely rather than paying for a KDF unseal +
163
+ # keypair derivation just to read (and immediately discard) a private
164
+ # key. Falls through to the full decrypt path below if no pin exists
165
+ # yet (e.g. a wallet file predating pin files).
166
+ pin = load_wallet_pin(path)
167
+ if pin is not None and pin["network"] == effective_network:
168
+ return {
169
+ "user_id": user_id,
170
+ "address": pin["address"],
171
+ "path": str(path),
172
+ "storage_format": "encrypted",
173
+ "key_scope": "pinned-address-only",
174
+ }
154
175
  if path.exists():
155
176
  secret_material, storage_format, key_scope = _load_user_wallet_secret_material(
156
177
  path,
@@ -362,6 +383,7 @@ def create_openclaw_solana_backend(
362
383
  user_id: str,
363
384
  *,
364
385
  sign_only: bool | None = None,
386
+ read_only: bool = False,
365
387
  network: str | None = None,
366
388
  rpc_url: str | None = None,
367
389
  ) -> tuple[SolanaWalletBackend, dict[str, str], bool]:
@@ -396,6 +418,11 @@ def create_openclaw_solana_backend(
396
418
  raise WalletBackendError(
397
419
  "Configured Solana publicKey does not match the signer derived from keypairPath/runtime secret."
398
420
  )
421
+ effective_sign_only = True if read_only else (
422
+ settings.agent_wallet_sign_only if sign_only is None else sign_only
423
+ )
424
+ if read_only:
425
+ signer = None
399
426
 
400
427
  rpc_config = resolve_runtime_solana_rpc_config(
401
428
  effective_network,
@@ -411,7 +438,7 @@ def create_openclaw_solana_backend(
411
438
  network=effective_network,
412
439
  signer=signer,
413
440
  address=resolved_address or None,
414
- sign_only=settings.agent_wallet_sign_only if sign_only is None else sign_only,
441
+ sign_only=effective_sign_only,
415
442
  rpc_provider_mode=str(rpc_config["mode"]),
416
443
  rpc_provider=str(rpc_config["provider"]),
417
444
  rpc_transport=str(rpc_config["transport"]),
@@ -429,11 +456,27 @@ def create_openclaw_solana_backend(
429
456
 
430
457
  wallet_path = resolve_user_wallet_path(user_id, network=effective_network)
431
458
  created_now = not wallet_path.exists()
459
+ wallet_info = ensure_user_solana_wallet(user_id, network=effective_network, read_only=read_only)
460
+ if read_only:
461
+ backend = SolanaWalletBackend(
462
+ rpc_url=rpc_config["rpc_urls"],
463
+ commitment=settings.solana_commitment,
464
+ network=effective_network,
465
+ signer=None,
466
+ address=wallet_info["address"] or None,
467
+ sign_only=effective_sign_only,
468
+ rpc_provider_mode=str(rpc_config["mode"]),
469
+ rpc_provider=str(rpc_config["provider"]),
470
+ rpc_transport=str(rpc_config["transport"]),
471
+ swap_provider=str(swap_config["provider"]),
472
+ swap_transport=str(swap_config["transport"]),
473
+ )
474
+ return backend, wallet_info, created_now
475
+
432
476
  backend = create_wallet_backend_for_user(
433
477
  user_id,
434
478
  sign_only=sign_only,
435
479
  network=effective_network,
436
480
  rpc_url=rpc_url,
437
481
  )
438
- wallet_info = ensure_user_solana_wallet(user_id, network=effective_network)
439
482
  return backend, wallet_info, created_now
@@ -339,17 +339,37 @@ class SolanaWalletBackend(AgentWalletBackend):
339
339
 
340
340
  price_data_by_mint: dict[str, dict[str, Any]] = {}
341
341
  price_errors: list[str] = []
342
- for index in range(0, len(mints), 20):
343
- batch = mints[index : index + 20]
344
- try:
345
- price_data = await jupiter.fetch_prices(mints=batch)
346
- except ProviderError as exc:
347
- price_errors.append(str(exc))
342
+ token_metadata_by_mint: dict[str, dict[str, Any]] = {}
343
+ token_metadata_errors: list[str] = []
344
+ price_batches = [mints[index : index + 20] for index in range(0, len(mints), 20)]
345
+ # Independent HTTP calls against a shared async client, so fetch every
346
+ # price batch AND every token symbol/name lookup batch concurrently
347
+ # instead of paying sequential round trips for wallets with many SPL
348
+ # token accounts.
349
+ all_results = await asyncio.gather(
350
+ *(jupiter.fetch_prices(mints=batch) for batch in price_batches),
351
+ *(jupiter.fetch_token_metadata(mints=batch) for batch in price_batches),
352
+ return_exceptions=True,
353
+ )
354
+ batch_results = all_results[: len(price_batches)]
355
+ metadata_batch_results = all_results[len(price_batches) :]
356
+ for batch, result in zip(price_batches, batch_results):
357
+ if isinstance(result, ProviderError):
358
+ price_errors.append(str(result))
348
359
  continue
360
+ if isinstance(result, BaseException):
361
+ raise result
349
362
  for mint in batch:
350
- entry = _jupiter_price_entry(price_data, mint)
363
+ entry = _jupiter_price_entry(result, mint)
351
364
  if entry is not None:
352
365
  price_data_by_mint[mint] = entry
366
+ for result in metadata_batch_results:
367
+ if isinstance(result, ProviderError):
368
+ token_metadata_errors.append(str(result))
369
+ continue
370
+ if isinstance(result, BaseException):
371
+ raise result
372
+ token_metadata_by_mint.update(result)
353
373
 
354
374
  native_price = _jupiter_usd_price(price_data_by_mint.get(NATIVE_SOL_MINT))
355
375
  native_amount = _coerce_decimal(native_balance.get("balance_native"))
@@ -379,13 +399,15 @@ class SolanaWalletBackend(AgentWalletBackend):
379
399
  if value is not None:
380
400
  total_value += value
381
401
  priced_asset_count += 1
402
+ metadata = token_metadata_by_mint.get(mint) or {}
382
403
  enriched_tokens.append(
383
404
  {
384
405
  **token,
406
+ "symbol": metadata.get("symbol"),
407
+ "name": metadata.get("name"),
385
408
  "price_usd": str(price) if price is not None else None,
386
409
  "value_usd": _format_decimal(value),
387
410
  "pricing_source": "jupiter-price" if price is not None else None,
388
- "price_raw": price_data_by_mint.get(mint),
389
411
  }
390
412
  )
391
413
 
@@ -410,6 +432,8 @@ class SolanaWalletBackend(AgentWalletBackend):
410
432
  {
411
433
  "asset_type": "spl-token",
412
434
  "mint": token.get("mint"),
435
+ "symbol": token.get("symbol"),
436
+ "name": token.get("name"),
413
437
  "token_account": token.get("token_account"),
414
438
  "amount_raw": token.get("amount_raw"),
415
439
  "amount_ui": token.get("amount_ui"),
@@ -442,6 +466,8 @@ class SolanaWalletBackend(AgentWalletBackend):
442
466
  "total_value_usd": formatted_total_value,
443
467
  "pricing_source": "jupiter-price" if price_data_by_mint else None,
444
468
  "pricing_errors": price_errors,
469
+ "token_metadata_source": "jupiter-token-search" if token_metadata_by_mint else None,
470
+ "token_metadata_errors": token_metadata_errors,
445
471
  "token_discovery_source": "solana-rpc",
446
472
  "source": "solana-rpc+jupiter-price",
447
473
  }
@@ -2,7 +2,7 @@
2
2
  "id": "agent-wallet",
3
3
  "name": "Agent Wallet",
4
4
  "description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
5
- "version": "0.1.53",
5
+ "version": "0.1.58",
6
6
  "skills": ["skills/wallet-operator"],
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.53"
7
+ version = "0.1.58"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [