paynode-sdk-python 1.1.0__py3-none-any.whl → 1.1.2__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/__init__.py CHANGED
@@ -1,3 +1,10 @@
1
+ import warnings
2
+ import logging
3
+
4
+ # Silence upstream library deprecation warnings from web3's websocket dependency
5
+ # to ensure a clean experience for PayNode SDK users.
6
+ warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets.legacy")
7
+
1
8
  from .middleware import PayNodeMiddleware
2
9
  from .verifier import PayNodeVerifier
3
10
  from .errors import ErrorCode, PayNodeException
paynode_sdk/client.py CHANGED
@@ -2,6 +2,7 @@ import time
2
2
  import logging
3
3
  import threading
4
4
  import requests
5
+ from eth_account.messages import encode_typed_data
5
6
  from web3 import Web3
6
7
  from requests.adapters import HTTPAdapter
7
8
  from urllib3.util.retry import Retry
@@ -11,14 +12,20 @@ from .errors import PayNodeException, ErrorCode
11
12
  logger = logging.getLogger("paynode_sdk.client")
12
13
 
13
14
  class PayNodeAgentClient:
14
- def __init__(self, rpc_urls: list, private_key: str):
15
- self.rpc_urls = rpc_urls
15
+ """
16
+ The main PayNode Client for AI Agents (v1.1.1).
17
+ Automatically handles the x402 'Payment Required' handshake.
18
+ Supports RPC redundancy and EIP-2612 Permit-First payments.
19
+ """
20
+ def __init__(self, private_key: str, rpc_urls: list | str = "https://mainnet.base.org"):
21
+ self.rpc_urls = rpc_urls if isinstance(rpc_urls, list) else [rpc_urls]
16
22
  self.w3 = self._init_w3()
17
- # Initialize account and discard private key string to prevent Traceback leaks
23
+
24
+ # Initialize account and discard private key string
18
25
  self.account = self.w3.eth.account.from_key(private_key)
19
26
  self.nonce_lock = threading.Lock()
20
27
 
21
- # Setup session with standard HTTP retries for non-402 errors
28
+ # Setup session
22
29
  self.session = requests.Session()
23
30
  retry_strategy = Retry(
24
31
  total=3,
@@ -30,33 +37,37 @@ class PayNodeAgentClient:
30
37
  self.session.mount("http://", adapter)
31
38
 
32
39
  def _init_w3(self):
40
+ """Finds a working RPC from the list."""
33
41
  for rpc in self.rpc_urls:
34
42
  try:
35
- w3 = Web3(Web3.HTTPProvider(rpc, request_kwargs={'timeout': 10}))
43
+ w3 = Web3(Web3.HTTPProvider(rpc, request_kwargs={'timeout': 5}))
36
44
  if w3.is_connected():
37
45
  return w3
38
- except Exception:
46
+ except Exception as e:
47
+ logger.warning(f"⚠️ [PayNode-PY] RPC {rpc} failed: {str(e)}")
39
48
  continue
40
- raise PayNodeException("Failed to connect to any RPC URL", ErrorCode.RPC_ERROR)
49
+ raise PayNodeException("Failed to connect to any provided RPC nodes.", ErrorCode.RPC_ERROR)
50
+
51
+ def request_gate(self, url: str, method: str = "GET", **kwargs):
52
+ """The high-level autonomous method handling 402 loop."""
53
+ return self._request_with_402_retry(method.upper(), url, **kwargs)
41
54
 
42
55
  def get(self, url, **kwargs):
43
- return self._request_with_402_retry("GET", url, **kwargs)
56
+ return self.request_gate(url, "GET", **kwargs)
44
57
 
45
58
  def post(self, url, **kwargs):
46
- return self._request_with_402_retry("POST", url, **kwargs)
59
+ return self.request_gate(url, "POST", **kwargs)
47
60
 
48
61
  def _request_with_402_retry(self, method, url, max_retries=3, **kwargs):
49
- for attempt in range(max_retries):
62
+ for _ in range(max_retries):
50
63
  response = self.session.request(method, url, **kwargs)
51
64
  if response.status_code == 402:
52
65
  logger.info("💡 [PayNode-PY] 402 Detected. Handling payment...")
53
66
  try:
54
67
  kwargs = self._handle_402(response.headers, **kwargs)
55
68
  except Exception as e:
56
- if isinstance(e, PayNodeException):
57
- raise
58
- raise PayNodeException(f"Payment execution failed: {str(e)}", ErrorCode.INTERNAL_ERROR)
59
- time.sleep(1) # Backoff before retry
69
+ if isinstance(e, PayNodeException): raise
70
+ raise PayNodeException(f"An unexpected error occurred: {str(e)}", ErrorCode.INTERNAL_ERROR)
60
71
  continue
61
72
  return response
62
73
  return response
@@ -68,151 +79,138 @@ class PayNodeAgentClient:
68
79
  token_addr = headers.get('x-paynode-token-address')
69
80
  order_id = headers.get('x-paynode-order-id')
70
81
 
82
+ if not all([router_addr, merchant_addr, amount_raw, token_addr, order_id]):
83
+ raise PayNodeException("Malformed 402 headers: missing metadata", ErrorCode.INTERNAL_ERROR)
84
+
85
+ # v1.3 Constraint: Min payment protection
86
+ if amount_raw < 1000:
87
+ raise PayNodeException("Payment amount is below the protocol minimum (1000).", ErrorCode.AMOUNT_TOO_LOW)
88
+
89
+ # Protocol v1.3: Permit-First Execution
71
90
  try:
72
- self._ensure_allowance(token_addr, router_addr, amount_raw)
73
- tx_hash = self._execute_pay(router_addr, token_addr, merchant_addr, amount_raw, order_id)
91
+ # Check allowance first to decide if we need Permit
92
+ allowance = self._get_allowance(token_addr, router_addr)
93
+ if allowance >= amount_raw:
94
+ tx_hash = self._execute_pay(router_addr, token_addr, merchant_addr, amount_raw, order_id)
95
+ else:
96
+ logger.info("⚡ [PayNode-PY] Insufficient allowance. Attempting Permit-First payment...")
97
+ tx_hash = self.pay_with_permit_auto(router_addr, token_addr, merchant_addr, amount_raw, order_id)
98
+
74
99
  logger.info(f"✅ [PayNode-PY] Payment successful: {tx_hash}")
75
- except ValueError as e:
76
- err_msg = str(e).lower()
77
- if "insufficient funds" in err_msg:
78
- raise PayNodeException("Insufficient funds for gas or token.", ErrorCode.INSUFFICIENT_FUNDS)
79
- raise PayNodeException(f"Transaction failed: {str(e)}", ErrorCode.TRANSACTION_FAILED)
80
100
  except Exception as e:
81
- if isinstance(e, PayNodeException):
82
- raise
83
- raise PayNodeException(f"Unknown error during payment: {str(e)}", ErrorCode.INTERNAL_ERROR)
101
+ if isinstance(e, PayNodeException): raise
102
+ raise PayNodeException(f"On-chain transaction reverted or failed: {str(e)}", ErrorCode.TRANSACTION_FAILED)
84
103
 
85
104
  retry_headers = kwargs.get('headers', {}).copy()
86
- retry_headers.update({
87
- 'x-paynode-receipt': tx_hash,
88
- 'x-paynode-order-id': order_id
89
- })
105
+ retry_headers.update({'x-paynode-receipt': tx_hash, 'x-paynode-order-id': order_id})
90
106
  kwargs['headers'] = retry_headers
91
107
  return kwargs
92
108
 
93
- def _ensure_allowance(self, token_addr, spender_addr, amount):
94
- token_abi = [
95
- {"constant": True, "inputs": [{"name": "o", "type": "address"}, {"name": "s", "type": "address"}], "name": "allowance", "outputs": [{"name": "", "type": "uint256"}], "type": "function"},
96
- {"constant": False, "inputs": [{"name": "s", "type": "address"}, {"name": "a", "type": "uint256"}], "name": "approve", "outputs": [{"name": "", "type": "bool"}], "type": "function"}
109
+ def _get_allowance(self, token_addr, spender_addr):
110
+ abi = [{"constant": True, "inputs": [{"name": "o", "type": "address"}, {"name": "s", "type": "address"}], "name": "allowance", "outputs": [{"name": "", "type": "uint256"}], "type": "function"}]
111
+ token = self.w3.eth.contract(address=Web3.to_checksum_address(token_addr), abi=abi)
112
+ return token.functions.allowance(self.account.address, Web3.to_checksum_address(spender_addr)).call()
113
+
114
+ def sign_permit(self, token_addr, spender_addr, amount, deadline=None):
115
+ """Signs EIP-2612 Permit data."""
116
+ if deadline is None:
117
+ deadline = int(time.time()) + 3600
118
+
119
+ token_addr = Web3.to_checksum_address(token_addr)
120
+ spender_addr = Web3.to_checksum_address(spender_addr)
121
+
122
+ # Get nonce and domain separator
123
+ abi = [
124
+ {"inputs": [{"name": "o", "type": "address"}], "name": "nonces", "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
125
+ {"inputs": [], "name": "name", "outputs": [{"name": "", "type": "string"}], "stateMutability": "view", "type": "function"}
97
126
  ]
98
- token = self.w3.eth.contract(address=Web3.to_checksum_address(token_addr), abi=token_abi)
99
- allowance = token.functions.allowance(self.account.address, Web3.to_checksum_address(spender_addr)).call()
127
+ token = self.w3.eth.contract(address=token_addr, abi=abi)
128
+ nonce = token.functions.nonces(self.account.address).call()
129
+ name = token.functions.name().call()
130
+ chain_id = self.w3.eth.chain_id
131
+
132
+ domain = {
133
+ "name": name,
134
+ "version": "1",
135
+ "chainId": chain_id,
136
+ "verifyingContract": token_addr,
137
+ }
138
+ message = {
139
+ "owner": self.account.address,
140
+ "spender": spender_addr,
141
+ "value": amount,
142
+ "nonce": nonce,
143
+ "deadline": deadline,
144
+ }
145
+ types = {
146
+ "EIP712Domain": [
147
+ {"name": "name", "type": "string"},
148
+ {"name": "version", "type": "string"},
149
+ {"name": "chainId", "type": "uint256"},
150
+ {"name": "verifyingContract", "type": "address"},
151
+ ],
152
+ "Permit": [
153
+ {"name": "owner", "type": "address"},
154
+ {"name": "spender", "type": "address"},
155
+ {"name": "value", "type": "uint256"},
156
+ {"name": "nonce", "type": "uint256"},
157
+ {"name": "deadline", "type": "uint256"},
158
+ ],
159
+ }
100
160
 
101
- if allowance < amount:
102
- logger.info("🔐 [PayNode-PY] Allowance too low. Granting Infinite Approval...")
103
- current_gas_price = int(self.w3.eth.gas_price * 1.2)
104
-
105
- with self.nonce_lock:
106
- nonce = self.w3.eth.get_transaction_count(self.account.address, 'pending')
107
- tx = token.functions.approve(Web3.to_checksum_address(spender_addr), 2**256 - 1).build_transaction({
108
- 'from': self.account.address,
109
- 'nonce': nonce,
110
- 'gas': 100000,
111
- 'gasPrice': current_gas_price
112
- })
113
- signed_tx = self.account.sign_transaction(tx)
114
- tx_h = self.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
115
-
116
- logger.info(f"⏳ Waiting for approval confirmation: {self.w3.to_hex(tx_h)}...")
117
- self.w3.eth.wait_for_transaction_receipt(tx_h, timeout=60)
161
+ structured_data = {
162
+ "types": types,
163
+ "domain": domain,
164
+ "primaryType": "Permit",
165
+ "message": message,
166
+ }
167
+
168
+ signed = self.account.sign_typed_data(full_message=structured_data)
169
+ return {"v": signed.v, "r": signed.r, "s": signed.s, "deadline": deadline}
118
170
 
119
- def _execute_pay(self, router_addr, token_addr, merchant_addr, amount, order_id):
120
- router_abi = [
121
- {"inputs": [{"name": "t", "type": "address"}, {"name": "m", "type": "address"}, {"name": "a", "type": "uint256"}, {"name": "o", "type": "bytes32"}], "name": "pay", "outputs": [], "stateMutability": "nonpayable", "type": "function"},
122
- {"inputs": [{"name": "payer", "type": "address"}, {"name": "token", "type": "address"}, {"name": "merchant", "type": "address"}, {"name": "amount", "type": "uint256"}, {"name": "orderId", "type": "bytes32"}, {"name": "deadline", "type": "uint256"}, {"name": "v", "type": "uint8"}, {"name": "r", "type": "bytes32"}, {"name": "s", "type": "bytes32"}], "name": "payWithPermit", "outputs": [], "stateMutability": "nonpayable", "type": "function"}
123
- ]
171
+ def pay_with_permit_auto(self, router_addr, token_addr, merchant_addr, amount, order_id):
172
+ """Combines sign_permit and on-chain submission."""
173
+ sig = self.sign_permit(token_addr, router_addr, amount)
174
+ router_abi = [{"inputs": [{"name": "payer", "type": "address"}, {"name": "token", "type": "address"}, {"name": "merchant", "type": "address"}, {"name": "amount", "type": "uint256"}, {"name": "orderId", "type": "bytes32"}, {"name": "deadline", "type": "uint256"}, {"name": "v", "type": "uint8"}, {"name": "r", "type": "bytes32"}, {"name": "s", "type": "bytes32"}], "name": "payWithPermit", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
124
175
  router = self.w3.eth.contract(address=Web3.to_checksum_address(router_addr), abi=router_abi)
125
176
  order_id_bytes = self.w3.keccak(text=order_id)
126
-
177
+
127
178
  current_gas_price = int(self.w3.eth.gas_price * 1.2)
128
-
129
179
  with self.nonce_lock:
130
180
  nonce = self.w3.eth.get_transaction_count(self.account.address, 'pending')
131
- tx = router.functions.pay(
181
+ tx = router.functions.payWithPermit(
182
+ self.account.address,
132
183
  Web3.to_checksum_address(token_addr),
133
184
  Web3.to_checksum_address(merchant_addr),
134
185
  amount,
135
- order_id_bytes
186
+ order_id_bytes,
187
+ sig["deadline"], sig["v"], sig["r"], sig["s"]
136
188
  ).build_transaction({
137
189
  'from': self.account.address,
138
190
  'nonce': nonce,
139
- 'gas': 200000,
191
+ 'gas': 300000,
140
192
  'gasPrice': current_gas_price
141
193
  })
142
194
  signed_tx = self.account.sign_transaction(tx)
143
195
  tx_h = self.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
144
-
196
+
145
197
  self.w3.eth.wait_for_transaction_receipt(tx_h, timeout=60)
146
198
  return self.w3.to_hex(tx_h)
147
199
 
148
- def pay_with_permit(
149
- self,
150
- router_addr: str,
151
- payer_address: str,
152
- token_addr: str,
153
- merchant_addr: str,
154
- amount: int,
155
- order_id: str,
156
- deadline: int,
157
- v: int,
158
- r: bytes,
159
- s: bytes
160
- ) -> str:
161
- """
162
- Execute payment using EIP-2612 Permit — single-tx approve + pay.
163
- The payer signs the permit offline, and this Agent relays it on-chain.
164
-
165
- Args:
166
- router_addr: PayNode Router contract address
167
- payer_address: The address that holds the tokens and signed the permit
168
- token_addr: ERC20 token address (must support EIP-2612)
169
- merchant_addr: Merchant receiving 99% of payment
170
- amount: Token amount in smallest unit (e.g. 1000000 = 1 USDC)
171
- order_id: Order identifier string
172
- deadline: Unix timestamp after which the permit is invalid
173
- v: ECDSA recovery id
174
- r: ECDSA signature r component (bytes32)
175
- s: ECDSA signature s component (bytes32)
176
- """
177
- router_abi = [
178
- {"inputs": [{"name": "payer", "type": "address"}, {"name": "token", "type": "address"}, {"name": "merchant", "type": "address"}, {"name": "amount", "type": "uint256"}, {"name": "orderId", "type": "bytes32"}, {"name": "deadline", "type": "uint256"}, {"name": "v", "type": "uint8"}, {"name": "r", "type": "bytes32"}, {"name": "s", "type": "bytes32"}], "name": "payWithPermit", "outputs": [], "stateMutability": "nonpayable", "type": "function"}
179
- ]
200
+ def _execute_pay(self, router_addr, token_addr, merchant_addr, amount, order_id):
201
+ """Standard pay method (fallback)."""
202
+ router_abi = [{"inputs": [{"name": "token", "type": "address"}, {"name": "merchant", "type": "address"}, {"name": "amount", "type": "uint256"}, {"name": "orderId", "type": "bytes32"}], "name": "pay", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
180
203
  router = self.w3.eth.contract(address=Web3.to_checksum_address(router_addr), abi=router_abi)
181
204
  order_id_bytes = self.w3.keccak(text=order_id)
182
-
183
205
  current_gas_price = int(self.w3.eth.gas_price * 1.2)
184
-
185
- try:
186
- with self.nonce_lock:
187
- nonce = self.w3.eth.get_transaction_count(self.account.address, 'pending')
188
- tx = router.functions.payWithPermit(
189
- Web3.to_checksum_address(payer_address),
190
- Web3.to_checksum_address(token_addr),
191
- Web3.to_checksum_address(merchant_addr),
192
- amount,
193
- order_id_bytes,
194
- deadline,
195
- v,
196
- r,
197
- s
198
- ).build_transaction({
199
- 'from': self.account.address,
200
- 'nonce': nonce,
201
- 'gas': 300000,
202
- 'gasPrice': current_gas_price
203
- })
204
- signed_tx = self.account.sign_transaction(tx)
205
- tx_h = self.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
206
-
207
- self.w3.eth.wait_for_transaction_receipt(tx_h, timeout=60)
208
- logger.info(f"✅ [PayNode-PY] Permit payment confirmed: {self.w3.to_hex(tx_h)}")
209
- return self.w3.to_hex(tx_h)
210
- except ValueError as e:
211
- err_msg = str(e).lower()
212
- if "insufficient funds" in err_msg:
213
- raise PayNodeException("Insufficient funds for gas.", ErrorCode.INSUFFICIENT_FUNDS)
214
- raise PayNodeException(f"Permit transaction failed: {str(e)}", ErrorCode.PERMIT_FAILED)
215
- except Exception as e:
216
- if isinstance(e, PayNodeException):
217
- raise
218
- raise PayNodeException(f"Permit payment error: {str(e)}", ErrorCode.INTERNAL_ERROR)
206
+
207
+ with self.nonce_lock:
208
+ nonce = self.w3.eth.get_transaction_count(self.account.address, 'pending')
209
+ tx = router.functions.pay(Web3.to_checksum_address(token_addr), Web3.to_checksum_address(merchant_addr), amount, order_id_bytes).build_transaction({
210
+ 'from': self.account.address, 'nonce': nonce, 'gas': 200000, 'gasPrice': current_gas_price
211
+ })
212
+ signed_tx = self.account.sign_transaction(tx)
213
+ tx_h = self.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
214
+
215
+ self.w3.eth.wait_for_transaction_receipt(tx_h, timeout=60)
216
+ return self.w3.to_hex(tx_h)
paynode_sdk/constants.py CHANGED
@@ -7,8 +7,29 @@ BASE_USDC_DECIMALS = 6
7
7
 
8
8
  PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E"
9
9
  PROTOCOL_FEE_BPS = 100
10
+ MIN_PAYMENT_AMOUNT = 1000
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"]
10
14
 
11
15
  ACCEPTED_TOKENS = {
12
- 8453: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2"],
16
+ 8453: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
13
17
  84532: ["0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798"]
14
18
  }
19
+
20
+ PAYNODE_ROUTER_ABI = [
21
+ {
22
+ "anonymous": False,
23
+ "inputs": [
24
+ {"indexed": True, "name": "orderId", "type": "bytes32"},
25
+ {"indexed": True, "name": "merchant", "type": "address"},
26
+ {"indexed": True, "name": "payer", "type": "address"},
27
+ {"indexed": False, "name": "token", "type": "address"},
28
+ {"indexed": False, "name": "amount", "type": "uint256"},
29
+ {"indexed": False, "name": "fee", "type": "uint256"},
30
+ {"indexed": False, "name": "chainId", "type": "uint256"}
31
+ ],
32
+ "name": "PaymentReceived",
33
+ "type": "event"
34
+ }
35
+ ]
paynode_sdk/errors.py CHANGED
@@ -14,6 +14,7 @@ class ErrorCode(str, Enum):
14
14
  WRONG_MERCHANT = 'PAYNODE_WRONG_MERCHANT'
15
15
  WRONG_TOKEN = 'PAYNODE_WRONG_TOKEN'
16
16
  TOKEN_NOT_ACCEPTED = 'PAYNODE_TOKEN_NOT_ACCEPTED'
17
+ AMOUNT_TOO_LOW = 'PAYNODE_AMOUNT_TOO_LOW'
17
18
  INSUFFICIENT_FUNDS = 'PAYNODE_INSUFFICIENT_FUNDS'
18
19
  ORDER_MISMATCH = 'PAYNODE_ORDER_MISMATCH'
19
20
  PERMIT_FAILED = 'PAYNODE_PERMIT_FAILED'
paynode_sdk/middleware.py CHANGED
@@ -6,10 +6,13 @@ from .verifier import PayNodeVerifier
6
6
  from .errors import ErrorCode
7
7
  from .idempotency import IdempotencyStore
8
8
 
9
- class PayNodeMiddleware:
9
+ from starlette.middleware.base import BaseHTTPMiddleware
10
+
11
+ class PayNodeMiddleware(BaseHTTPMiddleware):
10
12
  def __init__(
11
13
  self,
12
- rpc_url: str,
14
+ app: Any,
15
+ rpc_urls: list | str,
13
16
  contract_address: str,
14
17
  merchant_address: str,
15
18
  chain_id: int,
@@ -20,8 +23,9 @@ class PayNodeMiddleware:
20
23
  store: Optional[IdempotencyStore] = None,
21
24
  generate_order_id: Optional[Callable[[Request], str]] = None
22
25
  ):
26
+ super().__init__(app)
23
27
  # The Verifier holds the state of the idempotency store
24
- self.verifier = PayNodeVerifier(rpc_url, contract_address, chain_id, store=store)
28
+ self.verifier = PayNodeVerifier(rpc_urls=rpc_urls, contract_address=contract_address, chain_id=chain_id, store=store)
25
29
  self.merchant_address = merchant_address
26
30
  self.contract_address = contract_address
27
31
  self.currency = currency
@@ -34,7 +38,7 @@ class PayNodeMiddleware:
34
38
  # Calculate raw amount (integer)
35
39
  self.amount_int = int(float(price) * (10 ** decimals))
36
40
 
37
- async def __call__(self, request: Request, call_next):
41
+ async def dispatch(self, request: Request, call_next):
38
42
  receipt_hash = request.headers.get('x-paynode-receipt')
39
43
  order_id = request.headers.get('x-paynode-order-id')
40
44
 
@@ -78,6 +82,7 @@ class PayNodeMiddleware:
78
82
  else:
79
83
  # Validation Failed
80
84
  err = result.get("error")
85
+ print(f"❌ [PayNode-PY] Verification Failed for Order: {order_id}. Reason: {str(err)}")
81
86
  return JSONResponse(
82
87
  status_code=403,
83
88
  content={
paynode_sdk/verifier.py CHANGED
@@ -1,13 +1,23 @@
1
1
  from .errors import ErrorCode, PayNodeException
2
- from .constants import PAYNODE_ROUTER_ABI, ACCEPTED_TOKENS
2
+ from .constants import PAYNODE_ROUTER_ABI, ACCEPTED_TOKENS, MIN_PAYMENT_AMOUNT
3
3
  from .idempotency import MemoryIdempotencyStore
4
4
  from web3 import Web3
5
5
 
6
6
  class PayNodeVerifier:
7
- def __init__(self, rpc_url=None, contract_address=None, chain_id=None, w3=None, store=None, accepted_tokens=None):
7
+ def __init__(self, rpc_urls=None, contract_address=None, chain_id=None, w3=None, store=None, accepted_tokens=None):
8
8
  self.w3 = w3
9
- if not self.w3 and rpc_url:
10
- self.w3 = Web3(Web3.HTTPProvider(rpc_url))
9
+ if not self.w3 and rpc_urls:
10
+ urls = rpc_urls if isinstance(rpc_urls, list) else [rpc_urls]
11
+ for rpc in urls:
12
+ try:
13
+ temp_w3 = Web3(Web3.HTTPProvider(rpc, request_kwargs={'timeout': 5}))
14
+ if temp_w3.is_connected():
15
+ self.w3 = temp_w3
16
+ break
17
+ except Exception:
18
+ continue
19
+ if not self.w3:
20
+ raise PayNodeException("Failed to connect to any provided RPC nodes.", ErrorCode.RPC_ERROR)
11
21
  self.contract_address = contract_address
12
22
  self.chain_id = int(chain_id) if chain_id else None
13
23
  self.store = store or MemoryIdempotencyStore()
@@ -26,7 +36,15 @@ class PayNodeVerifier:
26
36
  if not self.w3:
27
37
  return {"isValid": False, "error": PayNodeException("Verifier Provider Missing", ErrorCode.RPC_ERROR)}
28
38
 
29
- # 0. Token Whitelist Check (Anti-FakeToken)
39
+ # 0. Dust Exploit Check (Minimum Payment)
40
+ amount = int(expected.get("amount", 0))
41
+ if amount < MIN_PAYMENT_AMOUNT:
42
+ return {"isValid": False, "error": PayNodeException(
43
+ f"Payment amount {amount} is below the minimum threshold of {MIN_PAYMENT_AMOUNT}.",
44
+ ErrorCode.AMOUNT_TOO_LOW
45
+ )}
46
+
47
+ # 1. Token Whitelist Check (Anti-FakeToken)
30
48
  expected_token = expected.get("tokenAddress", "").lower()
31
49
  if self.accepted_tokens and expected_token not in self.accepted_tokens:
32
50
  return {"isValid": False, "error": PayNodeException(
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: paynode-sdk-python
3
+ Version: 1.1.2
4
+ Summary: PayNode Protocol Python SDK for AI Agents
5
+ Author-email: PayNodeLabs <contact@paynode.dev>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/PayNodeLabs/paynode-sdk-python
8
+ Keywords: paynode,x402,base,agentic-web3,payments
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: requests>=2.31.0
12
+ Requires-Dist: web3>=6.15.0
13
+ Requires-Dist: python-dotenv>=1.0.1
14
+ Requires-Dist: fastapi>=0.111.0
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=7.0.0; extra == "test"
17
+ Requires-Dist: responses>=0.23.0; extra == "test"
18
+ Requires-Dist: pytest-mock>=3.10.0; extra == "test"
19
+ Dynamic: license-file
20
+
21
+ # PayNode Python SDK
22
+
23
+ [![Official Documentation](https://img.shields.io/badge/Docs-docs.paynode.dev-00ff88?style=for-the-badge&logo=readthedocs)](https://docs.paynode.dev)
24
+ [![PyPI Version](https://img.shields.io/pypi/v/paynode-sdk-python.svg?style=for-the-badge)](https://pypi.org/project/paynode-sdk-python/)
25
+
26
+ The official Python SDK for the **PayNode Protocol**. PayNode allows autonomous AI Agents to seamlessly pay for APIs and computational resources using USDC on Base L2, utilizing the standardized HTTP 402 protocol.
27
+
28
+ ## 📖 Read the Docs
29
+
30
+ **For complete installation guides, advanced usage, API references, and architecture details, please visit our official documentation:**
31
+ 👉 **[docs.paynode.dev](https://docs.paynode.dev)**
32
+
33
+ ## ⚡ Quick Start
34
+
35
+ ### Installation
36
+
37
+ ```bash
38
+ pip install paynode-sdk-python web3
39
+ ```
40
+
41
+ ### Agent Client (Payer)
42
+
43
+ ```python
44
+ from paynode_sdk import PayNodeAgentClient
45
+
46
+ agent = PayNodeAgentClient(
47
+ private_key="YOUR_AGENT_PRIVATE_KEY",
48
+ rpc_urls=["https://mainnet.base.org", "https://rpc.ankr.com/base"]
49
+ )
50
+
51
+ # Automatically handles the 402 challenge, executes the Base L2 transaction, and gets the data.
52
+ response = agent.request_gate("https://api.merchant.com/premium-data", method="POST", json={"agent": "PythonAgent"})
53
+
54
+ print(response.json())
55
+ ```
56
+
57
+ ## 🚀 Run the Demo
58
+
59
+ The SDK includes a complete Merchant/Agent demo in the `examples/` directory.
60
+
61
+ ### 1. Setup Environment
62
+
63
+ Copy the example environment file and fill in your keys:
64
+
65
+ ```bash
66
+ cp .env.example .env
67
+ # Edit .env with your private key and RPC URLs
68
+ ```
69
+
70
+ ### 2. Run the Merchant Server (FastAPI)
71
+
72
+ ```bash
73
+ python examples/fastapi_server.py
74
+ ```
75
+
76
+ ### 3. Run the Agent Client
77
+
78
+ In another terminal:
79
+
80
+ ```bash
81
+ python examples/agent_client.py
82
+ ```
83
+
84
+ The demo will perform a full loop: `402 Handshake -> On-chain Payment -> 200 Verification`.
85
+
86
+ ---
87
+
88
+ ## 📦 Publishing to PyPI
89
+
90
+ To publish a new version of the SDK:
91
+
92
+ 1. **Install build tools**:
93
+ ```bash
94
+ pip install build twine
95
+ ```
96
+ 2. **Build the package**:
97
+ ```bash
98
+ python -m build
99
+ ```
100
+ 3. **Upload to PyPI**:
101
+ ```bash
102
+ python -m twine upload dist/*
103
+ ```
104
+
105
+ ---
106
+
107
+ _Built for the Autonomous AI Economy by PayNodeLabs._
@@ -0,0 +1,13 @@
1
+ paynode_sdk/__init__.py,sha256=p6URBqxFz1AErG7rIRWKnfwnzAC19qrMURtG7YhVR1I,821
2
+ paynode_sdk/client.py,sha256=rwn6bTSyIuIXd8aLnK5Nnd7R5p8Hx25ETzfNQil6y1k,10639
3
+ paynode_sdk/constants.py,sha256=puqz09qeKcMoigWrqdIzkhtAzFpECxEwHGLvDd3U_CQ,1429
4
+ paynode_sdk/errors.py,sha256=GN0qycgjATT47dF4yZ6Ulg18jf2OwholvYl0pgCvuzo,1121
5
+ paynode_sdk/idempotency.py,sha256=od7HuSxFdejBP0oE4QCzbJdrDZWvziiu09d3BRErU2k,999
6
+ paynode_sdk/middleware.py,sha256=KAcSVaVyApO4evby4vnv1erifqi3VzSBFbWelGGSF_w,3563
7
+ paynode_sdk/verifier.py,sha256=V7VXWqXzkObfN9v9agb7CWiLLwKbX1f2gsIWQE0qDdk,5274
8
+ paynode_sdk/webhook.py,sha256=KxlXGkXe3wpR8ueO8FMW2iypJgWaLwMfxAAZLNRvNDo,8302
9
+ paynode_sdk_python-1.1.2.dist-info/licenses/LICENSE,sha256=U8RjGlEBtXN6PA-qN_N3Uh60jyu3qe26ZBmgt-LAHc4,1069
10
+ paynode_sdk_python-1.1.2.dist-info/METADATA,sha256=_qVjPReImedgxEqo1w4FlZVgead-vXFxAGvR3nZVyx4,2858
11
+ paynode_sdk_python-1.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ paynode_sdk_python-1.1.2.dist-info/top_level.txt,sha256=c6Skc1Xx-9O-JJ7sHghLW8Kyn4hyJoVPUawH1Mu8iTU,12
13
+ paynode_sdk_python-1.1.2.dist-info/RECORD,,
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PayNode Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,67 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: paynode-sdk-python
3
- Version: 1.1.0
4
- Summary: PayNode Protocol Python SDK for AI Agents
5
- Author-email: PayNodeLabs <contact@paynode.dev>
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/PayNodeLabs/paynode-sdk-python
8
- Keywords: paynode,x402,base,agentic-web3,payments
9
- Description-Content-Type: text/markdown
10
- Requires-Dist: requests>=2.31.0
11
- Requires-Dist: web3>=6.15.0
12
- Requires-Dist: python-dotenv>=1.0.1
13
-
14
- # PayNode Python SDK
15
-
16
- [![Official Documentation](https://img.shields.io/badge/Docs-docs.paynode.dev-00ff88?style=for-the-badge&logo=readthedocs)](https://docs.paynode.dev)
17
- [![PyPI Version](https://img.shields.io/pypi/v/paynode-sdk-python.svg?style=for-the-badge)](https://pypi.org/project/paynode-sdk-python/)
18
-
19
- The official Python SDK for the **PayNode Protocol**. PayNode allows autonomous AI Agents to seamlessly pay for APIs and computational resources using USDC on Base L2, utilizing the standardized HTTP 402 protocol.
20
-
21
- ## 📖 Read the Docs
22
-
23
- **For complete installation guides, advanced usage, API references, and architecture details, please visit our official documentation:**
24
- 👉 **[docs.paynode.dev](https://docs.paynode.dev)**
25
-
26
- ## ⚡ Quick Start
27
-
28
- ### Installation
29
-
30
- ```bash
31
- pip install paynode-sdk-python web3
32
- ```
33
-
34
- ### Agent Client (Payer)
35
-
36
- ```python
37
- from paynode_sdk import Client
38
-
39
- agent = Client(private_key="YOUR_AGENT_PRIVATE_KEY")
40
-
41
- # Automatically handles the 402 challenge, executes the Base L2 transaction, and gets the data.
42
- response = agent.get("https://api.merchant.com/premium-data")
43
-
44
- print(response.json())
45
- ```
46
-
47
- ### Merchant Middleware (FastAPI Receiver)
48
-
49
- ```python
50
- from fastapi import FastAPI, Depends
51
- from paynode_sdk.middleware import PayNodeMiddleware
52
-
53
- app = FastAPI()
54
-
55
- # Protect routes with a 1.50 USDC fee requirement
56
- require_payment = PayNodeMiddleware(
57
- price=1.50,
58
- merchant_wallet="0xYourWalletAddress..."
59
- )
60
-
61
- @app.get("/premium-data", dependencies=[Depends(require_payment)])
62
- def get_premium_data():
63
- return {"secret": "This is paid M2M data."}
64
- ```
65
-
66
- ---
67
- *Built for the Autonomous AI Economy by PayNodeLabs.*
@@ -1,12 +0,0 @@
1
- paynode_sdk/__init__.py,sha256=dK8m7FfOsWUJakKqHaY44x-xC-uUWVwDrXa0G_zUs8E,562
2
- paynode_sdk/client.py,sha256=NWzsb9OryI5EEld-L-svQHk6sVwbCulepB7xSIAVr6A,10777
3
- paynode_sdk/constants.py,sha256=kmE45nR7XKUwn-FEt8CvcvzH3VoQbT-MD2mt71LPVEk,621
4
- paynode_sdk/errors.py,sha256=LaTwDJxinKYd2UD58lyANRm-rLXF3KSb1XHvIKOcgt0,1075
5
- paynode_sdk/idempotency.py,sha256=od7HuSxFdejBP0oE4QCzbJdrDZWvziiu09d3BRErU2k,999
6
- paynode_sdk/middleware.py,sha256=dg3hSmHAjW8wTF3AH3id8LFxbN2kXKKUsBRp1gyc6qw,3292
7
- paynode_sdk/verifier.py,sha256=huMXbBF361Yj9HLehaY34ToGBrYSuy5Uy8Jau3Seg7k,4414
8
- paynode_sdk/webhook.py,sha256=KxlXGkXe3wpR8ueO8FMW2iypJgWaLwMfxAAZLNRvNDo,8302
9
- paynode_sdk_python-1.1.0.dist-info/METADATA,sha256=_18F4H7sRGtdYA9-lA7kdEiO_n2Xnrv6Y_8XN-nj1VQ,2087
10
- paynode_sdk_python-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
- paynode_sdk_python-1.1.0.dist-info/top_level.txt,sha256=c6Skc1Xx-9O-JJ7sHghLW8Kyn4hyJoVPUawH1Mu8iTU,12
12
- paynode_sdk_python-1.1.0.dist-info/RECORD,,