paynode-sdk-python 2.1.0__py3-none-any.whl → 2.2.1__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.1).
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
@@ -109,17 +111,22 @@ class PayNodeAgentClient:
109
111
  except Exception as e:
110
112
  logger.debug(f"⚠️ [PayNode-PY] Failed to parse 402 JSON body: {e}")
111
113
 
112
- if not body and b64_required:
114
+ header_body = None
115
+ if b64_required:
113
116
  try:
114
- import base64
115
- import json
116
- body = json.loads(base64.b64decode(b64_required).decode())
117
+ header_body = json.loads(base64.b64decode(b64_required).decode())
117
118
  except Exception as e:
118
- logger.warning(f"❌ [PayNode-PY] Failed to decode X-402-Required header: {e}")
119
+ logger.warning(f"❌ [PayNode-PY] Failed to decode PAYMENT-REQUIRED header: {e}")
120
+
121
+ if not body and header_body:
122
+ body = header_body
123
+ elif body and header_body and not body.get('x402Version'):
124
+ # Robustness: Merge header info into body if body is missing critical bits
125
+ body.update({k: v for k, v in header_body.items() if k not in body})
119
126
 
120
127
  if body and body.get('x402Version') == 2:
121
128
  logger.info("🚀 [PayNode-PY] x402 v2 detected. Handling autonomous payment...")
122
- if order_id: body['orderId'] = order_id
129
+ if order_id and not body.get('orderId'): body['orderId'] = order_id
123
130
  kwargs = self._handle_x402_v2(url, body, **kwargs)
124
131
  continue
125
132
 
@@ -202,29 +209,67 @@ class PayNodeAgentClient:
202
209
 
203
210
  payload_data = {"txHash": tx_hash}
204
211
 
205
- # Unified Payload for v3.1
212
+ # Official X402 V2 Payload with PayNode extensions
206
213
  payment_payload = {
207
- "version": "3.1",
208
- "type": ptype,
209
- "orderId": order_id,
210
- "payload": payload_data
214
+ "x402Version": 2,
215
+ "resource": requirements.get('resource'),
216
+ "accepted": {
217
+ "scheme": requirement.get('scheme'),
218
+ "network": requirement.get('network'),
219
+ "amount": requirement.get('amount'),
220
+ "asset": requirement.get('asset'),
221
+ "payTo": requirement.get('payTo'),
222
+ "maxTimeoutSeconds": requirement.get('maxTimeoutSeconds'),
223
+ "extra": requirement.get('extra', {})
224
+ },
225
+ "payload": payload_data,
226
+ "_paynode": {
227
+ "version": "2.2.1",
228
+ "type": ptype,
229
+ "orderId": order_id
230
+ }
211
231
  }
232
+
233
+ # Add router for onchain
234
+ if ptype == 'onchain':
235
+ payment_payload["accepted"]["router"] = requirement.get('router')
212
236
 
213
237
  logger.info(f"✅ [PayNode-PY] {ptype} payment prepared. Retrying request...")
214
238
 
215
- import json
216
- import base64
217
239
  b64_payload = base64.b64encode(json.dumps(payment_payload).encode()).decode()
218
240
 
219
241
  retry_headers = kwargs.get('headers', {}).copy()
220
242
  retry_headers.update({
221
243
  'Content-Type': 'application/json',
222
- 'X-402-Payload': b64_payload,
244
+ 'PAYMENT-SIGNATURE': b64_payload,
245
+ 'X-402-Payload': b64_payload, # Backward compatibility
223
246
  'X-402-Order-Id': order_id
224
247
  })
225
248
  kwargs['headers'] = retry_headers
226
249
  return kwargs
227
250
 
251
+ def _request_with_settlement_check(self, method: str, url: str, **kwargs) -> requests.Response:
252
+ """Wrapper to check for PAYMENT-RESPONSE after retry."""
253
+ response = self.session.request(method, url, **kwargs)
254
+
255
+ settle_header = response.headers.get('PAYMENT-RESPONSE') or response.headers.get('X-PAYMENT-RESPONSE')
256
+ if settle_header:
257
+ try:
258
+ settle_header_str = settle_header.strip()
259
+ if settle_header_str.startswith('{'):
260
+ decoded = settle_header_str
261
+ else:
262
+ decoded = base64.b64decode(settle_header).decode()
263
+ settle_data = json.loads(decoded)
264
+ if settle_data.get('success'):
265
+ logger.info(f"✅ [PayNode-PY] Settlement confirmed: {settle_data.get('transaction')}")
266
+ else:
267
+ logger.warning(f"⚠️ [PayNode-PY] Settlement failed: {settle_data.get('errorReason', 'Unknown error')}")
268
+ except Exception as e:
269
+ logger.warning(f"⚠️ [PayNode-PY] Failed to parse settlement response: {e}")
270
+
271
+ return response
272
+
228
273
  def sign_transfer_with_authorization(self, token_addr, to, amount, valid_after, valid_before, nonce, extra=None):
229
274
  extra = extra or {}
230
275
  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.1",
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 isinstance(parsed.get('version'), str) and parsed.get('version').startswith(("2.3", "2.2")):
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.1
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.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).
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.1)
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=ZL-Q67OjbnAnyUaGwd_IrScsHYfkVVdPccbRXL4xL04,20136
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=HkZoo3H4zpUCPw7uE6cCt4b9Zx_A1d7g3h7pYyLBx88,9663
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.1.dist-info/licenses/LICENSE,sha256=U8RjGlEBtXN6PA-qN_N3Uh60jyu3qe26ZBmgt-LAHc4,1069
10
+ paynode_sdk_python-2.2.1.dist-info/METADATA,sha256=LThVm20jSReaQ4BHbYyh_spVZEGsbKFcwnxmZBnAjkQ,3835
11
+ paynode_sdk_python-2.2.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ paynode_sdk_python-2.2.1.dist-info/top_level.txt,sha256=c6Skc1Xx-9O-JJ7sHghLW8Kyn4hyJoVPUawH1Mu8iTU,12
13
+ paynode_sdk_python-2.2.1.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,,