@agentlayer.tech/wallet 0.1.16 → 0.1.18

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.
@@ -0,0 +1,1323 @@
1
+ """x402 discovery, preview, and buyer execution helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import json
8
+ from typing import Any
9
+ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
10
+
11
+ from agent_wallet.config import resolve_solana_rpc_url
12
+ from agent_wallet.exceptions import ProviderError
13
+ from agent_wallet.http_client import get_client
14
+ from agent_wallet.wallet_layer.base import AgentWalletBackend
15
+
16
+ CDP_BAZAAR_DISCOVERY_BASE_URL = "https://api.cdp.coinbase.com/platform/v2/x402/discovery"
17
+ AGENTIC_MARKET_API_BASE_URL = "https://api.agentic.market/v1"
18
+ SOLANA_CAIP_BY_NETWORK = {
19
+ "mainnet": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
20
+ "devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
21
+ }
22
+ EVM_CAIP_BY_NETWORK = {
23
+ "ethereum": "eip155:1",
24
+ "base": "eip155:8453",
25
+ "sepolia": "eip155:11155111",
26
+ "base-sepolia": "eip155:84532",
27
+ }
28
+ _USDC_IDENTIFIERS = {
29
+ "usdc",
30
+ "usd coin",
31
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
32
+ "0x036cbd53842c5426634e7929541ec2318f3dcf7e",
33
+ "epjfwdd5aufqssqem2qn1xzybapc8g4wegkgkzwytdt1v",
34
+ }
35
+
36
+
37
+ def _backend_chain(backend: AgentWalletBackend) -> str:
38
+ chain = _trim(getattr(backend, "chain", "")).lower()
39
+ if chain:
40
+ return chain
41
+ try:
42
+ capabilities = backend.get_capabilities()
43
+ except Exception:
44
+ return ""
45
+ return _trim(getattr(capabilities, "chain", "")).lower()
46
+
47
+
48
+ def _backend_network(backend: AgentWalletBackend) -> str:
49
+ return _trim(getattr(backend, "network", "")).lower()
50
+
51
+
52
+ def _backend_solana_sdk_rpc_url(backend: AgentWalletBackend) -> str | None:
53
+ candidates = getattr(backend, "rpc_urls", None)
54
+ if isinstance(candidates, list):
55
+ for candidate in candidates:
56
+ text = _trim(candidate)
57
+ if text.startswith(("http://", "https://")):
58
+ return text
59
+ primary = _trim(getattr(backend, "rpc_url", None))
60
+ if primary.startswith(("http://", "https://")):
61
+ return primary
62
+ network = _backend_network(backend)
63
+ fallback = resolve_solana_rpc_url(network or "mainnet", "")
64
+ return _trim(fallback) or None
65
+
66
+
67
+ def _trim(value: Any) -> str:
68
+ return str(value or "").strip()
69
+
70
+
71
+ def _canonical_json_text(payload: Any) -> str:
72
+ return json.dumps(payload, sort_keys=True, separators=(",", ":"))
73
+
74
+
75
+ def _hash_text(text: str) -> str:
76
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
77
+
78
+
79
+ def _normalize_discovery_provider(value: Any) -> str:
80
+ provider = _trim(value).lower() or "auto"
81
+ aliases = {
82
+ "bazaar": "cdp_bazaar",
83
+ "cdp": "cdp_bazaar",
84
+ "agentic": "agentic_market",
85
+ "agenticmarket": "agentic_market",
86
+ "market": "agentic_market",
87
+ }
88
+ provider = aliases.get(provider, provider)
89
+ if provider not in {"auto", "cdp_bazaar", "agentic_market"}:
90
+ raise ProviderError("x402-discovery", f"Unsupported discovery provider: {provider}")
91
+ return provider
92
+
93
+
94
+ def _normalize_http_method(value: Any) -> str:
95
+ method = _trim(value).upper() or "GET"
96
+ if method not in {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}:
97
+ raise ProviderError("x402-http", f"Unsupported HTTP method: {method}")
98
+ return method
99
+
100
+
101
+ def _normalize_headers(value: Any) -> dict[str, str]:
102
+ if value is None:
103
+ return {}
104
+ if not isinstance(value, dict):
105
+ raise ProviderError("x402-http", "headers must be an object when provided.")
106
+ headers: dict[str, str] = {}
107
+ for key, raw_value in value.items():
108
+ name = _trim(key)
109
+ if not name:
110
+ raise ProviderError("x402-http", "headers must not contain empty names.")
111
+ headers[name] = str(raw_value)
112
+ return headers
113
+
114
+
115
+ def _normalize_query_params(value: Any) -> dict[str, str]:
116
+ if value is None:
117
+ return {}
118
+ if not isinstance(value, dict):
119
+ raise ProviderError("x402-http", "query must be an object when provided.")
120
+ params: dict[str, str] = {}
121
+ for key, raw_value in value.items():
122
+ name = _trim(key)
123
+ if not name:
124
+ raise ProviderError("x402-http", "query must not contain empty names.")
125
+ params[name] = str(raw_value)
126
+ return params
127
+
128
+
129
+ def _append_query(url: str, query: dict[str, str]) -> str:
130
+ if not query:
131
+ return url
132
+ parts = urlsplit(url)
133
+ merged = dict(parse_qsl(parts.query, keep_blank_values=True))
134
+ merged.update(query)
135
+ return urlunsplit(
136
+ (parts.scheme, parts.netloc, parts.path, urlencode(merged, doseq=True), parts.fragment)
137
+ )
138
+
139
+
140
+ def _response_text(response: Any) -> str:
141
+ try:
142
+ text = response.text
143
+ except Exception:
144
+ return ""
145
+ return str(text or "")
146
+
147
+
148
+ def _parse_json_response(response: Any, *, provider: str, operation: str) -> Any:
149
+ body = _response_text(response)
150
+ if not body.strip():
151
+ raise ProviderError(provider, f"{operation} returned an empty response body.")
152
+ try:
153
+ return response.json()
154
+ except ValueError as exc:
155
+ snippet = body.strip().replace("\n", " ")[:200]
156
+ detail = f": {snippet}" if snippet else ""
157
+ raise ProviderError(provider, f"{operation} returned invalid JSON{detail}") from exc
158
+
159
+
160
+ def _decode_payment_required(header_value: str) -> dict[str, Any]:
161
+ raw = _trim(header_value)
162
+ if not raw:
163
+ raise ProviderError("x402-http", "PAYMENT-REQUIRED header is empty.")
164
+ decoded_bytes: bytes | None = None
165
+ for decoder in (base64.b64decode, base64.urlsafe_b64decode):
166
+ try:
167
+ padding = "=" * (-len(raw) % 4)
168
+ decoded_bytes = decoder(raw + padding)
169
+ break
170
+ except Exception:
171
+ continue
172
+ if decoded_bytes is None:
173
+ raise ProviderError("x402-http", "PAYMENT-REQUIRED header is not valid base64.")
174
+ try:
175
+ payload = json.loads(decoded_bytes.decode("utf-8"))
176
+ except Exception as exc:
177
+ raise ProviderError("x402-http", "PAYMENT-REQUIRED header is not valid JSON.") from exc
178
+ if isinstance(payload, list):
179
+ accepts = payload
180
+ x402_version = None
181
+ elif isinstance(payload, dict):
182
+ accepts = payload.get("accepts")
183
+ x402_version = payload.get("x402Version")
184
+ else:
185
+ raise ProviderError("x402-http", "PAYMENT-REQUIRED payload must decode to JSON.")
186
+ if not isinstance(accepts, list) or not accepts:
187
+ raise ProviderError("x402-http", "PAYMENT-REQUIRED payload does not contain accepts[].")
188
+ return {
189
+ "x402_version": x402_version,
190
+ "accepts": accepts,
191
+ "raw": payload,
192
+ "encoded": raw,
193
+ }
194
+
195
+
196
+ def _extract_requirement_extra(requirement: dict[str, Any]) -> dict[str, Any]:
197
+ extra = requirement.get("extra")
198
+ return dict(extra) if isinstance(extra, dict) else {}
199
+
200
+
201
+ def _requirement_field(requirement: Any, field_name: str) -> Any:
202
+ if isinstance(requirement, dict):
203
+ aliases = {
204
+ "pay_to": ("pay_to", "payTo"),
205
+ "max_timeout_seconds": ("max_timeout_seconds", "maxTimeoutSeconds"),
206
+ }
207
+ for candidate in aliases.get(field_name, (field_name,)):
208
+ if candidate in requirement:
209
+ return requirement.get(candidate)
210
+ return None
211
+ return getattr(requirement, field_name, None)
212
+
213
+
214
+ def _looks_like_usdc(requirement: dict[str, Any]) -> bool:
215
+ asset = _trim(requirement.get("asset")).lower()
216
+ if asset in _USDC_IDENTIFIERS:
217
+ return True
218
+ extra = _extract_requirement_extra(requirement)
219
+ return _trim(extra.get("name")).lower() in _USDC_IDENTIFIERS
220
+
221
+
222
+ def _normalize_amount_hint(requirement: dict[str, Any]) -> str | None:
223
+ amount = _trim(requirement.get("amount"))
224
+ if not amount.isdigit():
225
+ return None
226
+ if _looks_like_usdc(requirement):
227
+ raw = int(amount)
228
+ whole = raw // 1_000_000
229
+ fraction = raw % 1_000_000
230
+ if fraction:
231
+ return f"{whole}.{fraction:06d}".rstrip("0").rstrip(".")
232
+ return f"{whole}"
233
+ return None
234
+
235
+
236
+ def normalize_payment_requirement(
237
+ requirement: dict[str, Any],
238
+ *,
239
+ source: str,
240
+ resource_url: str | None = None,
241
+ ) -> dict[str, Any]:
242
+ extra = _extract_requirement_extra(requirement)
243
+ amount = _trim(requirement.get("amount"))
244
+ return {
245
+ "scheme": _trim(requirement.get("scheme")).lower() or None,
246
+ "network": _trim(requirement.get("network")) or None,
247
+ "asset": _trim(requirement.get("asset")) or None,
248
+ "amount": amount or None,
249
+ "amount_display": _normalize_amount_hint(requirement),
250
+ "pay_to": _trim(requirement.get("payTo")) or None,
251
+ "max_timeout_seconds": requirement.get("maxTimeoutSeconds"),
252
+ "resource_url": resource_url,
253
+ "extra": extra,
254
+ "source": source,
255
+ "raw": requirement,
256
+ }
257
+
258
+
259
+ def _normalize_cdp_resource(item: dict[str, Any]) -> dict[str, Any]:
260
+ metadata = item.get("metadata")
261
+ metadata_dict = dict(metadata) if isinstance(metadata, dict) else {}
262
+ accepts = item.get("accepts") if isinstance(item.get("accepts"), list) else []
263
+ resource = _trim(item.get("resource"))
264
+ return {
265
+ "discovery_provider": "cdp_bazaar",
266
+ "resource": resource,
267
+ "type": _trim(item.get("type")) or "http",
268
+ "x402_version": item.get("x402Version"),
269
+ "description": _trim(item.get("description") or metadata_dict.get("description")) or None,
270
+ "last_updated": item.get("lastUpdated"),
271
+ "accepts": [
272
+ normalize_payment_requirement(requirement, source="cdp_bazaar", resource_url=resource)
273
+ for requirement in accepts
274
+ if isinstance(requirement, dict)
275
+ ],
276
+ "metadata": metadata_dict,
277
+ "raw": item,
278
+ }
279
+
280
+
281
+ def _normalize_agentic_service(service: dict[str, Any]) -> dict[str, Any]:
282
+ endpoints = service.get("endpoints") if isinstance(service.get("endpoints"), list) else []
283
+ normalized_endpoints: list[dict[str, Any]] = []
284
+ accepts: list[dict[str, Any]] = []
285
+ for endpoint in endpoints:
286
+ if not isinstance(endpoint, dict):
287
+ continue
288
+ pricing = endpoint.get("pricing") if isinstance(endpoint.get("pricing"), dict) else {}
289
+ normalized = {
290
+ "url": _trim(endpoint.get("url")) or None,
291
+ "description": _trim(endpoint.get("description")) or None,
292
+ "method": _normalize_http_method(endpoint.get("method")),
293
+ "pricing": {
294
+ "amount": _trim(pricing.get("amount")) or None,
295
+ "currency": _trim(pricing.get("currency")) or None,
296
+ "network": _trim(pricing.get("network")) or None,
297
+ },
298
+ }
299
+ normalized_endpoints.append(normalized)
300
+ amount_text = _trim(pricing.get("amount"))
301
+ currency = _trim(pricing.get("currency")).lower()
302
+ network = _trim(pricing.get("network")).lower()
303
+ if amount_text and currency == "usdc":
304
+ accepts.append(
305
+ {
306
+ "scheme": "exact",
307
+ "network": EVM_CAIP_BY_NETWORK.get(network, SOLANA_CAIP_BY_NETWORK.get(network, network)),
308
+ "asset": "USDC",
309
+ "amount": amount_text,
310
+ "amount_display": amount_text,
311
+ "pay_to": None,
312
+ "max_timeout_seconds": None,
313
+ "resource_url": normalized["url"],
314
+ "extra": {
315
+ "marketplace": "agentic_market",
316
+ "pricingCurrency": pricing.get("currency"),
317
+ "pricingNetwork": pricing.get("network"),
318
+ },
319
+ "source": "agentic_market",
320
+ "raw": endpoint,
321
+ }
322
+ )
323
+ return {
324
+ "discovery_provider": "agentic_market",
325
+ "service_id": _trim(service.get("id")) or None,
326
+ "service_name": _trim(service.get("name")) or None,
327
+ "description": _trim(service.get("description")) or None,
328
+ "domain": _trim(service.get("domain")) or None,
329
+ "category": _trim(service.get("category")) or None,
330
+ "networks": [str(item) for item in service.get("networks") or []],
331
+ "integration_type": _trim(service.get("integrationType")) or None,
332
+ "is_new": bool(service.get("isNew")),
333
+ "endpoints": normalized_endpoints,
334
+ "accepts": accepts,
335
+ "raw": service,
336
+ }
337
+
338
+
339
+ def _wallet_caip_networks(backend: AgentWalletBackend) -> list[str]:
340
+ chain = _backend_chain(backend)
341
+ network = _backend_network(backend)
342
+ if chain == "evm":
343
+ caip = EVM_CAIP_BY_NETWORK.get(network)
344
+ return [caip] if caip else []
345
+ if chain == "solana":
346
+ caip = SOLANA_CAIP_BY_NETWORK.get(network)
347
+ return [caip] if caip else []
348
+ return []
349
+
350
+
351
+ def _solana_exact_execution_supported(backend: AgentWalletBackend) -> bool:
352
+ return (
353
+ _backend_chain(backend) == "solana"
354
+ and _backend_network(backend) in {"mainnet", "devnet"}
355
+ and getattr(backend, "signer", None) is not None
356
+ )
357
+
358
+
359
+ def _evm_exact_execution_supported(backend: AgentWalletBackend) -> bool:
360
+ return (
361
+ _backend_chain(backend) == "evm"
362
+ and _backend_network(backend) in {"base", "base-sepolia"}
363
+ and callable(getattr(backend, "sign_x402_evm_exact_typed_data", None))
364
+ )
365
+
366
+
367
+ def _evm_payment_requirement_supported(requirement: dict[str, Any]) -> bool:
368
+ if _trim(requirement.get("scheme")).lower() != "exact":
369
+ return False
370
+ extra = _extract_requirement_extra(requirement)
371
+ transfer_method = _trim(extra.get("assetTransferMethod")).lower()
372
+ return transfer_method in {"", "eip3009", "transferwithauthorization"}
373
+
374
+
375
+ def _wallet_x402_support_summary(backend: AgentWalletBackend) -> dict[str, Any]:
376
+ chain = _backend_chain(backend)
377
+ network = _backend_network(backend)
378
+ supported_networks = _wallet_caip_networks(backend)
379
+ planned_execution_networks = {
380
+ "eip155:8453",
381
+ "eip155:84532",
382
+ "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
383
+ "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
384
+ }
385
+ execution_modes: list[str] = []
386
+ if _solana_exact_execution_supported(backend):
387
+ execution_modes.append("solana_exact")
388
+ if _evm_exact_execution_supported(backend):
389
+ execution_modes.append("evm_exact")
390
+ return {
391
+ "chain": chain,
392
+ "network": network,
393
+ "supported_caip_networks": supported_networks,
394
+ "wallet_type_supported": chain in {"evm", "solana"},
395
+ "execution_available": bool(execution_modes),
396
+ "execution_modes": execution_modes,
397
+ "planned_execution_networks": sorted(planned_execution_networks),
398
+ }
399
+
400
+
401
+ def _requirement_compatibility(requirement: dict[str, Any], backend: AgentWalletBackend) -> dict[str, Any]:
402
+ wallet_summary = _wallet_x402_support_summary(backend)
403
+ network = _trim(requirement.get("network"))
404
+ scheme = _trim(requirement.get("scheme")).lower()
405
+ wallet_network_matches = network in wallet_summary["supported_caip_networks"]
406
+ chain = _backend_chain(backend)
407
+ planned_execution_supported = False
408
+ currently_executable = False
409
+ if chain == "solana":
410
+ planned_execution_supported = scheme == "exact" and network in set(
411
+ wallet_summary["planned_execution_networks"]
412
+ )
413
+ currently_executable = (
414
+ planned_execution_supported
415
+ and wallet_network_matches
416
+ and _solana_exact_execution_supported(backend)
417
+ )
418
+ elif chain == "evm":
419
+ planned_execution_supported = (
420
+ scheme == "exact"
421
+ and network in set(wallet_summary["planned_execution_networks"])
422
+ and _evm_payment_requirement_supported(requirement)
423
+ )
424
+ currently_executable = (
425
+ planned_execution_supported
426
+ and wallet_network_matches
427
+ and _evm_exact_execution_supported(backend)
428
+ )
429
+ if currently_executable:
430
+ reason = (
431
+ "Executable now through the local Solana exact buyer flow."
432
+ if chain == "solana"
433
+ else "Executable now through the local EVM exact buyer flow."
434
+ )
435
+ elif chain == "evm" and scheme == "exact" and not _evm_payment_requirement_supported(requirement):
436
+ reason = "This EVM exact payment requires a transfer method that is not enabled in the current wallet runtime."
437
+ elif planned_execution_supported and wallet_network_matches:
438
+ reason = "Wallet network matches, but this backend does not yet expose a supported x402 signer path."
439
+ elif planned_execution_supported:
440
+ reason = "Planned execution path exists, but the requirement targets a different network than the active wallet."
441
+ else:
442
+ reason = "Unsupported scheme or network for the planned execution path."
443
+ return {
444
+ "wallet_type_supported": wallet_summary["wallet_type_supported"],
445
+ "wallet_network_matches": wallet_network_matches,
446
+ "planned_execution_supported": planned_execution_supported,
447
+ "currently_executable": currently_executable,
448
+ "reason": reason,
449
+ }
450
+
451
+
452
+ def _select_preferred_requirement(
453
+ requirements: list[dict[str, Any]],
454
+ backend: AgentWalletBackend,
455
+ ) -> dict[str, Any] | None:
456
+ compatible = [
457
+ requirement
458
+ for requirement in requirements
459
+ if _requirement_compatibility(requirement, backend)["planned_execution_supported"]
460
+ ]
461
+ exact_match = [
462
+ requirement
463
+ for requirement in compatible
464
+ if _requirement_compatibility(requirement, backend)["wallet_network_matches"]
465
+ ]
466
+ candidates = exact_match or compatible
467
+ if not candidates:
468
+ return None
469
+
470
+ def sort_key(item: dict[str, Any]) -> tuple[int, str]:
471
+ amount = _trim(item.get("amount"))
472
+ return (0 if amount.isdigit() else 1, amount)
473
+
474
+ return sorted(candidates, key=sort_key)[0]
475
+
476
+
477
+ def _build_request_metadata(
478
+ *,
479
+ url: str,
480
+ method: str = "GET",
481
+ headers: dict[str, Any] | None = None,
482
+ query: dict[str, Any] | None = None,
483
+ json_body: Any | None = None,
484
+ text_body: str | None = None,
485
+ ) -> dict[str, Any]:
486
+ request_url = _trim(url)
487
+ if not request_url:
488
+ raise ProviderError("x402-http", "url is required.")
489
+ http_method = _normalize_http_method(method)
490
+ normalized_headers = _normalize_headers(headers)
491
+ normalized_query = _normalize_query_params(query)
492
+ if json_body is not None and text_body is not None:
493
+ raise ProviderError("x402-http", "Provide either json_body or text_body, not both.")
494
+ final_url = _append_query(request_url, normalized_query)
495
+ body_hash = None
496
+ content_type = None
497
+ if json_body is not None:
498
+ body_hash = _hash_text(_canonical_json_text(json_body))
499
+ content_type = normalized_headers.get("Content-Type") or normalized_headers.get("content-type")
500
+ if not content_type:
501
+ normalized_headers["Content-Type"] = "application/json"
502
+ elif text_body is not None:
503
+ body_hash = _hash_text(text_body)
504
+ content_type = normalized_headers.get("Content-Type") or normalized_headers.get("content-type")
505
+ request_fingerprint = _hash_text(
506
+ _canonical_json_text(
507
+ {
508
+ "method": http_method,
509
+ "url": final_url,
510
+ "body_hash": body_hash,
511
+ }
512
+ )
513
+ )
514
+ return {
515
+ "url": final_url,
516
+ "method": http_method,
517
+ "headers": normalized_headers,
518
+ "query": normalized_query,
519
+ "json_body": json_body,
520
+ "text_body": text_body,
521
+ "body_hash": body_hash,
522
+ "content_type": content_type,
523
+ "request_fingerprint": request_fingerprint,
524
+ }
525
+
526
+
527
+ async def _send_request(
528
+ *,
529
+ client: Any,
530
+ request: dict[str, Any],
531
+ extra_headers: dict[str, str] | None = None,
532
+ ) -> Any:
533
+ headers = dict(request["headers"])
534
+ if extra_headers:
535
+ headers.update(extra_headers)
536
+ return await client.request(
537
+ request["method"],
538
+ request["url"],
539
+ headers=headers,
540
+ json=request["json_body"] if request["json_body"] is not None else None,
541
+ content=request["text_body"] if request["text_body"] is not None else None,
542
+ )
543
+
544
+
545
+ def _build_x402_action_payload(
546
+ *,
547
+ backend: AgentWalletBackend,
548
+ request: dict[str, Any],
549
+ wallet_summary: dict[str, Any],
550
+ address: str | None,
551
+ status_code: int,
552
+ selected_payment: dict[str, Any] | None,
553
+ accepted_payments: list[dict[str, Any]] | None = None,
554
+ ) -> dict[str, Any]:
555
+ return {
556
+ "asset_type": "x402-request",
557
+ "source": "x402-http",
558
+ "chain": _backend_chain(backend),
559
+ "network": _backend_network(backend),
560
+ "x402_network": selected_payment.get("network") if isinstance(selected_payment, dict) else None,
561
+ "x402_scheme": selected_payment.get("scheme") if isinstance(selected_payment, dict) else None,
562
+ "x402_asset": selected_payment.get("asset") if isinstance(selected_payment, dict) else None,
563
+ "x402_amount": selected_payment.get("amount") if isinstance(selected_payment, dict) else None,
564
+ "x402_amount_display": selected_payment.get("amount_display")
565
+ if isinstance(selected_payment, dict)
566
+ else None,
567
+ "x402_pay_to": selected_payment.get("pay_to") if isinstance(selected_payment, dict) else None,
568
+ "request_url": request["url"],
569
+ "method": request["method"],
570
+ "request_fingerprint": request["request_fingerprint"],
571
+ "body_hash": request["body_hash"],
572
+ "content_type": request["content_type"],
573
+ "wallet": {
574
+ **wallet_summary,
575
+ "address": address,
576
+ },
577
+ "status_code": status_code,
578
+ "selected_payment": selected_payment,
579
+ "accepted_payments": accepted_payments,
580
+ "payment_required": selected_payment is not None,
581
+ }
582
+
583
+
584
+ def _response_preview(response: Any) -> Any:
585
+ try:
586
+ return response.json()
587
+ except Exception:
588
+ return _response_text(response)[:2000]
589
+
590
+
591
+ def _parse_payment_required_response(
592
+ response: Any,
593
+ *,
594
+ backend: AgentWalletBackend,
595
+ request: dict[str, Any],
596
+ address: str | None,
597
+ wallet_summary: dict[str, Any],
598
+ ) -> dict[str, Any]:
599
+ payment_required = response.headers.get("PAYMENT-REQUIRED")
600
+ if not payment_required:
601
+ raise ProviderError(
602
+ "x402-http",
603
+ "Server returned HTTP 402 without a PAYMENT-REQUIRED header.",
604
+ details={"status_code": response.status_code, "url": request["url"]},
605
+ )
606
+
607
+ decoded = _decode_payment_required(payment_required)
608
+ normalized_accepts = [
609
+ normalize_payment_requirement(requirement, source="payment_required", resource_url=request["url"])
610
+ for requirement in decoded["accepts"]
611
+ if isinstance(requirement, dict)
612
+ ]
613
+ compatibility = [
614
+ {
615
+ **requirement,
616
+ "compatibility": _requirement_compatibility(requirement, backend),
617
+ }
618
+ for requirement in normalized_accepts
619
+ ]
620
+ selected = _select_preferred_requirement(normalized_accepts, backend)
621
+ preview = _build_x402_action_payload(
622
+ backend=backend,
623
+ request=request,
624
+ wallet_summary=wallet_summary,
625
+ address=address,
626
+ status_code=response.status_code,
627
+ selected_payment=selected,
628
+ accepted_payments=compatibility,
629
+ )
630
+ preview.update(
631
+ {
632
+ "execute_available": bool(
633
+ isinstance(selected, dict)
634
+ and _requirement_compatibility(selected, backend)["currently_executable"]
635
+ ),
636
+ "x402_version": decoded["x402_version"],
637
+ "response_headers": {
638
+ "payment-required": decoded["encoded"],
639
+ "content-type": response.headers.get("content-type"),
640
+ },
641
+ }
642
+ )
643
+ return preview
644
+
645
+
646
+ def _require_executable_payment(
647
+ *,
648
+ preview: dict[str, Any],
649
+ backend: AgentWalletBackend,
650
+ ) -> dict[str, Any]:
651
+ selected = preview.get("selected_payment")
652
+ if not isinstance(selected, dict):
653
+ raise ProviderError(
654
+ "x402-http",
655
+ "No compatible x402 payment requirement was selected for this wallet.",
656
+ details={
657
+ "request_url": preview.get("request_url"),
658
+ "accepted_payments": preview.get("accepted_payments"),
659
+ },
660
+ )
661
+ compatibility = _requirement_compatibility(selected, backend)
662
+ if not compatibility["currently_executable"]:
663
+ raise ProviderError(
664
+ "x402-http",
665
+ str(compatibility["reason"]),
666
+ details={
667
+ "selected_payment": selected,
668
+ "compatibility": compatibility,
669
+ },
670
+ )
671
+ return selected
672
+
673
+
674
+ def _select_sdk_payment_requirement(
675
+ payment_required: Any,
676
+ *,
677
+ selected_payment: dict[str, Any],
678
+ ) -> Any:
679
+ accepts = getattr(payment_required, "accepts", None)
680
+ if not isinstance(accepts, list) or not accepts:
681
+ raise ProviderError("x402-http", "Decoded x402 payment payload does not contain accepts[].")
682
+
683
+ selected_raw = selected_payment.get("raw") if isinstance(selected_payment, dict) else None
684
+ if isinstance(selected_raw, dict):
685
+ for requirement in accepts:
686
+ model_dump = getattr(requirement, "model_dump", None)
687
+ if callable(model_dump):
688
+ dumped = model_dump(by_alias=True, exclude_none=True)
689
+ if dumped == selected_raw:
690
+ return requirement
691
+
692
+ for requirement in accepts:
693
+ if (
694
+ _trim(_requirement_field(requirement, "scheme")).lower()
695
+ == _trim(selected_payment.get("scheme")).lower()
696
+ and _trim(_requirement_field(requirement, "network"))
697
+ == _trim(selected_payment.get("network"))
698
+ and _trim(_requirement_field(requirement, "asset"))
699
+ == _trim(selected_payment.get("asset"))
700
+ and _trim(_requirement_field(requirement, "amount"))
701
+ == _trim(selected_payment.get("amount"))
702
+ and _trim(_requirement_field(requirement, "pay_to"))
703
+ == _trim(selected_payment.get("pay_to"))
704
+ ):
705
+ return requirement
706
+
707
+ if len(accepts) == 1:
708
+ return accepts[0]
709
+
710
+ raise ProviderError(
711
+ "x402-http",
712
+ "Could not match the selected x402 payment back to the decoded PAYMENT-REQUIRED payload.",
713
+ details={
714
+ "selected_payment": selected_payment,
715
+ "accepts_count": len(accepts),
716
+ },
717
+ )
718
+
719
+
720
+ def _build_selected_payment_required_payload(
721
+ payment_required: Any,
722
+ *,
723
+ selected_payment: dict[str, Any],
724
+ ) -> Any:
725
+ selected_requirement = _select_sdk_payment_requirement(
726
+ payment_required,
727
+ selected_payment=selected_payment,
728
+ )
729
+ return payment_required.model_copy(update={"accepts": [selected_requirement]})
730
+
731
+
732
+ def _load_x402_common_sdk() -> dict[str, Any]:
733
+ try:
734
+ from x402 import x402Client
735
+ from x402.http.x402_http_client_base import x402HTTPClientBase
736
+ from x402.http.utils import decode_payment_required_header
737
+ except ImportError as exc:
738
+ raise ProviderError(
739
+ "x402-sdk",
740
+ "x402 execution requires the x402 Python package with HTTP client support.",
741
+ details={"hint": 'Install dependencies so `x402[httpx]` is available in the wallet runtime.'},
742
+ ) from exc
743
+ return {
744
+ "x402Client": x402Client,
745
+ "x402HTTPClientBase": x402HTTPClientBase,
746
+ "decode_payment_required_header": decode_payment_required_header,
747
+ }
748
+
749
+
750
+ def _load_x402_solana_sdk() -> dict[str, Any]:
751
+ sdk = _load_x402_common_sdk()
752
+ try:
753
+ from x402.mechanisms.svm.exact import register_exact_svm_client
754
+ except ImportError as exc:
755
+ raise ProviderError(
756
+ "x402-sdk",
757
+ "x402 Solana execution requires SVM support.",
758
+ details={"hint": 'Install dependencies so `x402[httpx,svm]` is available in the wallet runtime.'},
759
+ ) from exc
760
+ sdk.update(
761
+ {
762
+ "register_exact_svm_client": register_exact_svm_client,
763
+ }
764
+ )
765
+ return sdk
766
+
767
+
768
+ def _load_x402_evm_sdk() -> dict[str, Any]:
769
+ sdk = _load_x402_common_sdk()
770
+ try:
771
+ from x402.mechanisms.evm.exact import register_exact_evm_client
772
+ except ImportError as exc:
773
+ raise ProviderError(
774
+ "x402-sdk",
775
+ "x402 EVM execution requires EVM support.",
776
+ details={"hint": 'Install dependencies so `x402[httpx,evm]` is available in the wallet runtime.'},
777
+ ) from exc
778
+ sdk.update(
779
+ {
780
+ "register_exact_evm_client": register_exact_evm_client,
781
+ }
782
+ )
783
+ return sdk
784
+
785
+
786
+ def _load_x402_sdk() -> dict[str, Any]:
787
+ return _load_x402_common_sdk()
788
+
789
+
790
+ def _build_solana_sdk_signer(backend: AgentWalletBackend) -> Any:
791
+ signer = getattr(backend, "signer", None)
792
+ if signer is None or not hasattr(signer, "export_keypair_bytes"):
793
+ raise ProviderError(
794
+ "x402-solana",
795
+ "The active Solana backend does not expose a local signer for x402 payments.",
796
+ )
797
+ try:
798
+ from solders.keypair import Keypair
799
+ except ImportError as exc:
800
+ raise ProviderError(
801
+ "x402-solana",
802
+ "Solders is required for Solana x402 signing.",
803
+ ) from exc
804
+
805
+ class _OpenClawSolanaX402Signer:
806
+ def __init__(self, wallet_signer: Any):
807
+ self._wallet_signer = wallet_signer
808
+ self._keypair = Keypair.from_bytes(wallet_signer.export_keypair_bytes())
809
+
810
+ @property
811
+ def address(self) -> str:
812
+ return str(self._wallet_signer.address)
813
+
814
+ @property
815
+ def keypair(self) -> Any:
816
+ return self._keypair
817
+
818
+ def sign_transaction(self, tx: Any) -> Any:
819
+ tx.sign([self._keypair])
820
+ return tx
821
+
822
+ return _OpenClawSolanaX402Signer(signer)
823
+
824
+
825
+ def _build_evm_sdk_signer(backend: AgentWalletBackend, address: str) -> Any:
826
+ sign_typed_data = getattr(backend, "sign_x402_evm_exact_typed_data", None)
827
+ if not callable(sign_typed_data):
828
+ raise ProviderError(
829
+ "x402-evm",
830
+ "The active EVM backend does not expose an x402 exact typed-data signer.",
831
+ )
832
+
833
+ class _OpenClawEvmX402Signer:
834
+ def __init__(self, wallet_backend: AgentWalletBackend, wallet_address: str):
835
+ self._wallet_backend = wallet_backend
836
+ self._address = wallet_address
837
+
838
+ @property
839
+ def address(self) -> str:
840
+ return self._address
841
+
842
+ def sign_typed_data(
843
+ self,
844
+ domain: Any,
845
+ types: dict[str, list[Any]],
846
+ primary_type: str,
847
+ message: dict[str, Any],
848
+ ) -> bytes:
849
+ normalized_types: dict[str, list[dict[str, str]]] = {}
850
+ for type_name, fields in types.items():
851
+ normalized_types[type_name] = [
852
+ {
853
+ "name": _trim(getattr(field, "name", "")),
854
+ "type": _trim(getattr(field, "type", "")),
855
+ }
856
+ for field in fields
857
+ ]
858
+ domain_payload = {
859
+ "name": getattr(domain, "name", None),
860
+ "version": getattr(domain, "version", None),
861
+ "chainId": getattr(domain, "chain_id", None),
862
+ "verifyingContract": getattr(domain, "verifying_contract", None),
863
+ }
864
+ return self._wallet_backend.sign_x402_evm_exact_typed_data(
865
+ domain=domain_payload,
866
+ types=normalized_types,
867
+ primary_type=primary_type,
868
+ message=message,
869
+ )
870
+
871
+ return _OpenClawEvmX402Signer(backend, address)
872
+
873
+
874
+ async def _create_payment_headers(
875
+ *,
876
+ backend: AgentWalletBackend,
877
+ payment_required_header: str,
878
+ selected_payment: dict[str, Any],
879
+ ) -> dict[str, str]:
880
+ chain = _backend_chain(backend)
881
+ if chain == "solana":
882
+ sdk = _load_x402_solana_sdk()
883
+ payment_required = sdk["decode_payment_required_header"](payment_required_header)
884
+ selected_payload = _build_selected_payment_required_payload(
885
+ payment_required,
886
+ selected_payment=selected_payment,
887
+ )
888
+ client = sdk["x402Client"]()
889
+ sdk_rpc_url = _backend_solana_sdk_rpc_url(backend)
890
+ if not sdk_rpc_url:
891
+ raise ProviderError(
892
+ "x402-solana",
893
+ "No direct Solana RPC URL is available for the x402 SDK signer path.",
894
+ details={"network": _backend_network(backend)},
895
+ )
896
+ sdk["register_exact_svm_client"](
897
+ client,
898
+ _build_solana_sdk_signer(backend),
899
+ networks=str(selected_payment["network"]),
900
+ rpc_url=sdk_rpc_url,
901
+ )
902
+ try:
903
+ payment_payload = await client.create_payment_payload(selected_payload)
904
+ except Exception as exc:
905
+ raise ProviderError(
906
+ "x402-solana",
907
+ "Failed to build the Solana x402 payment payload.",
908
+ details={
909
+ "network": _backend_network(backend),
910
+ "sdk_rpc_url": sdk_rpc_url,
911
+ "error_type": type(exc).__name__,
912
+ "error": str(exc) or None,
913
+ },
914
+ ) from exc
915
+ return sdk["x402HTTPClientBase"]().encode_payment_signature_header(payment_payload)
916
+
917
+ if chain == "evm":
918
+ sdk = _load_x402_evm_sdk()
919
+ payment_required = sdk["decode_payment_required_header"](payment_required_header)
920
+ selected_payload = _build_selected_payment_required_payload(
921
+ payment_required,
922
+ selected_payment=selected_payment,
923
+ )
924
+ client = sdk["x402Client"]()
925
+ address = await backend.get_address()
926
+ if not isinstance(address, str) or not address.strip():
927
+ raise ProviderError("x402-evm", "The active EVM backend did not resolve a payer address.")
928
+ sdk["register_exact_evm_client"](
929
+ client,
930
+ _build_evm_sdk_signer(backend, address.strip()),
931
+ networks=str(selected_payment["network"]),
932
+ )
933
+ payment_payload = await client.create_payment_payload(selected_payload)
934
+ return sdk["x402HTTPClientBase"]().encode_payment_signature_header(payment_payload)
935
+
936
+ raise ProviderError(
937
+ "x402-http",
938
+ "Only Solana and EVM buyer flows are executable in this milestone.",
939
+ details={"chain": chain},
940
+ )
941
+
942
+
943
+ def _extract_settlement_header(response: Any) -> dict[str, Any] | None:
944
+ sdk = _load_x402_sdk()
945
+ try:
946
+ settle = sdk["x402HTTPClientBase"]().get_payment_settle_response(
947
+ lambda name: response.headers.get(name)
948
+ )
949
+ except Exception:
950
+ return None
951
+ return settle.model_dump(by_alias=True, exclude_none=True)
952
+
953
+
954
+ async def search_services(
955
+ *,
956
+ query: str | None = None,
957
+ discovery_provider: str = "auto",
958
+ network: str | None = None,
959
+ asset: str | None = None,
960
+ scheme: str | None = None,
961
+ max_usd_price: str | None = None,
962
+ limit: int = 10,
963
+ ) -> dict[str, Any]:
964
+ provider = _normalize_discovery_provider(discovery_provider)
965
+ if provider == "auto":
966
+ provider = "cdp_bazaar"
967
+ if limit <= 0:
968
+ raise ProviderError("x402-discovery", "limit must be greater than zero.")
969
+ client = get_client()
970
+
971
+ if provider == "cdp_bazaar":
972
+ if query and _trim(query):
973
+ response = await client.get(
974
+ f"{CDP_BAZAAR_DISCOVERY_BASE_URL}/search",
975
+ params={
976
+ "query": _trim(query),
977
+ "network": _trim(network) or None,
978
+ "asset": _trim(asset) or None,
979
+ "scheme": _trim(scheme) or None,
980
+ "maxUsdPrice": _trim(max_usd_price) or None,
981
+ "limit": min(limit, 20),
982
+ },
983
+ )
984
+ payload = _parse_json_response(
985
+ response, provider="x402-cdp-bazaar", operation="CDP Bazaar search"
986
+ )
987
+ resources = payload.get("resources") if isinstance(payload, dict) else None
988
+ if not isinstance(resources, list):
989
+ raise ProviderError("x402-cdp-bazaar", "Unexpected CDP Bazaar search response.")
990
+ items = [_normalize_cdp_resource(item) for item in resources if isinstance(item, dict)]
991
+ return {
992
+ "discovery_provider": provider,
993
+ "query": _trim(query),
994
+ "count": len(items),
995
+ "partial_results": bool(payload.get("partialResults")),
996
+ "search_method": payload.get("searchMethod"),
997
+ "items": items,
998
+ }
999
+
1000
+ response = await client.get(
1001
+ f"{CDP_BAZAAR_DISCOVERY_BASE_URL}/resources",
1002
+ params={"type": "http", "limit": min(limit, 1000), "offset": 0},
1003
+ )
1004
+ payload = _parse_json_response(
1005
+ response, provider="x402-cdp-bazaar", operation="CDP Bazaar resources"
1006
+ )
1007
+ resources = payload.get("items") if isinstance(payload, dict) else None
1008
+ if not isinstance(resources, list):
1009
+ raise ProviderError("x402-cdp-bazaar", "Unexpected CDP Bazaar resources response.")
1010
+ items = [_normalize_cdp_resource(item) for item in resources if isinstance(item, dict)]
1011
+ return {
1012
+ "discovery_provider": provider,
1013
+ "query": "",
1014
+ "count": len(items),
1015
+ "pagination": payload.get("pagination") if isinstance(payload, dict) else None,
1016
+ "items": items,
1017
+ }
1018
+
1019
+ endpoint = "/services/search" if query and _trim(query) else "/services/"
1020
+ params = {"q": _trim(query)} if query and _trim(query) else None
1021
+ response = await client.get(f"{AGENTIC_MARKET_API_BASE_URL}{endpoint}", params=params)
1022
+ payload = _parse_json_response(
1023
+ response, provider="x402-agentic-market", operation="Agentic Market search"
1024
+ )
1025
+ services = payload.get("services") if isinstance(payload, dict) else None
1026
+ if not isinstance(services, list):
1027
+ raise ProviderError("x402-agentic-market", "Unexpected Agentic Market response.")
1028
+ normalized = [
1029
+ _normalize_agentic_service(service)
1030
+ for service in services
1031
+ if isinstance(service, dict)
1032
+ ]
1033
+ if network:
1034
+ needle = _trim(network).lower()
1035
+ normalized = [
1036
+ item
1037
+ for item in normalized
1038
+ if needle in {entry.lower() for entry in item.get("networks") or []}
1039
+ ]
1040
+ if scheme:
1041
+ needle = _trim(scheme).lower()
1042
+ normalized = [
1043
+ item
1044
+ for item in normalized
1045
+ if any(_trim(req.get("scheme")).lower() == needle for req in item.get("accepts") or [])
1046
+ ]
1047
+ if asset:
1048
+ needle = _trim(asset).lower()
1049
+ normalized = [
1050
+ item
1051
+ for item in normalized
1052
+ if any(_trim(req.get("asset")).lower() == needle for req in item.get("accepts") or [])
1053
+ ]
1054
+ return {
1055
+ "discovery_provider": provider,
1056
+ "query": _trim(query),
1057
+ "count": len(normalized[:limit]),
1058
+ "items": normalized[:limit],
1059
+ }
1060
+
1061
+
1062
+ async def get_service_details(
1063
+ *,
1064
+ reference: str,
1065
+ discovery_provider: str = "auto",
1066
+ ) -> dict[str, Any]:
1067
+ ref = _trim(reference)
1068
+ if not ref:
1069
+ raise ProviderError("x402-discovery", "reference is required.")
1070
+ provider = _normalize_discovery_provider(discovery_provider)
1071
+ if provider == "auto":
1072
+ provider = (
1073
+ "cdp_bazaar" if ref.startswith("http://") or ref.startswith("https://") else "agentic_market"
1074
+ )
1075
+
1076
+ if provider == "cdp_bazaar":
1077
+ resources = await search_services(discovery_provider="cdp_bazaar", limit=200)
1078
+ exact = next((item for item in resources["items"] if item.get("resource") == ref), None)
1079
+ if exact is None:
1080
+ needle = ref.lower()
1081
+ exact = next(
1082
+ (
1083
+ item
1084
+ for item in resources["items"]
1085
+ if needle in _trim(item.get("resource")).lower()
1086
+ or needle in _trim(item.get("description")).lower()
1087
+ ),
1088
+ None,
1089
+ )
1090
+ if exact is None:
1091
+ raise ProviderError("x402-cdp-bazaar", f"No Bazaar resource matched: {ref}")
1092
+ return {"discovery_provider": provider, "service": exact}
1093
+
1094
+ query = ref
1095
+ if ref.startswith("http://") or ref.startswith("https://"):
1096
+ query = urlsplit(ref).netloc or ref
1097
+ services = await search_services(query=query, discovery_provider="agentic_market", limit=20)
1098
+ needle = ref.lower()
1099
+ exact = next(
1100
+ (
1101
+ item
1102
+ for item in services["items"]
1103
+ if needle
1104
+ in {
1105
+ _trim(item.get("service_id")).lower(),
1106
+ _trim(item.get("domain")).lower(),
1107
+ _trim(item.get("service_name")).lower(),
1108
+ }
1109
+ ),
1110
+ None,
1111
+ )
1112
+ if exact is None and services["items"]:
1113
+ exact = services["items"][0]
1114
+ if exact is None:
1115
+ raise ProviderError("x402-agentic-market", f"No Agentic Market service matched: {ref}")
1116
+ return {"discovery_provider": provider, "service": exact}
1117
+
1118
+
1119
+ async def preview_request(
1120
+ *,
1121
+ backend: AgentWalletBackend,
1122
+ url: str,
1123
+ method: str = "GET",
1124
+ headers: dict[str, Any] | None = None,
1125
+ query: dict[str, Any] | None = None,
1126
+ json_body: Any | None = None,
1127
+ text_body: str | None = None,
1128
+ ) -> dict[str, Any]:
1129
+ request = _build_request_metadata(
1130
+ url=url,
1131
+ method=method,
1132
+ headers=headers,
1133
+ query=query,
1134
+ json_body=json_body,
1135
+ text_body=text_body,
1136
+ )
1137
+ client = get_client()
1138
+ response = await _send_request(client=client, request=request)
1139
+ wallet_summary = _wallet_x402_support_summary(backend)
1140
+ address = await backend.get_address()
1141
+ preview: dict[str, Any] = {
1142
+ "mode": "preview",
1143
+ **_build_x402_action_payload(
1144
+ backend=backend,
1145
+ request=request,
1146
+ wallet_summary=wallet_summary,
1147
+ address=address,
1148
+ status_code=response.status_code,
1149
+ selected_payment=None,
1150
+ ),
1151
+ "execute_available": False,
1152
+ "request": {
1153
+ "url": request["url"],
1154
+ "method": request["method"],
1155
+ "request_fingerprint": request["request_fingerprint"],
1156
+ "query": request["query"],
1157
+ "body_hash": request["body_hash"],
1158
+ "content_type": request["content_type"],
1159
+ },
1160
+ }
1161
+
1162
+ if response.status_code != 402:
1163
+ preview.update(
1164
+ {
1165
+ "payment_required": False,
1166
+ "response_preview": _response_preview(response),
1167
+ "response_headers": {
1168
+ "content-type": response.headers.get("content-type"),
1169
+ },
1170
+ }
1171
+ )
1172
+ return preview
1173
+
1174
+ payment_preview = _parse_payment_required_response(
1175
+ response,
1176
+ backend=backend,
1177
+ request=request,
1178
+ address=address,
1179
+ wallet_summary=wallet_summary,
1180
+ )
1181
+ payment_preview["mode"] = "preview"
1182
+ payment_preview["request"] = {
1183
+ "url": request["url"],
1184
+ "method": request["method"],
1185
+ "request_fingerprint": request["request_fingerprint"],
1186
+ "query": request["query"],
1187
+ "body_hash": request["body_hash"],
1188
+ "content_type": request["content_type"],
1189
+ }
1190
+ return payment_preview
1191
+
1192
+
1193
+ async def prepare_request(
1194
+ *,
1195
+ backend: AgentWalletBackend,
1196
+ url: str,
1197
+ method: str = "GET",
1198
+ headers: dict[str, Any] | None = None,
1199
+ query: dict[str, Any] | None = None,
1200
+ json_body: Any | None = None,
1201
+ text_body: str | None = None,
1202
+ ) -> dict[str, Any]:
1203
+ preview = await preview_request(
1204
+ backend=backend,
1205
+ url=url,
1206
+ method=method,
1207
+ headers=headers,
1208
+ query=query,
1209
+ json_body=json_body,
1210
+ text_body=text_body,
1211
+ )
1212
+ if not preview.get("payment_required"):
1213
+ prepared = dict(preview)
1214
+ prepared["mode"] = "prepare"
1215
+ prepared["prepared"] = False
1216
+ prepared["prepare_note"] = "The endpoint did not require x402 payment for this request."
1217
+ return prepared
1218
+
1219
+ selected_payment = _require_executable_payment(preview=preview, backend=backend)
1220
+ payment_required_header = (
1221
+ dict(preview.get("response_headers") or {}).get("payment-required")
1222
+ )
1223
+ if not isinstance(payment_required_header, str) or not payment_required_header.strip():
1224
+ raise ProviderError("x402-http", "Missing PAYMENT-REQUIRED header in preview state.")
1225
+ # Create the payload once during prepare to validate that the active wallet can sign it.
1226
+ await _create_payment_headers(
1227
+ backend=backend,
1228
+ payment_required_header=payment_required_header,
1229
+ selected_payment=selected_payment,
1230
+ )
1231
+ prepared = dict(preview)
1232
+ prepared["mode"] = "prepare"
1233
+ prepared["prepared"] = True
1234
+ prepared["signed"] = False
1235
+ prepared["broadcasted"] = False
1236
+ prepared["confirmed"] = False
1237
+ prepared["payment_payload_withheld"] = True
1238
+ prepared["prepare_note"] = (
1239
+ "x402 payment authorization was validated locally, but the PAYMENT-SIGNATURE header is withheld until execute."
1240
+ )
1241
+ return prepared
1242
+
1243
+
1244
+ async def execute_request(
1245
+ *,
1246
+ backend: AgentWalletBackend,
1247
+ url: str,
1248
+ method: str = "GET",
1249
+ headers: dict[str, Any] | None = None,
1250
+ query: dict[str, Any] | None = None,
1251
+ json_body: Any | None = None,
1252
+ text_body: str | None = None,
1253
+ ) -> dict[str, Any]:
1254
+ preview = await preview_request(
1255
+ backend=backend,
1256
+ url=url,
1257
+ method=method,
1258
+ headers=headers,
1259
+ query=query,
1260
+ json_body=json_body,
1261
+ text_body=text_body,
1262
+ )
1263
+ if not preview.get("payment_required"):
1264
+ executed = dict(preview)
1265
+ executed["mode"] = "execute"
1266
+ executed["paid"] = False
1267
+ executed["broadcasted"] = False
1268
+ executed["confirmed"] = False
1269
+ return executed
1270
+
1271
+ selected_payment = _require_executable_payment(preview=preview, backend=backend)
1272
+ payment_required_header = (
1273
+ dict(preview.get("response_headers") or {}).get("payment-required")
1274
+ )
1275
+ if not isinstance(payment_required_header, str) or not payment_required_header.strip():
1276
+ raise ProviderError("x402-http", "Missing PAYMENT-REQUIRED header in preview state.")
1277
+
1278
+ request = _build_request_metadata(
1279
+ url=url,
1280
+ method=method,
1281
+ headers=headers,
1282
+ query=query,
1283
+ json_body=json_body,
1284
+ text_body=text_body,
1285
+ )
1286
+ payment_headers = await _create_payment_headers(
1287
+ backend=backend,
1288
+ payment_required_header=payment_required_header,
1289
+ selected_payment=selected_payment,
1290
+ )
1291
+ payment_headers["Access-Control-Expose-Headers"] = "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE"
1292
+ client = get_client()
1293
+ response = await _send_request(client=client, request=request, extra_headers=payment_headers)
1294
+ settlement = _extract_settlement_header(response)
1295
+
1296
+ executed = dict(preview)
1297
+ executed.update(
1298
+ {
1299
+ "mode": "execute",
1300
+ "paid": True,
1301
+ "broadcasted": bool(settlement and settlement.get("transaction")),
1302
+ "confirmed": bool(settlement and settlement.get("success")),
1303
+ "payment_settlement": settlement,
1304
+ "status_code": response.status_code,
1305
+ "response_preview": _response_preview(response),
1306
+ "response_headers": {
1307
+ "content-type": response.headers.get("content-type"),
1308
+ "payment-response": response.headers.get("PAYMENT-RESPONSE")
1309
+ or response.headers.get("X-PAYMENT-RESPONSE"),
1310
+ },
1311
+ }
1312
+ )
1313
+ if response.status_code == 402:
1314
+ raise ProviderError(
1315
+ "x402-http",
1316
+ "The paid x402 retry still returned HTTP 402.",
1317
+ details={
1318
+ "request_url": request["url"],
1319
+ "selected_payment": selected_payment,
1320
+ "response_preview": executed["response_preview"],
1321
+ },
1322
+ )
1323
+ return executed