paynode-sdk-python 2.1.0__py3-none-any.whl → 2.2.0__py3-none-any.whl

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.
paynode_sdk/client.py CHANGED
@@ -1,3 +1,5 @@
1
+ import json
2
+ import base64
1
3
  import time
2
4
  import logging
3
5
  import threading
@@ -15,7 +17,7 @@ logger = logging.getLogger("paynode_sdk.client")
15
17
 
16
18
  class PayNodeAgentClient:
17
19
  """
18
- The main PayNode Client for AI Agents (v3.1).
20
+ The main PayNode Client for AI Agents (v2.2.0).
19
21
  Automatically handles the x402 'Payment Required' handshake.
20
22
  Supports RPC redundancy, EIP-2612 Permit, and EIP-3009 Authorization.
21
23
  """
@@ -93,13 +95,13 @@ class PayNodeAgentClient:
93
95
  def _request_with_402_retry(self, method: str, url: str, max_retries: int = 3, **kwargs) -> requests.Response:
94
96
  response = None
95
97
  for attempt in range(max_retries):
96
- response = self.session.request(method, url, **kwargs)
98
+ response = self._request_with_settlement_check(method, url, **kwargs)
97
99
  if response.status_code == 402:
98
100
  logger.info(f"💡 [PayNode-PY] 402 Detected (Attempt {attempt+1}/{max_retries}). Analyzing protocol version...")
99
101
 
100
- # Check for x402 v2 (JSON body or X-402-Required header)
102
+ # Check for x402 v2 (JSON body or official PAYMENT-REQUIRED header)
101
103
  content_type = response.headers.get('Content-Type', '')
102
- b64_required = response.headers.get('X-402-Required')
104
+ b64_required = response.headers.get('PAYMENT-REQUIRED') or response.headers.get('X-402-Required')
103
105
  order_id = response.headers.get('X-402-Order-Id')
104
106
 
105
107
  body = None
@@ -111,15 +113,13 @@ class PayNodeAgentClient:
111
113
 
112
114
  if not body and b64_required:
113
115
  try:
114
- import base64
115
- import json
116
116
  body = json.loads(base64.b64decode(b64_required).decode())
117
117
  except Exception as e:
118
- logger.warning(f"❌ [PayNode-PY] Failed to decode X-402-Required header: {e}")
118
+ logger.warning(f"❌ [PayNode-PY] Failed to decode PAYMENT-REQUIRED header: {e}")
119
119
 
120
120
  if body and body.get('x402Version') == 2:
121
121
  logger.info("🚀 [PayNode-PY] x402 v2 detected. Handling autonomous payment...")
122
- if order_id: body['orderId'] = order_id
122
+ if order_id and not body.get('orderId'): body['orderId'] = order_id
123
123
  kwargs = self._handle_x402_v2(url, body, **kwargs)
124
124
  continue
125
125
 
@@ -202,29 +202,62 @@ class PayNodeAgentClient:
202
202
 
203
203
  payload_data = {"txHash": tx_hash}
204
204
 
205
- # Unified Payload for v3.1
205
+ # Official X402 V2 Payload with PayNode extensions
206
206
  payment_payload = {
207
- "version": "3.1",
208
- "type": ptype,
209
- "orderId": order_id,
210
- "payload": payload_data
207
+ "x402Version": 2,
208
+ "resource": requirements.get('resource'),
209
+ "accepted": {
210
+ "scheme": requirement.get('scheme'),
211
+ "network": requirement.get('network'),
212
+ "amount": requirement.get('amount'),
213
+ "asset": requirement.get('asset'),
214
+ "payTo": requirement.get('payTo'),
215
+ "maxTimeoutSeconds": requirement.get('maxTimeoutSeconds'),
216
+ "extra": requirement.get('extra', {})
217
+ },
218
+ "payload": payload_data,
219
+ "_paynode": {
220
+ "version": "2.2.0",
221
+ "type": ptype,
222
+ "orderId": order_id
223
+ }
211
224
  }
225
+
226
+ # Add router for onchain
227
+ if ptype == 'onchain':
228
+ payment_payload["accepted"]["router"] = requirement.get('router')
212
229
 
213
230
  logger.info(f"✅ [PayNode-PY] {ptype} payment prepared. Retrying request...")
214
231
 
215
- import json
216
- import base64
217
232
  b64_payload = base64.b64encode(json.dumps(payment_payload).encode()).decode()
218
233
 
219
234
  retry_headers = kwargs.get('headers', {}).copy()
220
235
  retry_headers.update({
221
236
  'Content-Type': 'application/json',
222
- 'X-402-Payload': b64_payload,
237
+ 'PAYMENT-SIGNATURE': b64_payload,
238
+ 'X-402-Payload': b64_payload, # Backward compatibility
223
239
  'X-402-Order-Id': order_id
224
240
  })
225
241
  kwargs['headers'] = retry_headers
226
242
  return kwargs
227
243
 
244
+ def _request_with_settlement_check(self, method: str, url: str, **kwargs) -> requests.Response:
245
+ """Wrapper to check for PAYMENT-RESPONSE after retry."""
246
+ response = self.session.request(method, url, **kwargs)
247
+
248
+ settle_header = response.headers.get('PAYMENT-RESPONSE') or response.headers.get('X-PAYMENT-RESPONSE')
249
+ if settle_header:
250
+ try:
251
+ settle_data = json.loads(base64.b64decode(settle_header).decode())
252
+ if settle_data.get('success'):
253
+ logger.info(f"✅ [PayNode-PY] Settlement confirmed: {settle_data.get('transaction')}")
254
+ else:
255
+ logger.warning(f"⚠️ [PayNode-PY] Settlement failed: {settle_data.get('errorReason', 'Unknown error')}")
256
+ except Exception as e:
257
+ logger.warning(f"⚠️ [PayNode-PY] Failed to parse settlement response: {e}")
258
+
259
+ return response
260
+
228
261
  def sign_transfer_with_authorization(self, token_addr, to, amount, valid_after, valid_before, nonce, extra=None):
229
262
  extra = extra or {}
230
263
  token_addr = Web3.to_checksum_address(token_addr)
paynode_sdk/constants.py CHANGED
@@ -9,8 +9,8 @@ PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E"
9
9
  PROTOCOL_FEE_BPS = 100
10
10
  MIN_PAYMENT_AMOUNT = 1000
11
11
 
12
- BASE_RPC_URLS = ["https://mainnet.base.org", "https://base.meowrpc.com", "https://1rpc.io/base"]
13
- BASE_RPC_URLS_SANDBOX = ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"]
12
+ BASE_RPC_URLS = ['https://mainnet.base.org', 'https://base.meowrpc.com', 'https://1rpc.io/base']
13
+ BASE_RPC_URLS_SANDBOX = ['https://sepolia.base.org', 'https://base-sepolia-rpc.publicnode.com']
14
14
 
15
15
  ACCEPTED_TOKENS = {
16
16
  8453: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
paynode_sdk/errors.py CHANGED
@@ -28,7 +28,7 @@ ERROR_MESSAGES = {
28
28
  ErrorCode.transaction_not_found: "Transaction not found on-chain.",
29
29
  ErrorCode.wrong_contract: "Payment event was not emitted by the official PayNode contract.",
30
30
  ErrorCode.order_mismatch: "OrderId in receipt does not match requested ID.",
31
- ErrorCode.missing_receipt: "Please pay to PayNode contract and provide 'X-402-Payload' header.",
31
+ ErrorCode.missing_receipt: "Please pay to PayNode contract and provide 'PAYMENT-SIGNATURE' header.",
32
32
  }
33
33
 
34
34
  class PayNodeException(Exception):
paynode_sdk/middleware.py CHANGED
@@ -59,19 +59,51 @@ class PayNodeMiddleware(BaseHTTPMiddleware):
59
59
  self.max_timeout_seconds = kwargs.get('max_timeout_seconds', 3600)
60
60
 
61
61
  async def dispatch(self, request: Request, call_next):
62
- v2_payload_header = request.headers.get('X-402-Payload')
62
+ v2_payload_header = request.headers.get('PAYMENT-SIGNATURE') or request.headers.get('X-402-Payload')
63
63
  order_id = request.headers.get('X-402-Order-Id')
64
64
 
65
65
  if not order_id:
66
- order_id = self.generate_order_id(request)
66
+ order_id = (self.generate_order_id)(request)
67
67
 
68
68
  # Handle x402 v2 Unified Payload
69
69
  unified_payload = None
70
70
  if v2_payload_header:
71
71
  try:
72
- unified_payload = json.loads(base64.b64decode(v2_payload_header.encode()).decode())
72
+ parsed = json.loads(base64.b64decode(v2_payload_header.encode()).decode())
73
+
74
+ if parsed.get('x402Version') == 2 and parsed.get('accepted'):
75
+ # Official X402 V2 format - convert to internal format
76
+ internal_order_id = parsed.get('_paynode', {}).get('orderId') \
77
+ or order_id \
78
+ or f"auto_{int(time.time() * 1000)}"
79
+
80
+ # Infer type from payload content if missing
81
+ payload_content = parsed.get('payload', {})
82
+ inferred_type = 'onchain'
83
+ if payload_content.get('signature') or payload_content.get('authorization'):
84
+ inferred_type = 'eip3009'
85
+ elif payload_content.get('txHash'):
86
+ inferred_type = 'onchain'
87
+
88
+ p_type = parsed.get('_paynode', {}).get('type') or inferred_type
89
+
90
+ unified_payload = {
91
+ "version": "2.2.0",
92
+ "type": p_type,
93
+ "orderId": internal_order_id,
94
+ "router": parsed.get('accepted', {}).get('router'),
95
+ "payload": parsed.get('payload')
96
+ }
97
+ order_id = internal_order_id
98
+ elif parsed.get('version') == "2.2.0":
99
+ # Legacy PayNode format
100
+ unified_payload = parsed
101
+ if 'orderId' in unified_payload:
102
+ order_id = unified_payload['orderId']
103
+ elif 'order_id' in unified_payload:
104
+ order_id = unified_payload['order_id']
73
105
  except Exception as e:
74
- logger.error(f"❌ [PayNode-Middleware] Failed to decode X-402-Payload header: {e}")
106
+ logger.error(f"❌ [PayNode-Middleware] Failed to decode payment payload header: {e}")
75
107
 
76
108
  if unified_payload:
77
109
  try:
@@ -91,14 +123,43 @@ class PayNodeMiddleware(BaseHTTPMiddleware):
91
123
  )
92
124
  if result.get("isValid"):
93
125
  request.state.paynode = {"unified_payload": unified_payload, "orderId": order_id}
94
- return await call_next(request)
126
+
127
+ # Construct settlement response header
128
+ settle_response = {
129
+ "success": True,
130
+ "transaction": unified_payload.get("payload", {}).get("txHash") or "",
131
+ "network": f"eip155:{self.chain_id}",
132
+ "payer": result.get("payer", "")
133
+ }
134
+ b64_settle = base64.b64encode(json.dumps(settle_response).encode()).decode()
135
+
136
+ response = await call_next(request)
137
+ response.headers["PAYMENT-RESPONSE"] = b64_settle
138
+ response.headers["X-PAYMENT-RESPONSE"] = b64_settle # Compatibility
139
+ return response
95
140
  else:
96
141
  err = result.get("error")
142
+ error_code = err.code if hasattr(err, 'code') else ErrorCode.invalid_receipt
143
+
144
+ # Also include PAYMENT-RESPONSE header on failure for protocol symmetry
145
+ settle_fail = {
146
+ "success": False,
147
+ "errorReason": error_code,
148
+ "transaction": "",
149
+ "network": f"eip155:{self.chain_id}"
150
+ }
151
+ b64_settle_fail = base64.b64encode(json.dumps(settle_fail).encode()).decode()
152
+
153
+ headers = {
154
+ "PAYMENT-RESPONSE": b64_settle_fail,
155
+ "X-PAYMENT-RESPONSE": b64_settle_fail
156
+ }
97
157
  return JSONResponse(
98
158
  status_code=403,
159
+ headers=headers,
99
160
  content={
100
161
  "error": "Forbidden",
101
- "code": err.code if hasattr(err, 'code') else ErrorCode.invalid_receipt,
162
+ "code": error_code,
102
163
  "message": str(err)
103
164
  }
104
165
  )
@@ -144,6 +205,7 @@ class PayNodeMiddleware(BaseHTTPMiddleware):
144
205
  b64_required = base64.b64encode(json.dumps(v2_response).encode()).decode()
145
206
 
146
207
  headers = {
208
+ 'PAYMENT-REQUIRED': b64_required,
147
209
  'X-402-Required': b64_required,
148
210
  'X-402-Order-Id': order_id,
149
211
  }
paynode_sdk/verifier.py CHANGED
@@ -136,6 +136,7 @@ class PayNodeVerifier:
136
136
  order_id_bytes = self.w3.keccak(text=expected.get("orderId", ""))
137
137
 
138
138
  valid_log_found = False
139
+ found_payer = None
139
140
  order_id_mismatch_found = False
140
141
  for log in processed_logs:
141
142
  args = log.args
@@ -147,6 +148,7 @@ class PayNodeVerifier:
147
148
  if is_merchant_match and is_token_match and is_amount_match:
148
149
  if is_order_match:
149
150
  valid_log_found = True
151
+ found_payer = args.get("payer")
150
152
  break
151
153
  else:
152
154
  order_id_mismatch_found = True
@@ -161,7 +163,7 @@ class PayNodeVerifier:
161
163
  if not is_new:
162
164
  return {"isValid": False, "error": PayNodeException(ErrorCode.duplicate_transaction)}
163
165
 
164
- return {"isValid": True}
166
+ return {"isValid": True, "payer": found_payer}
165
167
 
166
168
  async def verify_transfer_with_authorization(
167
169
  self,
@@ -305,6 +307,6 @@ class PayNodeVerifier:
305
307
  if self.store: await self.store.delete(nonce)
306
308
  return {"isValid": False, "error": PayNodeException(ErrorCode.duplicate_transaction, message="Nonce already consumed on-chain")}
307
309
 
308
- return {"isValid": True}
310
+ return {"isValid": True, "payer": auth["from"]}
309
311
  except Exception as e:
310
312
  return {"isValid": False, "error": PayNodeException(ErrorCode.internal_error, message=str(e))}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paynode-sdk-python
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: PayNode Protocol Python SDK for AI Agents
5
5
  Author-email: PayNodeLabs <contact@paynode.dev>
6
6
  License: MIT
@@ -16,6 +16,8 @@ Provides-Extra: test
16
16
  Requires-Dist: pytest>=7.0.0; extra == "test"
17
17
  Requires-Dist: responses>=0.23.0; extra == "test"
18
18
  Requires-Dist: pytest-mock>=3.10.0; extra == "test"
19
+ Provides-Extra: webhook
20
+ Requires-Dist: aiohttp>=3.9.0; extra == "webhook"
19
21
  Dynamic: license-file
20
22
 
21
23
  # PayNode Python SDK
@@ -23,7 +25,7 @@ Dynamic: license-file
23
25
  [![Official Documentation](https://img.shields.io/badge/Docs-docs.paynode.dev-00ff88?style=for-the-badge&logo=readthedocs)](https://docs.paynode.dev)
24
26
  [![PyPI Version](https://img.shields.io/pypi/v/paynode-sdk-python.svg?style=for-the-badge)](https://pypi.org/project/paynode-sdk-python/)
25
27
 
26
- The official Python SDK for the **PayNode Protocol (v3.1)**. PayNode allows autonomous AI Agents to seamlessly pay for APIs and computational resources using USDC on Base L2, utilizing the standardized HTTP 402 protocol with support for both on-chain receipts and off-chain signatures (EIP-3009).
28
+ The official Python SDK for the **PayNode Protocol (v2.2.0)**. PayNode allows autonomous AI Agents to seamlessly pay for APIs and computational resources using USDC on Base L2, utilizing the standardized HTTP 402 protocol with support for both on-chain receipts and off-chain signatures (EIP-3009).
27
29
 
28
30
  ## 📖 Read the Docs
29
31
 
@@ -54,7 +56,7 @@ response = agent.request_gate("https://api.merchant.com/premium-data", method="P
54
56
  print(response.json())
55
57
  ```
56
58
 
57
- ### Key Features (v2.1)
59
+ ### Key Features (v2.2.0)
58
60
  - **EIP-3009 Support**: Sign payments off-chain using `TransferWithAuthorization`, allowing for gasless or relayer-mediated settlement.
59
61
  - **X402 V2 Protocol**: JSON-based handshake for more structured and machine-readable payment instructions.
60
62
  - **Dual Flow**: Automatic fallback to V1 (on-chain receipts) for legacy merchant support.
@@ -0,0 +1,13 @@
1
+ paynode_sdk/__init__.py,sha256=dDP3qUvuhpyeUcCRRIeaHMifaYPE_p6IwZjcmaHgAHU,1187
2
+ paynode_sdk/client.py,sha256=QMCm7y0bltBqToZvHl5VRjEmF7rlpF9SWpa1Wei5WHg,19543
3
+ paynode_sdk/constants.py,sha256=MaFiVkzD7pahM9Wn29Qgb9i5hvOafKpndJ8U4QylpWA,6346
4
+ paynode_sdk/errors.py,sha256=dpPXm-e0dHKOYkY87-DCB5LBf79hqPwUAaz9JmTomms,1965
5
+ paynode_sdk/idempotency.py,sha256=IOdyv8STj97EDGlwpGQnGE7K_NHRMmULvLTNTaglnB8,2450
6
+ paynode_sdk/middleware.py,sha256=plyoRmPft0r2WPuKyewYLW2V9HonhsHMTiDC7_ZYy3s,9604
7
+ paynode_sdk/verifier.py,sha256=AKy-OVwVKhShisycIywglHYgMBX1hhXFuj2zitK1EAE,14974
8
+ paynode_sdk/webhook.py,sha256=ngP0Az_-20gPVeHPzeaXuy1_AK1TTqqsmP-HCpAWEEM,8317
9
+ paynode_sdk_python-2.2.0.dist-info/licenses/LICENSE,sha256=U8RjGlEBtXN6PA-qN_N3Uh60jyu3qe26ZBmgt-LAHc4,1069
10
+ paynode_sdk_python-2.2.0.dist-info/METADATA,sha256=_n_jc3qWb3ZjKtA3dRUwzDNtsSqzhrLwaF58WIZKMHo,3835
11
+ paynode_sdk_python-2.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ paynode_sdk_python-2.2.0.dist-info/top_level.txt,sha256=c6Skc1Xx-9O-JJ7sHghLW8Kyn4hyJoVPUawH1Mu8iTU,12
13
+ paynode_sdk_python-2.2.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- paynode_sdk/__init__.py,sha256=dDP3qUvuhpyeUcCRRIeaHMifaYPE_p6IwZjcmaHgAHU,1187
2
- paynode_sdk/client.py,sha256=JGY2W00RDiYLjg4VWexaG85BepyWISTfZmZzUX2UabU,17770
3
- paynode_sdk/constants.py,sha256=axeDk308gMql4riCfSsFPWgk0O3o5FpeiIdDlpDnrgA,6346
4
- paynode_sdk/errors.py,sha256=0L7pxDA1V305mkmi886OFp5qIaAoVTLmIB0hFQB0zG8,1961
5
- paynode_sdk/idempotency.py,sha256=IOdyv8STj97EDGlwpGQnGE7K_NHRMmULvLTNTaglnB8,2450
6
- paynode_sdk/middleware.py,sha256=l3YY6ZsqJ3KBf_2vEtrJNm9XHUXEpJUpL8RG6xwDxC8,6342
7
- paynode_sdk/verifier.py,sha256=PxSBbhX7ba-0nUdNP_F-CazjN7zmqwmTUJZ6IXw3Nsw,14850
8
- paynode_sdk/webhook.py,sha256=ngP0Az_-20gPVeHPzeaXuy1_AK1TTqqsmP-HCpAWEEM,8317
9
- paynode_sdk_python-2.1.0.dist-info/licenses/LICENSE,sha256=U8RjGlEBtXN6PA-qN_N3Uh60jyu3qe26ZBmgt-LAHc4,1069
10
- paynode_sdk_python-2.1.0.dist-info/METADATA,sha256=MK9N2HYmcj3CXfSo0wbQiBmXn35Dg5CPXJDMbZpCErs,3757
11
- paynode_sdk_python-2.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
- paynode_sdk_python-2.1.0.dist-info/top_level.txt,sha256=c6Skc1Xx-9O-JJ7sHghLW8Kyn4hyJoVPUawH1Mu8iTU,12
13
- paynode_sdk_python-2.1.0.dist-info/RECORD,,