@agentlayer.tech/wallet 0.1.65 → 0.1.66

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.
@@ -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.65",
5
+ "version": "0.1.66",
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.65",
3
+ "version": "0.1.66",
4
4
  "description": "OpenClaw plugin bridge for the AgentLayer wallet runtime.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN ../../../LICENSE",
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.65
1
+ 0.1.66
@@ -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.65"
5
+ __version__ = "0.1.66"
6
6
 
7
7
  __all__ = [
8
8
  "config",
@@ -3928,6 +3928,9 @@ class OpenClawWalletAdapter:
3928
3928
  raise WalletBackendError("text_body must be a string when provided.")
3929
3929
  if not isinstance(purpose, str) or not purpose.strip():
3930
3930
  raise WalletBackendError("purpose is required.")
3931
+ approved_preview = args.get("_approved_preview")
3932
+ if approved_preview is not None and not isinstance(approved_preview, dict):
3933
+ raise WalletBackendError("_approved_preview must be an object when provided.")
3931
3934
  data = await x402.pay_and_fetch(
3932
3935
  backend=active_backend,
3933
3936
  url=url.strip(),
@@ -3936,6 +3939,7 @@ class OpenClawWalletAdapter:
3936
3939
  query=query,
3937
3940
  json_body=json_body,
3938
3941
  text_body=text_body,
3942
+ approved_preview=approved_preview,
3939
3943
  )
3940
3944
  data["purpose"] = purpose.strip()
3941
3945
  data = self._annotate_x402_payload(data, mode="execute")
@@ -6,6 +6,7 @@ import base64
6
6
  import hashlib
7
7
  import json
8
8
  import logging
9
+ import time
9
10
  from typing import Any
10
11
  from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
11
12
 
@@ -17,6 +18,11 @@ from agent_wallet.wallet_layer.base import AgentWalletBackend
17
18
  CDP_BAZAAR_DISCOVERY_BASE_URL = "https://api.cdp.coinbase.com/platform/v2/x402/discovery"
18
19
  AGENTIC_MARKET_API_BASE_URL = "https://api.agentic.market/v1"
19
20
  X402_EXECUTE_TIMEOUT_SECONDS = 45.0
21
+ DISCOVERY_CACHE_TTL_SECONDS = 300.0
22
+ _DISCOVERY_CACHE_MAX_ENTRIES = 64
23
+ # Discovery responses cached as JSON text so hits hand out independent copies.
24
+ # Long-lived only inside the resident read worker; cold CLI runs start empty.
25
+ _discovery_cache: dict[str, tuple[float, str]] = {}
20
26
  SOLANA_CAIP_BY_NETWORK = {
21
27
  "mainnet": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
22
28
  }
@@ -24,6 +30,12 @@ EVM_CAIP_BY_NETWORK = {
24
30
  "ethereum": "eip155:1",
25
31
  "base": "eip155:8453",
26
32
  }
33
+ # x402 v1 requirements carry legacy network names instead of CAIP-2 ids.
34
+ LEGACY_NETWORK_TO_CAIP = {
35
+ "ethereum": "eip155:1",
36
+ "base": "eip155:8453",
37
+ "solana": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
38
+ }
27
39
  _USDC_IDENTIFIERS = {
28
40
  "usdc",
29
41
  "usd coin",
@@ -200,6 +212,38 @@ def _decode_payment_required(header_value: str) -> dict[str, Any]:
200
212
  }
201
213
 
202
214
 
215
+ def _caip_network(value: Any) -> str:
216
+ """Map a requirement network to CAIP-2 form, passing CAIP ids through."""
217
+ network = _trim(value)
218
+ if ":" in network:
219
+ return network
220
+ return LEGACY_NETWORK_TO_CAIP.get(network.lower(), network)
221
+
222
+
223
+ def _decode_payment_required_body(response: Any) -> dict[str, Any] | None:
224
+ """Parse a legacy x402 v1 402 response whose requirements live in the JSON body.
225
+
226
+ Returns the same shape as _decode_payment_required, with `encoded` set to a
227
+ base64 form the x402 SDK can decode (it detects the version from the JSON).
228
+ """
229
+ try:
230
+ payload = response.json()
231
+ except Exception:
232
+ return None
233
+ if not isinstance(payload, dict) or payload.get("x402Version") != 1:
234
+ return None
235
+ accepts = payload.get("accepts")
236
+ if not isinstance(accepts, list) or not accepts:
237
+ return None
238
+ encoded = base64.b64encode(json.dumps(payload).encode("utf-8")).decode("ascii")
239
+ return {
240
+ "x402_version": 1,
241
+ "accepts": accepts,
242
+ "raw": payload,
243
+ "encoded": encoded,
244
+ }
245
+
246
+
203
247
  def _extract_requirement_extra(requirement: dict[str, Any]) -> dict[str, Any]:
204
248
  extra = requirement.get("extra")
205
249
  return dict(extra) if isinstance(extra, dict) else {}
@@ -210,12 +254,24 @@ def _requirement_field(requirement: Any, field_name: str) -> Any:
210
254
  aliases = {
211
255
  "pay_to": ("pay_to", "payTo"),
212
256
  "max_timeout_seconds": ("max_timeout_seconds", "maxTimeoutSeconds"),
257
+ "amount": ("amount", "maxAmountRequired"),
213
258
  }
214
259
  for candidate in aliases.get(field_name, (field_name,)):
215
260
  if candidate in requirement:
216
261
  return requirement.get(candidate)
217
262
  return None
218
- return getattr(requirement, field_name, None)
263
+ value = getattr(requirement, field_name, None)
264
+ if value is None and field_name == "amount":
265
+ # v1 SDK models expose the amount as max_amount_required / get_amount().
266
+ getter = getattr(requirement, "get_amount", None)
267
+ if callable(getter):
268
+ return getter()
269
+ return value
270
+
271
+
272
+ def _requirement_amount(requirement: dict[str, Any]) -> str:
273
+ # x402 v2 uses "amount"; v1 uses "maxAmountRequired".
274
+ return _trim(requirement.get("amount") or requirement.get("maxAmountRequired"))
219
275
 
220
276
 
221
277
  def _looks_like_usdc(requirement: dict[str, Any]) -> bool:
@@ -227,7 +283,7 @@ def _looks_like_usdc(requirement: dict[str, Any]) -> bool:
227
283
 
228
284
 
229
285
  def _normalize_amount_hint(requirement: dict[str, Any]) -> str | None:
230
- amount = _trim(requirement.get("amount"))
286
+ amount = _requirement_amount(requirement)
231
287
  if not amount.isdigit():
232
288
  return None
233
289
  if _looks_like_usdc(requirement):
@@ -247,7 +303,7 @@ def normalize_payment_requirement(
247
303
  resource_url: str | None = None,
248
304
  ) -> dict[str, Any]:
249
305
  extra = _extract_requirement_extra(requirement)
250
- amount = _trim(requirement.get("amount"))
306
+ amount = _requirement_amount(requirement)
251
307
  return {
252
308
  "scheme": _trim(requirement.get("scheme")).lower() or None,
253
309
  "network": _trim(requirement.get("network")) or None,
@@ -405,7 +461,7 @@ def _wallet_x402_support_summary(backend: AgentWalletBackend) -> dict[str, Any]:
405
461
 
406
462
  def _requirement_compatibility(requirement: dict[str, Any], backend: AgentWalletBackend) -> dict[str, Any]:
407
463
  wallet_summary = _wallet_x402_support_summary(backend)
408
- network = _trim(requirement.get("network"))
464
+ network = _caip_network(requirement.get("network"))
409
465
  scheme = _trim(requirement.get("scheme")).lower()
410
466
  wallet_network_matches = network in wallet_summary["supported_caip_networks"]
411
467
  chain = _backend_chain(backend)
@@ -458,23 +514,24 @@ def _select_preferred_requirement(
458
514
  requirements: list[dict[str, Any]],
459
515
  backend: AgentWalletBackend,
460
516
  ) -> dict[str, Any] | None:
461
- compatible = [
462
- requirement
463
- for requirement in requirements
464
- if _requirement_compatibility(requirement, backend)["planned_execution_supported"]
465
- ]
466
- exact_match = [
467
- requirement
468
- for requirement in compatible
469
- if _requirement_compatibility(requirement, backend)["wallet_network_matches"]
470
- ]
517
+ compatible: list[dict[str, Any]] = []
518
+ exact_match: list[dict[str, Any]] = []
519
+ for requirement in requirements:
520
+ compatibility = _requirement_compatibility(requirement, backend)
521
+ if not compatibility["planned_execution_supported"]:
522
+ continue
523
+ compatible.append(requirement)
524
+ if compatibility["wallet_network_matches"]:
525
+ exact_match.append(requirement)
471
526
  candidates = exact_match or compatible
472
527
  if not candidates:
473
528
  return None
474
529
 
475
- def sort_key(item: dict[str, Any]) -> tuple[int, str]:
530
+ def sort_key(item: dict[str, Any]) -> tuple[int, int]:
476
531
  amount = _trim(item.get("amount"))
477
- return (0 if amount.isdigit() else 1, amount)
532
+ if amount.isdigit():
533
+ return (0, int(amount))
534
+ return (1, 0)
478
535
 
479
536
  return sorted(candidates, key=sort_key)[0]
480
537
 
@@ -605,14 +662,16 @@ def _parse_payment_required_response(
605
662
  wallet_summary: dict[str, Any],
606
663
  ) -> dict[str, Any]:
607
664
  payment_required = response.headers.get("PAYMENT-REQUIRED")
608
- if not payment_required:
609
- raise ProviderError(
610
- "x402-http",
611
- "Server returned HTTP 402 without a PAYMENT-REQUIRED header.",
612
- details={"status_code": response.status_code, "url": request["url"]},
613
- )
614
-
615
- decoded = _decode_payment_required(payment_required)
665
+ if payment_required:
666
+ decoded = _decode_payment_required(payment_required)
667
+ else:
668
+ decoded = _decode_payment_required_body(response)
669
+ if decoded is None:
670
+ raise ProviderError(
671
+ "x402-http",
672
+ "Server returned HTTP 402 without a PAYMENT-REQUIRED header or an x402 v1 JSON body.",
673
+ details={"status_code": response.status_code, "url": request["url"]},
674
+ )
616
675
  normalized_accepts = [
617
676
  normalize_payment_requirement(requirement, source="payment_required", resource_url=request["url"])
618
677
  for requirement in decoded["accepts"]
@@ -651,34 +710,6 @@ def _parse_payment_required_response(
651
710
  return preview
652
711
 
653
712
 
654
- def _require_executable_payment(
655
- *,
656
- preview: dict[str, Any],
657
- backend: AgentWalletBackend,
658
- ) -> dict[str, Any]:
659
- selected = preview.get("selected_payment")
660
- if not isinstance(selected, dict):
661
- raise ProviderError(
662
- "x402-http",
663
- "No compatible x402 payment requirement was selected for this wallet.",
664
- details={
665
- "request_url": preview.get("request_url"),
666
- "accepted_payments": preview.get("accepted_payments"),
667
- },
668
- )
669
- compatibility = _requirement_compatibility(selected, backend)
670
- if not compatibility["currently_executable"]:
671
- raise ProviderError(
672
- "x402-http",
673
- str(compatibility["reason"]),
674
- details={
675
- "selected_payment": selected,
676
- "compatibility": compatibility,
677
- },
678
- )
679
- return selected
680
-
681
-
682
713
  def _validate_payment_requirement(
683
714
  selected: dict[str, Any] | None,
684
715
  *,
@@ -717,7 +748,7 @@ def _validate_payment_requirement(
717
748
 
718
749
  chain = _backend_chain(backend) or "unknown"
719
750
  network = _backend_network(backend) or "unknown"
720
- requirement_network = _trim(selected.get("network")) or "unknown"
751
+ requirement_network = _caip_network(selected.get("network")) or "unknown"
721
752
  if chain == "solana" and requirement_network not in SOLANA_CAIP_BY_NETWORK.values():
722
753
  message = (
723
754
  f"This endpoint requires payment on {requirement_network}, but the active wallet is Solana ({network})."
@@ -1091,6 +1122,30 @@ def _log_x402_execute(
1091
1122
  )
1092
1123
 
1093
1124
 
1125
+ def _discovery_cache_get(cache_key: str) -> dict[str, Any] | None:
1126
+ entry = _discovery_cache.get(cache_key)
1127
+ if entry is None:
1128
+ return None
1129
+ expires_at, payload_text = entry
1130
+ if expires_at <= time.monotonic():
1131
+ _discovery_cache.pop(cache_key, None)
1132
+ return None
1133
+ return json.loads(payload_text)
1134
+
1135
+
1136
+ def _discovery_cache_set(cache_key: str, payload: dict[str, Any]) -> None:
1137
+ try:
1138
+ payload_text = json.dumps(payload)
1139
+ except (TypeError, ValueError):
1140
+ return
1141
+ if len(_discovery_cache) >= _DISCOVERY_CACHE_MAX_ENTRIES:
1142
+ for stale_key in sorted(_discovery_cache, key=lambda key: _discovery_cache[key][0])[
1143
+ : len(_discovery_cache) - _DISCOVERY_CACHE_MAX_ENTRIES + 1
1144
+ ]:
1145
+ _discovery_cache.pop(stale_key, None)
1146
+ _discovery_cache[cache_key] = (time.monotonic() + DISCOVERY_CACHE_TTL_SECONDS, payload_text)
1147
+
1148
+
1094
1149
  async def search_services(
1095
1150
  *,
1096
1151
  query: str | None = None,
@@ -1106,20 +1161,62 @@ async def search_services(
1106
1161
  provider = "cdp_bazaar"
1107
1162
  if limit <= 0:
1108
1163
  raise ProviderError("x402-discovery", "limit must be greater than zero.")
1164
+ cache_key = _canonical_json_text(
1165
+ {
1166
+ "op": "search_services",
1167
+ "provider": provider,
1168
+ "query": _trim(query),
1169
+ "network": _trim(network),
1170
+ "asset": _trim(asset),
1171
+ "scheme": _trim(scheme),
1172
+ "max_usd_price": _trim(max_usd_price),
1173
+ "limit": limit,
1174
+ }
1175
+ )
1176
+ cached = _discovery_cache_get(cache_key)
1177
+ if cached is not None:
1178
+ return cached
1179
+ result = await _search_services_uncached(
1180
+ query=query,
1181
+ provider=provider,
1182
+ network=network,
1183
+ asset=asset,
1184
+ scheme=scheme,
1185
+ max_usd_price=max_usd_price,
1186
+ limit=limit,
1187
+ )
1188
+ _discovery_cache_set(cache_key, result)
1189
+ return result
1190
+
1191
+
1192
+ async def _search_services_uncached(
1193
+ *,
1194
+ query: str | None,
1195
+ provider: str,
1196
+ network: str | None,
1197
+ asset: str | None,
1198
+ scheme: str | None,
1199
+ max_usd_price: str | None,
1200
+ limit: int,
1201
+ ) -> dict[str, Any]:
1109
1202
  client = get_client()
1110
1203
 
1111
1204
  if provider == "cdp_bazaar":
1112
1205
  if query and _trim(query):
1206
+ # CDP rejects empty filter values with HTTP 400, so omit unset params
1207
+ # entirely instead of sending them as blank strings.
1208
+ search_params: dict[str, Any] = {"query": _trim(query), "limit": min(limit, 20)}
1209
+ for name, value in (
1210
+ ("network", network),
1211
+ ("asset", asset),
1212
+ ("scheme", scheme),
1213
+ ("maxUsdPrice", max_usd_price),
1214
+ ):
1215
+ if _trim(value):
1216
+ search_params[name] = _trim(value)
1113
1217
  response = await client.get(
1114
1218
  f"{CDP_BAZAAR_DISCOVERY_BASE_URL}/search",
1115
- params={
1116
- "query": _trim(query),
1117
- "network": _trim(network) or None,
1118
- "asset": _trim(asset) or None,
1119
- "scheme": _trim(scheme) or None,
1120
- "maxUsdPrice": _trim(max_usd_price) or None,
1121
- "limit": min(limit, 20),
1122
- },
1219
+ params=search_params,
1123
1220
  )
1124
1221
  payload = _parse_json_response(
1125
1222
  response, provider="x402-cdp-bazaar", operation="CDP Bazaar search"
@@ -1214,19 +1311,40 @@ async def get_service_details(
1214
1311
  )
1215
1312
 
1216
1313
  if provider == "cdp_bazaar":
1217
- resources = await search_services(discovery_provider="cdp_bazaar", limit=200)
1218
- exact = next((item for item in resources["items"] if item.get("resource") == ref), None)
1219
- if exact is None:
1314
+
1315
+ def _match(items: list[dict[str, Any]]) -> dict[str, Any] | None:
1316
+ exact = next((item for item in items if item.get("resource") == ref), None)
1317
+ if exact is not None:
1318
+ return exact
1220
1319
  needle = ref.lower()
1221
- exact = next(
1320
+ return next(
1222
1321
  (
1223
1322
  item
1224
- for item in resources["items"]
1323
+ for item in items
1225
1324
  if needle in _trim(item.get("resource")).lower()
1226
1325
  or needle in _trim(item.get("description")).lower()
1227
1326
  ),
1228
1327
  None,
1229
1328
  )
1329
+
1330
+ # Server-side search covers the full Bazaar catalog; a plain resource
1331
+ # listing only ever returns the first page, so use it as a last resort.
1332
+ queries = [ref]
1333
+ if ref.startswith(("http://", "https://")):
1334
+ netloc = _trim(urlsplit(ref).netloc)
1335
+ if netloc and netloc != ref:
1336
+ queries.append(netloc)
1337
+ exact: dict[str, Any] | None = None
1338
+ for candidate_query in queries:
1339
+ results = await search_services(
1340
+ query=candidate_query, discovery_provider="cdp_bazaar", limit=20
1341
+ )
1342
+ exact = _match(results["items"])
1343
+ if exact is not None:
1344
+ break
1345
+ if exact is None:
1346
+ resources = await search_services(discovery_provider="cdp_bazaar", limit=200)
1347
+ exact = _match(resources["items"])
1230
1348
  if exact is None:
1231
1349
  raise ProviderError("x402-cdp-bazaar", f"No Bazaar resource matched: {ref}")
1232
1350
  return {"discovery_provider": provider, "service": exact}
@@ -1330,78 +1448,30 @@ async def preview_request(
1330
1448
  return payment_preview
1331
1449
 
1332
1450
 
1333
- async def prepare_request(
1451
+ def _reusable_approved_preview(
1452
+ approved_preview: Any,
1334
1453
  *,
1335
- backend: AgentWalletBackend,
1336
- url: str,
1337
- method: str = "GET",
1338
- headers: dict[str, Any] | None = None,
1339
- query: dict[str, Any] | None = None,
1340
- json_body: Any | None = None,
1341
- text_body: str | None = None,
1342
- ) -> dict[str, Any]:
1343
- preview = await preview_request(
1344
- backend=backend,
1345
- url=url,
1346
- method=method,
1347
- headers=headers,
1348
- query=query,
1349
- json_body=json_body,
1350
- text_body=text_body,
1351
- )
1352
- if not preview.get("payment_required"):
1353
- prepared = dict(preview)
1354
- prepared["mode"] = "prepare"
1355
- prepared["prepared"] = False
1356
- prepared["prepare_note"] = "The endpoint did not require x402 payment for this request."
1357
- return prepared
1358
-
1359
- selected_payment = _require_executable_payment(preview=preview, backend=backend)
1360
- payment_required_header = (
1361
- dict(preview.get("response_headers") or {}).get("payment-required")
1362
- )
1363
- if not isinstance(payment_required_header, str) or not payment_required_header.strip():
1364
- raise ProviderError("x402-http", "Missing PAYMENT-REQUIRED header in preview state.")
1365
- # Create the payload once during prepare to validate that the active wallet can sign it.
1366
- await _create_payment_headers(
1367
- backend=backend,
1368
- payment_required_header=payment_required_header,
1369
- selected_payment=selected_payment,
1370
- )
1371
- prepared = dict(preview)
1372
- prepared["mode"] = "prepare"
1373
- prepared["prepared"] = True
1374
- prepared["signed"] = False
1375
- prepared["broadcasted"] = False
1376
- prepared["confirmed"] = False
1377
- prepared["payment_payload_withheld"] = True
1378
- prepared["prepare_note"] = (
1379
- "x402 payment authorization was validated locally, but the PAYMENT-SIGNATURE header is withheld until execute."
1380
- )
1381
- return prepared
1382
-
1383
-
1384
- async def execute_request(
1385
- *,
1386
- backend: AgentWalletBackend,
1387
- url: str,
1388
- method: str = "GET",
1389
- headers: dict[str, Any] | None = None,
1390
- query: dict[str, Any] | None = None,
1391
- json_body: Any | None = None,
1392
- text_body: str | None = None,
1393
- ) -> dict[str, Any]:
1394
- executed = await pay_and_fetch(
1395
- backend=backend,
1396
- url=url,
1397
- method=method,
1398
- headers=headers,
1399
- query=query,
1400
- json_body=json_body,
1401
- text_body=text_body,
1402
- )
1403
- executed["mode"] = "execute"
1404
- return executed
1454
+ request: dict[str, Any],
1455
+ ) -> dict[str, Any] | None:
1456
+ """Return an approval-time preview usable instead of a fresh unpaid probe.
1457
+
1458
+ Reuse is only safe when the preview describes exactly this request
1459
+ (fingerprint covers method, final URL, and body hash) and still carries the
1460
+ encoded PAYMENT-REQUIRED challenge plus a selected payment. Anything else
1461
+ falls back to a fresh probe.
1462
+ """
1463
+ if not isinstance(approved_preview, dict):
1464
+ return None
1465
+ if not approved_preview.get("payment_required"):
1466
+ return None
1467
+ if _trim(approved_preview.get("request_fingerprint")) != request["request_fingerprint"]:
1468
+ return None
1469
+ if not isinstance(approved_preview.get("selected_payment"), dict):
1470
+ return None
1471
+ header = dict(approved_preview.get("response_headers") or {}).get("payment-required")
1472
+ if not isinstance(header, str) or not header.strip():
1473
+ return None
1474
+ return dict(approved_preview)
1405
1475
 
1406
1476
 
1407
1477
  async def pay_and_fetch(
@@ -1413,9 +1483,9 @@ async def pay_and_fetch(
1413
1483
  query: dict[str, Any] | None = None,
1414
1484
  json_body: Any | None = None,
1415
1485
  text_body: str | None = None,
1486
+ approved_preview: dict[str, Any] | None = None,
1416
1487
  ) -> dict[str, Any]:
1417
- preview = await preview_request(
1418
- backend=backend,
1488
+ request = _build_request_metadata(
1419
1489
  url=url,
1420
1490
  method=method,
1421
1491
  headers=headers,
@@ -1423,6 +1493,18 @@ async def pay_and_fetch(
1423
1493
  json_body=json_body,
1424
1494
  text_body=text_body,
1425
1495
  )
1496
+ preview = _reusable_approved_preview(approved_preview, request=request)
1497
+ reused_approved_preview = preview is not None
1498
+ if preview is None:
1499
+ preview = await preview_request(
1500
+ backend=backend,
1501
+ url=url,
1502
+ method=method,
1503
+ headers=headers,
1504
+ query=query,
1505
+ json_body=json_body,
1506
+ text_body=text_body,
1507
+ )
1426
1508
  if not preview.get("payment_required"):
1427
1509
  executed = dict(preview)
1428
1510
  executed["mode"] = "execute"
@@ -1444,14 +1526,6 @@ async def pay_and_fetch(
1444
1526
  if not isinstance(payment_required_header, str) or not payment_required_header.strip():
1445
1527
  raise ProviderError("x402-http", "Missing PAYMENT-REQUIRED header in preview state.")
1446
1528
 
1447
- request = _build_request_metadata(
1448
- url=url,
1449
- method=method,
1450
- headers=headers,
1451
- query=query,
1452
- json_body=json_body,
1453
- text_body=text_body,
1454
- )
1455
1529
  _validate_request_execution_policy(request=request, backend=backend)
1456
1530
  payment_headers = await _create_payment_headers(
1457
1531
  backend=backend,
@@ -1478,6 +1552,7 @@ async def pay_and_fetch(
1478
1552
  {
1479
1553
  "mode": "execute",
1480
1554
  "paid": True,
1555
+ "reused_approved_preview": reused_approved_preview,
1481
1556
  "broadcasted": bool(settlement and settlement.get("transaction")),
1482
1557
  "confirmed": bool(settlement and settlement.get("success")),
1483
1558
  "payment_settlement": settlement,
@@ -1491,9 +1566,15 @@ async def pay_and_fetch(
1491
1566
  }
1492
1567
  )
1493
1568
  if response.status_code == 402:
1569
+ message = "The paid x402 retry still returned HTTP 402."
1570
+ if reused_approved_preview:
1571
+ message += (
1572
+ " The payment was signed from the approved preview quote; the server may have"
1573
+ " re-priced the endpoint since then. Run x402_preview_request again and retry."
1574
+ )
1494
1575
  raise ProviderError(
1495
1576
  "x402-http",
1496
- "The paid x402 retry still returned HTTP 402.",
1577
+ message,
1497
1578
  details={
1498
1579
  "request_url": request["url"],
1499
1580
  "selected_payment": selected_payment,
@@ -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.65",
5
+ "version": "0.1.66",
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.65"
7
+ version = "0.1.66"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wallet-operator
3
- description: Use when operating OpenClaw wallet tools: balances, transfers, swaps, LI.FI cross-chain swaps, Jupiter swaps, Velora EVM swaps, BTC transfers, staking, Kamino lending, Bags launches, and wallet execution safety.
3
+ description: Use whenever the user's own funds are involved, even if they never say "wallet": balances, portfolio, transfers, swaps, LI.FI cross-chain swaps, Jupiter swaps, Velora EVM swaps, BTC transfers, staking, Kamino lending, Bags launches, and wallet execution safety.
4
4
  ---
5
5
 
6
6
  # Wallet Operator
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
3
  "displayName": "Agent Wallet",
4
- "version": "0.1.65",
4
+ "version": "0.1.66",
5
5
  "description": "Claude Code bridge for the existing AgentLayer wallet runtime. Connects to Solana, Bitcoin, and EVM wallets without creating a new one.",
6
6
  "author": {
7
7
  "name": "AgentLayer"
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Use when the user asks Claude Code to interact with the local AgentLayer wallet runtime: check balances, transfer tokens, swap, DeFi operations, or x402 payments. Prefer wallet tools over shell commands. Preview writes first and execute only after explicit user confirmation."
2
+ description: "Use whenever the user's own funds or wallet are involved — balances, portfolio, transfers, swaps, bridging, staking, lending, or x402 payments — even if the user never says \"wallet\". Trigger phrases include \"my balance\", \"how much SOL/ETH/BTC/USDC do I have\", \"send/transfer X to\", \"swap\", \"bridge\", \"stake\", \"my portfolio\", \"pay for this API\", \"x402\". Prefer agent-wallet MCP tools over shell commands, raw RPC, or crypto data servers. Preview writes first and execute only after explicit user confirmation."
3
3
  ---
4
4
 
5
5
  # Agent Wallet Operator
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
4
4
  "description": "Codex plugin bridge for the AgentLayer wallet runtime.",
5
5
  "author": {
6
6
  "name": "AgentLayer"
@@ -273,6 +273,44 @@ def _cache_preview_for_approval(user_id: str, tool_name: str, payload: dict[str,
273
273
  }
274
274
 
275
275
 
276
+ X402_PREVIEW_REUSE_TTL_SECONDS = 300
277
+
278
+
279
+ def _cache_x402_preview_payload(user_id: str, payload: dict[str, Any]) -> None:
280
+ """Cache a paid-endpoint preview so x402_pay_request can skip its unpaid probe.
281
+
282
+ x402 previews run on the read-only resident-worker path, so they never reach
283
+ _cache_preview_for_approval (and preview-mode annotation strips
284
+ confirmation_summary anyway). Stored under the x402_preview_request key that
285
+ the pay alias resolves to. The wallet runtime revalidates the payload by
286
+ request fingerprint and falls back to a fresh probe on any mismatch, so a
287
+ stale or unrelated cached preview can never redirect a payment.
288
+ """
289
+ if not isinstance(payload, dict) or payload.get("ok") is False:
290
+ return
291
+ data = payload.get("data")
292
+ if not isinstance(data, dict):
293
+ return
294
+ if not data.get("payment_required") or not str(data.get("request_fingerprint") or "").strip():
295
+ return
296
+ with _approval_cache_lock:
297
+ _prune_approval_preview_cache()
298
+ approval_preview_cache[_approval_cache_key(user_id, "x402_preview_request")] = {
299
+ "digest": _preview_digest(data),
300
+ "expires_at": time.time() + X402_PREVIEW_REUSE_TTL_SECONDS,
301
+ "preview": data,
302
+ "summary": None,
303
+ }
304
+
305
+
306
+ def _attach_x402_approved_preview(tool_name: str, effective_params: dict[str, Any]) -> None:
307
+ if tool_name != "x402_pay_request" or "_approved_preview" in effective_params:
308
+ return
309
+ cached = _latest_cached_preview(_user_id(), tool_name)
310
+ if cached and isinstance(cached.get("preview"), dict):
311
+ effective_params["_approved_preview"] = cached["preview"]
312
+
313
+
276
314
  def _latest_cached_preview(user_id: str, tool_name: str) -> dict[str, Any] | None:
277
315
  with _approval_cache_lock:
278
316
  _prune_approval_preview_cache()
@@ -1403,6 +1441,7 @@ def _invoke_wallet_tool_blocking(
1403
1441
  cancellation stay responsive).
1404
1442
  """
1405
1443
  used_cache, approval_args = _attach_approval_for_execute(tool_name, config, effective_params)
1444
+ _attach_x402_approved_preview(tool_name, effective_params)
1406
1445
  try:
1407
1446
  payload = _invoke_tool(
1408
1447
  tool_name,
@@ -1413,6 +1452,10 @@ def _invoke_wallet_tool_blocking(
1413
1452
  )
1414
1453
  except Exception as exc:
1415
1454
  raise _normalize_approval_context_error(exc) from exc
1455
+ if tool_name == "x402_pay_request":
1456
+ # The cached x402 preview quote is single-use: whether the payment
1457
+ # succeeded or the endpoint re-priced, the next pay must re-preview.
1458
+ _consume_cached_preview(_user_id(), tool_name)
1416
1459
  _cache_preview_for_approval(_user_id(), tool_name, payload)
1417
1460
  # A bridge-managed preview that was just executed successfully is single-use:
1418
1461
  # drop it so a duplicate execute cannot silently re-run the operation.
@@ -1451,15 +1494,21 @@ async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[st
1451
1494
  _invoke_wallet_tool_blocking, tool_name, config, effective_params
1452
1495
  )
1453
1496
 
1497
+ if tool_name == "x402_preview_request":
1498
+ _cache_x402_preview_payload(_user_id(), payload)
1454
1499
  if payload.get("ok") is False:
1455
1500
  raise RuntimeError(str(payload.get("error") or f"{tool_name} failed"))
1456
1501
  return payload.get("data", {})
1457
1502
 
1458
1503
 
1459
1504
  BASE_INSTRUCTIONS = (
1460
- "Use the local AgentLayer wallet runtime through explicit wallet tools. Keep wallet "
1461
- "secrets local. Preview writes first when supported, and execute only after explicit "
1462
- "user confirmation."
1505
+ "This server is the user's own local AgentLayer wallet (Solana, EVM, Bitcoin). Use its "
1506
+ "tools whenever the user asks about their balances, portfolio, addresses, transfers, "
1507
+ "swaps, bridging, staking, lending, or x402 payments — even when they never say the "
1508
+ "word 'wallet'. For anything touching the user's own funds, prefer these tools over "
1509
+ "shell commands, raw RPC calls, or other crypto data servers (those are for arbitrary "
1510
+ "addresses and market data, not the user's wallet). Keep wallet secrets local. Preview "
1511
+ "writes first when supported, and execute only after explicit user confirmation."
1463
1512
  )
1464
1513
 
1465
1514
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "wallet-operator"
3
- description: "Use when the user asks Codex to interact with the local AgentLayer wallet runtime. Prefer wallet tools over shell commands, preview writes first, and keep approval/signing semantics intact."
3
+ description: "Use whenever the user's own funds or wallet are involved — balances, portfolio, transfers, swaps, bridging, staking, lending, or x402 payments — even if the user never says \"wallet\". Trigger phrases include \"my balance\", \"how much SOL/ETH/BTC/USDC do I have\", \"send/transfer X to\", \"swap\", \"bridge\", \"stake\", \"my portfolio\". Prefer wallet tools over shell commands, preview writes first, and keep approval/signing semantics intact."
4
4
  ---
5
5
 
6
6
  # Agent Wallet Operator
@@ -1,5 +1,5 @@
1
1
  name: agent-wallet
2
- version: 0.1.65
2
+ version: 0.1.66
3
3
  description: Thin Hermes Agent bridge to the existing AgentLayer/OpenClaw wallet backend
4
4
  provides_tools:
5
5
  - agent_wallet_tools
@@ -120,9 +120,15 @@ if [ -z "$SOURCE_ROOT" ] || [ ! -d "$SOURCE_ROOT" ]; then
120
120
  exit 1
121
121
  fi
122
122
 
123
+ # Pre-flight must be a superset of setup.sh's require_path list: everything is
124
+ # checked on the EXTRACTED temp tree, before the destructive swap below. A
125
+ # bundle that would fail setup.sh must never replace the user's runtime.
123
126
  require_path "$SOURCE_ROOT/setup.sh" "local setup entrypoint"
124
127
  require_path "$SOURCE_ROOT/agent-wallet" "agent-wallet package"
128
+ require_path "$SOURCE_ROOT/agent-wallet/scripts/install_agent_wallet.py" "Python installer"
125
129
  require_path "$SOURCE_ROOT/.openclaw/extensions/agent-wallet" "OpenClaw extension"
130
+ require_path "$SOURCE_ROOT/codex/plugins/agent-wallet/.codex-plugin/plugin.json" "Codex plugin"
131
+ require_path "$SOURCE_ROOT/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json" "Claude Code plugin"
126
132
  require_path "$SOURCE_ROOT/wdk-btc-wallet/package.json" "wdk-btc-wallet runtime"
127
133
  require_path "$SOURCE_ROOT/wdk-evm-wallet/package.json" "wdk-evm-wallet runtime"
128
134
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
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.65",
3
+ "version": "0.1.66",
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.65",
3
+ "version": "0.1.66",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate EVM wallet service built on Tether WDK.",