@agentlayer.tech/wallet 0.1.27 → 0.1.30

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 (56) hide show
  1. package/.openclaw/extensions/agent-wallet/README.md +4 -5
  2. package/.openclaw/extensions/agent-wallet/dist/index.js +31 -31
  3. package/.openclaw/extensions/agent-wallet/index.ts +31 -31
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +2 -2
  5. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  6. package/CHANGELOG.md +52 -0
  7. package/README.md +9 -0
  8. package/agent-wallet/README.md +18 -22
  9. package/agent-wallet/agent_wallet/bootstrap.py +28 -12
  10. package/agent-wallet/agent_wallet/btc_user_wallets.py +2 -7
  11. package/agent-wallet/agent_wallet/config.py +99 -22
  12. package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -14
  13. package/agent-wallet/agent_wallet/openclaw_adapter.py +72 -108
  14. package/agent-wallet/agent_wallet/openclaw_runtime.py +3 -12
  15. package/agent-wallet/agent_wallet/providers/kamino.py +21 -4
  16. package/agent-wallet/agent_wallet/providers/solana_rpc.py +0 -23
  17. package/agent-wallet/agent_wallet/providers/x402.py +198 -18
  18. package/agent-wallet/agent_wallet/user_wallets.py +4 -3
  19. package/agent-wallet/agent_wallet/wallet_layer/base.py +3 -3
  20. package/agent-wallet/agent_wallet/wallet_layer/factory.py +8 -5
  21. package/agent-wallet/agent_wallet/wallet_layer/solana.py +437 -44
  22. package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +2 -8
  23. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +13 -13
  24. package/agent-wallet/examples/openclaw_runtime_onboarding.py +1 -1
  25. package/agent-wallet/examples/openclaw_user_wallet_example.py +1 -1
  26. package/agent-wallet/openclaw.plugin.json +1 -1
  27. package/agent-wallet/pyproject.toml +2 -1
  28. package/agent-wallet/scripts/bootstrap_openclaw_btc.py +3 -5
  29. package/agent-wallet/scripts/bootstrap_openclaw_evm.py +2 -12
  30. package/agent-wallet/scripts/build_release_bundle.py +1 -0
  31. package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +1 -4
  32. package/agent-wallet/scripts/install_agent_wallet.py +1 -0
  33. package/agent-wallet/scripts/install_openclaw_local_config.py +4 -6
  34. package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +2 -4
  35. package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +2 -15
  36. package/agent-wallet/scripts/reveal_btc_seed.sh +7 -16
  37. package/agent-wallet/scripts/setup_btc_wallet.sh +7 -16
  38. package/agent-wallet/scripts/setup_evm_wallet.sh +1 -11
  39. package/agent-wallet/scripts/switch_openclaw_wallet_network.py +4 -1
  40. package/agent-wallet/skills/wallet-operator/SKILL.md +0 -1
  41. package/bin/openclaw-agent-wallet.mjs +289 -0
  42. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +20 -0
  43. package/claude-code/plugins/agent-wallet/.mcp.json +14 -0
  44. package/claude-code/plugins/agent-wallet/README.md +65 -0
  45. package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +34 -0
  46. package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
  47. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +38 -0
  48. package/codex/plugins/agent-wallet/.mcp.json +15 -0
  49. package/codex/plugins/agent-wallet/README.md +39 -0
  50. package/codex/plugins/agent-wallet/scripts/run_mcp.sh +21 -0
  51. package/codex/plugins/agent-wallet/server.py +1077 -0
  52. package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
  53. package/hermes/plugins/agent_wallet/schemas.py +2 -2
  54. package/hermes/plugins/agent_wallet/tools.py +17 -3
  55. package/package.json +6 -1
  56. package/setup.sh +2 -0
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  from pathlib import Path
7
7
 
8
- from agent_wallet.config import refuse_mainnet_wallet_recreation
8
+ from agent_wallet.config import normalize_solana_network, refuse_mainnet_wallet_recreation
9
9
  from agent_wallet.file_ops import atomic_write_text
10
10
  from agent_wallet.wallet_layer.base import WalletBackendError
11
11
  from agent_wallet.wallet_layer.base58 import b58encode
@@ -79,11 +79,12 @@ def load_wallet_pin(path: Path) -> dict[str, str] | None:
79
79
 
80
80
  def write_wallet_pin(path: Path, *, address: str, network: str) -> dict[str, str]:
81
81
  """Persist the expected wallet address for later mismatch checks."""
82
+ normalized_network = normalize_solana_network(network)
82
83
  payload = {
83
84
  "kind": WALLET_ADDRESS_PIN_KIND,
84
85
  "version": WALLET_ADDRESS_PIN_VERSION,
85
86
  "address": address,
86
- "network": network.strip().lower(),
87
+ "network": normalized_network,
87
88
  "wallet_file": path.name,
88
89
  }
89
90
  pin_path = resolve_wallet_pin_path(path)
@@ -97,7 +98,7 @@ def write_wallet_pin(path: Path, *, address: str, network: str) -> dict[str, str
97
98
 
98
99
  def ensure_wallet_pin(path: Path, *, address: str, network: str) -> dict[str, str]:
99
100
  """Ensure the wallet pin exists and matches the expected address."""
100
- expected_network = network.strip().lower()
101
+ expected_network = normalize_solana_network(network)
101
102
  existing = load_wallet_pin(path)
102
103
  if existing is None:
103
104
  return write_wallet_pin(path, address=address, network=expected_network)
@@ -114,7 +115,7 @@ def ensure_wallet_pin(path: Path, *, address: str, network: str) -> dict[str, st
114
115
 
115
116
  def refuse_recreation_if_pinned(path: Path, *, network: str) -> None:
116
117
  """Refuse to recreate a wallet when a mainnet address is already pinned."""
117
- expected_network = network.strip().lower()
118
+ expected_network = normalize_solana_network(network)
118
119
  if expected_network != "mainnet" or not refuse_mainnet_wallet_recreation():
119
120
  return
120
121
  existing = load_wallet_pin(path)
@@ -128,7 +129,11 @@ def refuse_recreation_if_pinned(path: Path, *, network: str) -> None:
128
129
 
129
130
  def ensure_solana_wallet_ready() -> dict[str, str] | None:
130
131
  """Ensure that a Solana wallet exists when auto-create is enabled."""
131
- from agent_wallet.config import default_solana_wallet_path, resolve_solana_private_key, settings
132
+ from agent_wallet.config import (
133
+ default_solana_wallet_path,
134
+ resolve_solana_private_key,
135
+ settings,
136
+ )
132
137
 
133
138
  if settings.agent_wallet_backend.strip().lower() not in {"solana", "solana_local", "solana-local"}:
134
139
  return None
@@ -136,8 +141,13 @@ def ensure_solana_wallet_ready() -> dict[str, str] | None:
136
141
  if resolve_solana_private_key():
137
142
  return {"address": "", "path": ""}
138
143
 
144
+ normalized_network = normalize_solana_network(settings.solana_network)
139
145
  configured_path = settings.solana_agent_keypair_path.strip()
140
- path = Path(configured_path).expanduser() if configured_path else default_solana_wallet_path(settings.solana_network)
146
+ path = (
147
+ Path(configured_path).expanduser()
148
+ if configured_path
149
+ else default_solana_wallet_path(normalized_network)
150
+ )
141
151
 
142
152
  if path.exists():
143
153
  return {"address": "", "path": str(path)}
@@ -145,9 +155,9 @@ def ensure_solana_wallet_ready() -> dict[str, str] | None:
145
155
  if not settings.solana_auto_create_wallet:
146
156
  return None
147
157
 
148
- refuse_recreation_if_pinned(path, network=settings.solana_network)
158
+ refuse_recreation_if_pinned(path, network=normalized_network)
149
159
  created = create_solana_wallet_file(path)
150
- write_wallet_pin(path, address=created["address"], network=settings.solana_network)
160
+ write_wallet_pin(path, address=created["address"], network=normalized_network)
151
161
  return created
152
162
 
153
163
 
@@ -155,22 +165,28 @@ def describe_bootstrap() -> dict[str, str | bool]:
155
165
  """Return the effective bootstrap configuration for installer/runtime usage."""
156
166
  from agent_wallet.config import (
157
167
  default_solana_wallet_path,
168
+ normalize_solana_network,
158
169
  resolve_solana_rpc_url,
159
170
  resolve_runtime_solana_rpc_urls,
160
171
  settings,
161
172
  )
162
173
 
174
+ normalized_network = normalize_solana_network(settings.solana_network)
163
175
  configured_path = settings.solana_agent_keypair_path.strip()
164
- path = Path(configured_path).expanduser() if configured_path else default_solana_wallet_path(settings.solana_network)
176
+ path = (
177
+ Path(configured_path).expanduser()
178
+ if configured_path
179
+ else default_solana_wallet_path(normalized_network)
180
+ )
165
181
  rpc_urls = resolve_runtime_solana_rpc_urls(
166
- settings.solana_network,
182
+ normalized_network,
167
183
  settings.solana_rpc_url,
168
184
  settings.solana_rpc_urls,
169
185
  )
170
186
  return {
171
187
  "backend": settings.agent_wallet_backend,
172
- "network": settings.solana_network,
173
- "rpc_url": rpc_urls[0] if rpc_urls else resolve_solana_rpc_url(settings.solana_network, ""),
188
+ "network": normalized_network,
189
+ "rpc_url": rpc_urls[0] if rpc_urls else resolve_solana_rpc_url(normalized_network, ""),
174
190
  "rpc_urls": rpc_urls,
175
191
  "auto_create_wallet": settings.solana_auto_create_wallet,
176
192
  "keypair_path": str(path),
@@ -6,19 +6,14 @@ import json
6
6
  from pathlib import Path
7
7
  from typing import Any
8
8
 
9
- from agent_wallet.config import resolve_openclaw_home, settings
9
+ from agent_wallet.config import normalize_btc_network, resolve_openclaw_home, settings
10
10
  from agent_wallet.providers.wdk_btc_local import WdkBtcLocalClient
11
11
  from agent_wallet.user_wallets import normalize_user_id
12
12
  from agent_wallet.wallet_layer.base import WalletBackendError
13
13
 
14
14
 
15
15
  def _normalize_btc_network(value: str | None) -> str:
16
- network = str(value or "").strip().lower()
17
- aliases = {"mainnet": "bitcoin"}
18
- network = aliases.get(network, network)
19
- if network not in {"bitcoin", "testnet", "regtest"}:
20
- return "bitcoin"
21
- return network
16
+ return normalize_btc_network(value)
22
17
 
23
18
 
24
19
  def _resolve_service_url(service_url: str | None = None) -> str:
@@ -82,6 +82,79 @@ class Settings(BaseSettings):
82
82
  settings = Settings()
83
83
 
84
84
 
85
+ def normalize_solana_network(network: str | None) -> str:
86
+ """Canonicalize supported Solana network names and reject test clusters."""
87
+ normalized = str(network or "").strip().lower() or "mainnet"
88
+ aliases = {
89
+ "mainnet-beta": "mainnet",
90
+ }
91
+ normalized = aliases.get(normalized, normalized)
92
+ if normalized in {"devnet", "testnet"}:
93
+ from agent_wallet.wallet_layer.base import WalletBackendError
94
+
95
+ raise WalletBackendError(
96
+ "Solana devnet/testnet are no longer supported by agent-wallet. "
97
+ "Use mainnet or remove the Solana network override."
98
+ )
99
+ if normalized != "mainnet":
100
+ from agent_wallet.wallet_layer.base import WalletBackendError
101
+
102
+ raise WalletBackendError(
103
+ f"Unsupported Solana network: {normalized}. Only mainnet is supported."
104
+ )
105
+ return "mainnet"
106
+
107
+
108
+ def normalize_evm_network(network: str | None) -> str:
109
+ """Canonicalize supported EVM network names and reject testnets."""
110
+ normalized = str(network or "").strip().lower() or "ethereum"
111
+ aliases = {
112
+ "mainnet": "ethereum",
113
+ "eth": "ethereum",
114
+ "eth-mainnet": "ethereum",
115
+ "base-mainnet": "base",
116
+ }
117
+ normalized = aliases.get(normalized, normalized)
118
+ if normalized in {"sepolia", "base-sepolia", "base_sepolia"}:
119
+ from agent_wallet.wallet_layer.base import WalletBackendError
120
+
121
+ raise WalletBackendError(
122
+ "EVM testnets are no longer supported by agent-wallet. Use ethereum or base."
123
+ )
124
+ if normalized not in {"ethereum", "base"}:
125
+ from agent_wallet.wallet_layer.base import WalletBackendError
126
+
127
+ raise WalletBackendError(
128
+ f"Unsupported EVM network: {normalized}. Use ethereum or base."
129
+ )
130
+ return normalized
131
+
132
+
133
+ def normalize_btc_network(network: str | None) -> str:
134
+ """Canonicalize supported BTC network names and reject non-mainnet chains."""
135
+ normalized = str(network or "").strip().lower() or "bitcoin"
136
+ aliases = {
137
+ "mainnet": "bitcoin",
138
+ "btc": "bitcoin",
139
+ "bitcoin-mainnet": "bitcoin",
140
+ "bitcoin_mainnet": "bitcoin",
141
+ }
142
+ normalized = aliases.get(normalized, normalized)
143
+ if normalized in {"testnet", "regtest"}:
144
+ from agent_wallet.wallet_layer.base import WalletBackendError
145
+
146
+ raise WalletBackendError(
147
+ "Bitcoin testnet/regtest are no longer supported by agent-wallet. Use bitcoin."
148
+ )
149
+ if normalized != "bitcoin":
150
+ from agent_wallet.wallet_layer.base import WalletBackendError
151
+
152
+ raise WalletBackendError(
153
+ f"Unsupported Bitcoin network: {normalized}. Only bitcoin is supported."
154
+ )
155
+ return "bitcoin"
156
+
157
+
85
158
  def _normalize_provider_mode(value: str | None) -> str:
86
159
  mode = (value or "").strip().lower()
87
160
  if not mode:
@@ -129,20 +202,20 @@ def resolve_openclaw_home() -> Path:
129
202
 
130
203
  def default_solana_wallet_path(network: str) -> Path:
131
204
  """Return the default keypair path for a Solana wallet."""
132
- return resolve_openclaw_home() / "wallets" / f"solana-{network}-agent.json"
205
+ normalized_network = normalize_solana_network(network)
206
+ return resolve_openclaw_home() / "wallets" / f"solana-{normalized_network}-agent.json"
133
207
 
134
208
 
135
209
  def resolve_solana_rpc_url(network: str, configured: str) -> str:
136
210
  """Resolve the effective Solana RPC URL from network + optional override."""
211
+ normalized_network = normalize_solana_network(network)
137
212
  if configured.strip():
138
213
  return configured.strip()
139
214
 
140
215
  mapping = {
141
216
  "mainnet": "https://api.mainnet-beta.solana.com",
142
- "devnet": "https://api.devnet.solana.com",
143
- "testnet": "https://api.testnet.solana.com",
144
217
  }
145
- return mapping.get(network.strip().lower(), mapping["mainnet"])
218
+ return mapping.get(normalized_network, mapping["mainnet"])
146
219
 
147
220
 
148
221
  def resolve_solana_rpc_urls(
@@ -151,17 +224,18 @@ def resolve_solana_rpc_urls(
151
224
  configured_list: str = "",
152
225
  ) -> list[str]:
153
226
  """Resolve the ordered list of Solana RPC URLs to try."""
227
+ normalized_network = normalize_solana_network(network)
154
228
  candidates: list[str] = []
155
229
  for raw in (configured_list or "").split(","):
156
230
  value = raw.strip()
157
231
  if value and value not in candidates:
158
232
  candidates.append(value)
159
233
 
160
- primary = resolve_solana_rpc_url(network, configured)
234
+ primary = resolve_solana_rpc_url(normalized_network, configured)
161
235
  if primary and primary not in candidates:
162
236
  candidates.insert(0, primary)
163
237
 
164
- official = resolve_solana_rpc_url(network, "")
238
+ official = resolve_solana_rpc_url(normalized_network, "")
165
239
  if official and official not in candidates:
166
240
  candidates.append(official)
167
241
 
@@ -169,7 +243,8 @@ def resolve_solana_rpc_urls(
169
243
 
170
244
 
171
245
  def _build_provider_gateway_rpc_url(base_url: str, provider: str, network: str) -> str:
172
- return f"gateway::{provider}::{network.strip().lower()}::{base_url.rstrip('/')}/v1/rpc"
246
+ normalized_network = normalize_solana_network(network)
247
+ return f"gateway::{provider}::{normalized_network}::{base_url.rstrip('/')}/v1/rpc"
173
248
 
174
249
 
175
250
  def resolve_runtime_solana_rpc_config(
@@ -185,6 +260,7 @@ def resolve_runtime_solana_rpc_config(
185
260
  3. shared proxy gateway
186
261
  4. public official fallback
187
262
  """
263
+ normalized_network = normalize_solana_network(network)
188
264
  mode = _normalize_provider_mode(
189
265
  os.getenv("SOLANA_RPC_PROVIDER_MODE", settings.solana_rpc_provider_mode)
190
266
  )
@@ -200,10 +276,10 @@ def resolve_runtime_solana_rpc_config(
200
276
  "mode": "user_direct",
201
277
  "provider": "custom",
202
278
  "transport": "direct",
203
- "rpc_urls": resolve_solana_rpc_urls(network, env_primary, env_list),
279
+ "rpc_urls": resolve_solana_rpc_urls(normalized_network, env_primary, env_list),
204
280
  }
205
281
  if env_list:
206
- official = resolve_solana_rpc_url(network, "")
282
+ official = resolve_solana_rpc_url(normalized_network, "")
207
283
  candidates = [item.strip() for item in env_list.split(",") if item.strip()]
208
284
  if official and official not in candidates:
209
285
  candidates.append(official)
@@ -218,16 +294,15 @@ def resolve_runtime_solana_rpc_config(
218
294
  if alchemy_key:
219
295
  alchemy_base_by_network = {
220
296
  "mainnet": "https://solana-mainnet.g.alchemy.com/v2",
221
- "devnet": "https://solana-devnet.g.alchemy.com/v2",
222
297
  }
223
- alchemy_base = alchemy_base_by_network.get(network.strip().lower())
298
+ alchemy_base = alchemy_base_by_network.get(normalized_network)
224
299
  if alchemy_base:
225
300
  return {
226
301
  "mode": "user_direct",
227
302
  "provider": "alchemy",
228
303
  "transport": "direct",
229
304
  "rpc_urls": resolve_solana_rpc_urls(
230
- network,
305
+ normalized_network,
231
306
  f"{alchemy_base}/{alchemy_key}",
232
307
  "",
233
308
  ),
@@ -237,35 +312,40 @@ def resolve_runtime_solana_rpc_config(
237
312
  if helius_key:
238
313
  helius_base_by_network = {
239
314
  "mainnet": "https://mainnet.helius-rpc.com/",
240
- "devnet": "https://devnet.helius-rpc.com/",
241
315
  }
242
- helius_base = helius_base_by_network.get(network.strip().lower())
316
+ helius_base = helius_base_by_network.get(normalized_network)
243
317
  if helius_base:
244
318
  return {
245
319
  "mode": "user_direct",
246
320
  "provider": "helius",
247
321
  "transport": "direct",
248
322
  "rpc_urls": resolve_solana_rpc_urls(
249
- network,
323
+ normalized_network,
250
324
  f"{helius_base}?api-key={helius_key}",
251
325
  "",
252
326
  ),
253
327
  }
254
328
 
255
- if network.strip().lower() == "mainnet" and (mode == "shared_proxy" or (mode == "auto" and gateway_url)):
329
+ if normalized_network == "mainnet" and (mode == "shared_proxy" or (mode == "auto" and gateway_url)):
256
330
  if gateway_url:
257
331
  return {
258
332
  "mode": "shared_proxy",
259
333
  "provider": gateway_provider,
260
334
  "transport": "proxy",
261
- "rpc_urls": [_build_provider_gateway_rpc_url(gateway_url, gateway_provider, network)],
335
+ "rpc_urls": [
336
+ _build_provider_gateway_rpc_url(
337
+ gateway_url,
338
+ gateway_provider,
339
+ normalized_network,
340
+ )
341
+ ],
262
342
  }
263
343
 
264
344
  return {
265
345
  "mode": "public_fallback",
266
346
  "provider": "official",
267
347
  "transport": "direct",
268
- "rpc_urls": resolve_solana_rpc_urls(network, configured, configured_list),
348
+ "rpc_urls": resolve_solana_rpc_urls(normalized_network, configured, configured_list),
269
349
  }
270
350
 
271
351
 
@@ -289,10 +369,7 @@ def resolve_runtime_solana_swap_config(network: str) -> dict[str, str]:
289
369
  requested = _normalize_swap_provider(
290
370
  os.getenv("SOLANA_SWAP_PROVIDER", settings.solana_swap_provider)
291
371
  )
292
- normalized_network = network.strip().lower()
293
-
294
- if normalized_network != "mainnet":
295
- return {"provider": "jupiter", "transport": "direct"}
372
+ normalize_solana_network(network)
296
373
 
297
374
  if requested == "jupiter":
298
375
  return {"provider": "jupiter", "transport": "direct"}
@@ -14,6 +14,7 @@ from urllib.parse import urlparse
14
14
  from urllib.request import urlopen
15
15
 
16
16
  from agent_wallet.config import (
17
+ normalize_evm_network,
17
18
  resolve_boot_key,
18
19
  resolve_evm_wallet_password,
19
20
  resolve_openclaw_home,
@@ -27,18 +28,7 @@ LOCAL_WDK_EVM_HOSTS = {"127.0.0.1", "localhost", "::1"}
27
28
 
28
29
 
29
30
  def _normalize_evm_network(value: str | None) -> str:
30
- network = str(value or "").strip().lower()
31
- aliases = {
32
- "mainnet": "ethereum",
33
- "eth": "ethereum",
34
- "eth-mainnet": "ethereum",
35
- "base-mainnet": "base",
36
- "base_sepolia": "base-sepolia",
37
- }
38
- network = aliases.get(network, network)
39
- if network not in {"ethereum", "sepolia", "base", "base-sepolia"}:
40
- return "ethereum"
41
- return network
31
+ return normalize_evm_network(value)
42
32
 
43
33
 
44
34
  def _resolve_service_url(service_url: str | None = None) -> str:
@@ -52,8 +42,6 @@ def _paired_network(network: str) -> str | None:
52
42
  mapping = {
53
43
  "ethereum": "base",
54
44
  "base": "ethereum",
55
- "sepolia": "base-sepolia",
56
- "base-sepolia": "sepolia",
57
45
  }
58
46
  return mapping.get(_normalize_evm_network(network))
59
47
 
@@ -101,9 +101,10 @@ class OpenClawWalletAdapter:
101
101
  "eth": "ethereum",
102
102
  "eth-mainnet": "ethereum",
103
103
  "base-mainnet": "base",
104
- "base_sepolia": "base-sepolia",
105
104
  }
106
105
  network = aliases.get(network, network)
106
+ if network in {"sepolia", "base-sepolia", "base_sepolia"}:
107
+ raise WalletBackendError("EVM testnets are no longer supported. Use ethereum or base.")
107
108
  if network not in {"ethereum", "base"}:
108
109
  raise WalletBackendError("EVM network must be 'ethereum' or 'base'.")
109
110
  return network
@@ -245,8 +246,9 @@ class OpenClawWalletAdapter:
245
246
  AgentToolSpec(
246
247
  name="x402_pay_request",
247
248
  description=(
248
- "Prepare or execute an x402 paid request using the active wallet backend. "
249
- "This milestone executes the Solana exact buyer flow and keeps EVM as prepare-only."
249
+ "Pay for and call an x402 endpoint using the active wallet backend. "
250
+ "The tool probes the endpoint, validates compatibility, signs the payment, "
251
+ "and returns the service response in one call."
250
252
  ),
251
253
  input_schema={
252
254
  "type": "object",
@@ -257,26 +259,13 @@ class OpenClawWalletAdapter:
257
259
  "query": {"type": "object", "additionalProperties": True},
258
260
  "json_body": {},
259
261
  "text_body": {"type": "string"},
260
- "mode": {
261
- "type": "string",
262
- "enum": ["prepare", "execute"],
263
- "description": "prepare validates the payment plan; execute sends the paid retry.",
264
- },
265
262
  "purpose": {"type": "string"},
266
- "user_intent": {
267
- "type": "boolean",
268
- "description": "Must be true for prepare mode.",
269
- },
270
- "approval_token": {
271
- "type": "string",
272
- "description": "Required for execute mode and must be issued against the exact x402 payment summary.",
273
- },
274
263
  },
275
- "required": ["url", "mode", "purpose"],
264
+ "required": ["url", "purpose"],
276
265
  "additionalProperties": False,
277
266
  },
278
267
  read_only=False,
279
- requires_explicit_user_intent=True,
268
+ requires_explicit_user_intent=False,
280
269
  risk_level="high",
281
270
  ),
282
271
  ]
@@ -872,6 +861,42 @@ class OpenClawWalletAdapter:
872
861
  )
873
862
  return annotated
874
863
 
864
+ def _annotate_x402_payload(
865
+ self,
866
+ payload: dict[str, Any],
867
+ *,
868
+ mode: str,
869
+ ) -> dict[str, Any]:
870
+ if not payload.get("payment_required"):
871
+ return dict(payload)
872
+ annotated = self._annotate_sensitive_payload(
873
+ payload,
874
+ action_label="x402 paid request",
875
+ mode=mode,
876
+ )
877
+ summary = dict(annotated.get("confirmation_summary") or {})
878
+ if summary:
879
+ annotated["payment_summary"] = summary
880
+ requirements = dict(annotated.get("confirmation_requirements") or {})
881
+ requirements["prepare_requires_user_intent"] = False
882
+ requirements["execute_requires_approval_token"] = False
883
+ requirements["execute_requires_mainnet_confirmed_in_token"] = False
884
+ annotated.pop("approval_hint", None)
885
+ if mode == "preview":
886
+ annotated.pop("confirmation_summary", None)
887
+ annotated.pop("confirmation_requirements", None)
888
+ if annotated.get("is_mainnet"):
889
+ annotated["preview_note"] = (
890
+ "This is a paid mainnet endpoint preview only. Review the service URL, network, asset, amount, and payment destination before calling x402_pay_request."
891
+ )
892
+ return annotated
893
+ annotated["confirmation_requirements"] = requirements
894
+ if annotated.get("is_mainnet"):
895
+ annotated["mainnet_warning"] = (
896
+ "Mainnet x402 payment. Confirm the service URL, network, asset, amount, and payment destination before paying."
897
+ )
898
+ return annotated
899
+
875
900
  def list_tools(self) -> list[AgentToolSpec]:
876
901
  """Return wallet tools suitable for agent registration."""
877
902
  capabilities = self.backend.get_capabilities()
@@ -2290,6 +2315,25 @@ class OpenClawWalletAdapter:
2290
2315
  read_only=True,
2291
2316
  risk_level="low",
2292
2317
  ),
2318
+ AgentToolSpec(
2319
+ name="get_kamino_open_positions",
2320
+ description=(
2321
+ "Get all open Kamino lending positions for a Solana wallet on mainnet, "
2322
+ "aggregated across markets with loan details, reserve APYs, and rewards."
2323
+ ),
2324
+ input_schema={
2325
+ "type": "object",
2326
+ "properties": {
2327
+ "user": {
2328
+ "type": "string",
2329
+ "description": "Optional Solana wallet address override. If omitted, use the configured wallet.",
2330
+ },
2331
+ },
2332
+ "additionalProperties": False,
2333
+ },
2334
+ read_only=True,
2335
+ risk_level="low",
2336
+ ),
2293
2337
  ]
2294
2338
 
2295
2339
  if capabilities.can_sign_message:
@@ -3197,30 +3241,6 @@ class OpenClawWalletAdapter:
3197
3241
  )
3198
3242
  )
3199
3243
 
3200
- tools.append(
3201
- AgentToolSpec(
3202
- name="request_devnet_airdrop",
3203
- description=(
3204
- "Request SOL from the Solana faucet on devnet or testnet. "
3205
- "Only available outside mainnet."
3206
- ),
3207
- input_schema={
3208
- "type": "object",
3209
- "properties": {
3210
- "amount": {
3211
- "type": "number",
3212
- "description": "Amount of SOL to request from faucet.",
3213
- }
3214
- },
3215
- "required": ["amount"],
3216
- "additionalProperties": False,
3217
- },
3218
- read_only=False,
3219
- requires_explicit_user_intent=True,
3220
- risk_level="low",
3221
- )
3222
- )
3223
-
3224
3244
  tools.extend(self._x402_tool_specs())
3225
3245
  return [tool for tool in tools if tool.name not in TEMPORARILY_DISABLED_TOOLS]
3226
3246
 
@@ -3309,14 +3329,7 @@ class OpenClawWalletAdapter:
3309
3329
  text_body=text_body,
3310
3330
  )
3311
3331
  if data.get("payment_required"):
3312
- data = self._annotate_sensitive_payload(
3313
- data,
3314
- action_label="x402 paid request",
3315
- mode="preview",
3316
- )
3317
- approval_hint = dict(data.get("approval_hint") or {})
3318
- approval_hint["tool_name"] = "x402_pay_request"
3319
- data["approval_hint"] = approval_hint
3332
+ data = self._annotate_x402_payload(data, mode="preview")
3320
3333
  return AgentToolResult(tool=tool_name, ok=True, data=data)
3321
3334
 
3322
3335
  if tool_name == "x402_pay_request":
@@ -3326,10 +3339,7 @@ class OpenClawWalletAdapter:
3326
3339
  query = args.get("query")
3327
3340
  json_body = args.get("json_body")
3328
3341
  text_body = args.get("text_body")
3329
- mode = str(args.get("mode") or "").strip().lower()
3330
3342
  purpose = args.get("purpose")
3331
- user_intent = args.get("user_intent")
3332
- approval_token = args.get("approval_token")
3333
3343
  if not isinstance(url, str) or not url.strip():
3334
3344
  raise WalletBackendError("url is required.")
3335
3345
  if method is not None and not isinstance(method, str):
@@ -3340,51 +3350,9 @@ class OpenClawWalletAdapter:
3340
3350
  raise WalletBackendError("query must be an object when provided.")
3341
3351
  if text_body is not None and not isinstance(text_body, str):
3342
3352
  raise WalletBackendError("text_body must be a string when provided.")
3343
- if mode not in {"prepare", "execute"}:
3344
- raise WalletBackendError("mode must be 'prepare' or 'execute'.")
3345
3353
  if not isinstance(purpose, str) or not purpose.strip():
3346
3354
  raise WalletBackendError("purpose is required.")
3347
- if mode == "prepare":
3348
- self._require_prepare_intent(user_intent)
3349
- data = await x402.prepare_request(
3350
- backend=active_backend,
3351
- url=url.strip(),
3352
- method=method,
3353
- headers=headers,
3354
- query=query,
3355
- json_body=json_body,
3356
- text_body=text_body,
3357
- )
3358
- data["purpose"] = purpose.strip()
3359
- data = self._annotate_sensitive_payload(
3360
- data,
3361
- action_label="x402 paid request",
3362
- mode="prepare",
3363
- )
3364
- return AgentToolResult(tool=tool_name, ok=True, data=data)
3365
- preview = await x402.prepare_request(
3366
- backend=active_backend,
3367
- url=url.strip(),
3368
- method=method,
3369
- headers=headers,
3370
- query=query,
3371
- json_body=json_body,
3372
- text_body=text_body,
3373
- )
3374
- preview["purpose"] = purpose.strip()
3375
- preview = self._annotate_sensitive_payload(
3376
- preview,
3377
- action_label="x402 paid request",
3378
- mode="execute",
3379
- )
3380
- self._require_execute_approval(
3381
- approval_token=approval_token,
3382
- tool_name=tool_name,
3383
- summary=preview["confirmation_summary"],
3384
- action_label="x402 paid request",
3385
- backend=active_backend,
3386
- )
3387
- data = await x402.execute_request(
3355
+ data = await x402.pay_and_fetch(
3388
3356
  backend=active_backend,
3389
3357
  url=url.strip(),
3390
3358
  method=method,
@@ -3394,11 +3362,7 @@ class OpenClawWalletAdapter:
3394
3362
  text_body=text_body,
3395
3363
  )
3396
3364
  data["purpose"] = purpose.strip()
3397
- data = self._annotate_sensitive_payload(
3398
- data,
3399
- action_label="x402 paid request",
3400
- mode="execute",
3401
- )
3365
+ data = self._annotate_x402_payload(data, mode="execute")
3402
3366
  return AgentToolResult(tool=tool_name, ok=True, data=data)
3403
3367
 
3404
3368
  if tool_name == "get_wallet_capabilities":
@@ -4669,6 +4633,13 @@ class OpenClawWalletAdapter:
4669
4633
  data = await self.backend.get_kamino_lend_user_rewards(user=user)
4670
4634
  return AgentToolResult(tool=tool_name, ok=True, data=data)
4671
4635
 
4636
+ if tool_name == "get_kamino_open_positions":
4637
+ user = args.get("user")
4638
+ if user is not None and not isinstance(user, str):
4639
+ raise WalletBackendError("user must be a string when provided.")
4640
+ data = await self.backend.get_kamino_open_positions(user=user)
4641
+ return AgentToolResult(tool=tool_name, ok=True, data=data)
4642
+
4672
4643
  if tool_name == "sign_wallet_message":
4673
4644
  user_confirmed = args.get("user_confirmed")
4674
4645
  if user_confirmed is not True:
@@ -5231,13 +5202,6 @@ class OpenClawWalletAdapter:
5231
5202
  ),
5232
5203
  )
5233
5204
 
5234
- if tool_name == "request_devnet_airdrop":
5235
- amount = args.get("amount")
5236
- if not isinstance(amount, (int, float)) or amount <= 0:
5237
- raise WalletBackendError("amount must be a positive number.")
5238
- result = await self.backend.request_testnet_airdrop(float(amount))
5239
- return AgentToolResult(tool=tool_name, ok=True, data=result)
5240
-
5241
5205
  if tool_name == "transfer_spl_token":
5242
5206
  recipient = args.get("recipient")
5243
5207
  mint = args.get("mint")