Setting the file. One moment.
Process Payment Tool · Agents Build · aws/agent-toolkit-for-aws · Skills Docs
ContentsBack to the top of the page Teardown
69
Creating Amazon Aurora Db Cluster With Instances
92
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Debugging Lambda Timeouts
scripts/process_payment_tool.py
scripts/ process_payment_tool.py
Python · 150 lines · 7 KB
so it cannot retry that — this tool re-runs the settle+replay flow up to
16 X402_MAX_PAYMENT_ATTEMPTS times before giving up. A single idempotency token
17 (client_token) is reused across all attempts of one fetch, so ProcessPayment is
18 idempotent: every retry replays the SAME on-chain authorization/nonce. That
19 recovers a not-yet-settled transient failure, and if the merchant actually did
20 settle but still returned 402, the replay simply reverts on-chain (nonce already
21 used) rather than charging the user a second time.
22
23 Control-plane resources (payment manager/connector) are created by the AgentCore
24 CLI; the per-user instrument/session are created by setup_payment_user.py. This
25 tool only consumes them, via these environment variables:
26
27 PAYMENT_MANAGER_ARN payment manager ARN (from deployed-state.json)
28 PAYMENT_INSTRUMENT_ID per-user wallet ID (from setup_payment_user.py)
29 PAYMENT_SESSION_ID per-conversation session (from setup_payment_user.py)
30 PAYMENT_USER_ID end-user identity (required)
31 AWS_REGION region (default us-west-2)
32 X402_MAX_PAYMENT_ATTEMPTS transient-402 retry cap (default 5)
33 """
34 import ipaddress
35 import json
36 import os
37 import socket
38 import uuid
39 from urllib.parse import urlparse
40
41 import httpx
42 from bedrock_agentcore.payments import PaymentManager
43
44 PAYMENT_MANAGER_ARN = os.getenv( "PAYMENT_MANAGER_ARN" )
45 PAYMENT_INSTRUMENT_ID = os.getenv( "PAYMENT_INSTRUMENT_ID" )
46 PAYMENT_SESSION_ID = os.getenv( "PAYMENT_SESSION_ID" )
47 PAYMENT_USER_ID = os.environ.get( "PAYMENT_USER_ID" ) # required — no insecure default
48 REGION = os.getenv( "AWS_REGION" , "us-west-2" )
49 # Transient on-chain settlement can leave the paid retry at 402 even though the
50 # header was valid; re-settle (fresh header + idempotency token) up to this many times.
51 MAX_PAYMENT_ATTEMPTS = int (os.getenv( "X402_MAX_PAYMENT_ATTEMPTS" , "5" ))
52
53 # AgentCore Payments data-plane client (SDK). Created when configured.
54 _manager = PaymentManager( payment_manager_arn = PAYMENT_MANAGER_ARN , region_name = REGION ) if PAYMENT_MANAGER_ARN else None
55
56
57 def _validate_url (url):
58 """Return an error string if the URL is not HTTPS or targets a private/internal IP."""
59 parsed = urlparse(url)
60 if parsed.scheme != "https" :
61 return "Only HTTPS URLs are supported for payment requests"
62 try :
63 for _family, _, _, _, sockaddr in socket.getaddrinfo(parsed.hostname, parsed.port or 443 ):
64 ip = ipaddress.ip_address(sockaddr[ 0 ])
65 if ip.is_private or ip.is_loopback or ip.is_link_local:
66 return "Cannot fetch private/internal network addresses"
67 except socket.gaierror:
68 return "Cannot resolve hostname"
69 return None
70
71
72 def _settle_and_retry (url, method, response, client_token):
73 """Build the payment header from a 402 response via the SDK, then replay the request.
74
75 The SDK's generate_payment_header does the whole settle workflow (validate the
76 402, pick the network, ProcessPayment, build the v1 `X-PAYMENT` / v2
77 `PAYMENT-SIGNATURE` proof) and returns {header_name: header_value}. We pass a
78 STABLE client_token (the same one for every attempt of a single fetch) so
79 ProcessPayment is idempotent — each retry replays the same authorization/nonce
80 and can never double-charge.
81 Returns the retry httpx.Response. Raises on a header-generation failure.
82 """
83 payment_header = _manager.generate_payment_header(
84 payment_instrument_id = PAYMENT_INSTRUMENT_ID ,
85 payment_session_id = PAYMENT_SESSION_ID ,
86 user_id = PAYMENT_USER_ID ,
87 client_token = client_token,
88 payment_required_request = {
89 "statusCode" : response.status_code,
90 "headers" : dict (response.headers),
91 "body" : response.text,
92 },
93 )
94 # Retry with a FRESH client so cookies from the 402 response don't contaminate it.
95 with httpx.Client( verify = True ) as client:
96 return client.request(method, url, headers = payment_header, timeout = 30 )
97
98
99 def x402_fetch (url, method = "GET" ):
100 """Fetch a URL, automatically settling any x402 402 Payment Required response.
101
102 Returns a JSON string with status_code, body, and (on payment) payment_made.
103 """
104 url_error = _validate_url(url)
105 if url_error:
106 return json.dumps({ "error" : url_error})
107 if not PAYMENT_USER_ID :
108 return json.dumps({ "error" : "PAYMENT_USER_ID environment variable is required" })
109
110 response = httpx.request(method, url, timeout = 30 )
111 if response.status_code != 402 :
112 return json.dumps({ "status_code" : response.status_code, "body" : response.text})
113
114 if not _manager:
115 return json.dumps({
116 "status_code" : 402 ,
117 "error" : "No payment configuration. Set PAYMENT_MANAGER_ARN." ,
118 "body" : response.text,
119 })
120
121 # One idempotency token for the whole fetch: every retry replays the SAME
122 # authorization/nonce, so a transient 402 can be re-settled without ever double-charging.
123 client_token = str (uuid.uuid4())
124 for attempt in range ( 1 , MAX_PAYMENT_ATTEMPTS + 1 ):
125 try :
126 retry_response = _settle_and_retry(url, method, response, client_token)
127 except Exception as e: # noqa: BLE001 - surface any payment failure (incl. typed SDK errors) to the agent
128 return json.dumps({ "status_code" : 402 , "error" : f "Payment header generation failed: { e } " })
129
130 if retry_response.status_code != 402 :
131 # Success (2xx) or a non-transient error — return it; payment_made reflects the actual status.
132 return json.dumps({
133 "status_code" : retry_response.status_code,
134 "body" : retry_response.text,
135 "payment_made" : 200 <= retry_response.status_code < 300 ,
136 "payment_attempts" : attempt,
137 })
138
139 # Transient post-payment 402 — retry with the same idempotency token (same
140 # authorization/nonce), giving settlement another chance without double-charging.
141 response = retry_response
142
143 return json.dumps({
144 "status_code" : 402 ,
145 "error" : f "Paid and retried { MAX_PAYMENT_ATTEMPTS } times but the merchant still returned 402 "
146 "(transient on-chain settlement). Try again shortly." ,
147 "body" : response.text,
148 "payment_made" : False ,
149 "payment_attempts" : MAX_PAYMENT_ATTEMPTS ,
150 })