paynode-sdk-python 1.1.0__tar.gz → 1.1.1__tar.gz
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_python-1.1.1/LICENSE +21 -0
- paynode_sdk_python-1.1.1/PKG-INFO +107 -0
- paynode_sdk_python-1.1.1/README.md +87 -0
- {paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/paynode_sdk/__init__.py +7 -0
- paynode_sdk_python-1.1.1/paynode_sdk/client.py +216 -0
- paynode_sdk_python-1.1.1/paynode_sdk/constants.py +35 -0
- {paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/paynode_sdk/errors.py +1 -0
- {paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/paynode_sdk/middleware.py +7 -2
- {paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/paynode_sdk/verifier.py +10 -2
- paynode_sdk_python-1.1.1/paynode_sdk_python.egg-info/PKG-INFO +107 -0
- {paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/paynode_sdk_python.egg-info/SOURCES.txt +2 -2
- paynode_sdk_python-1.1.1/paynode_sdk_python.egg-info/requires.txt +9 -0
- {paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/pyproject.toml +14 -2
- paynode_sdk_python-1.1.1/tests/test_client.py +108 -0
- paynode_sdk_python-1.1.0/PKG-INFO +0 -67
- paynode_sdk_python-1.1.0/README.md +0 -54
- paynode_sdk_python-1.1.0/paynode_sdk/client.py +0 -218
- paynode_sdk_python-1.1.0/paynode_sdk/constants.py +0 -14
- paynode_sdk_python-1.1.0/paynode_sdk_python.egg-info/PKG-INFO +0 -67
- paynode_sdk_python-1.1.0/paynode_sdk_python.egg-info/requires.txt +0 -3
- paynode_sdk_python-1.1.0/tests/test_mainnet_live.py +0 -51
- paynode_sdk_python-1.1.0/tests/test_verifier.py +0 -116
- {paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/paynode_sdk/idempotency.py +0 -0
- {paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/paynode_sdk/webhook.py +0 -0
- {paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/paynode_sdk_python.egg-info/dependency_links.txt +0 -0
- {paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/paynode_sdk_python.egg-info/top_level.txt +0 -0
- {paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/setup.cfg +0 -0
|
@@ -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.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paynode-sdk-python
|
|
3
|
+
Version: 1.1.1
|
|
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
|
+
[](https://docs.paynode.dev)
|
|
24
|
+
[](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,87 @@
|
|
|
1
|
+
# PayNode Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://docs.paynode.dev)
|
|
4
|
+
[](https://pypi.org/project/paynode-sdk-python/)
|
|
5
|
+
|
|
6
|
+
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.
|
|
7
|
+
|
|
8
|
+
## 📖 Read the Docs
|
|
9
|
+
|
|
10
|
+
**For complete installation guides, advanced usage, API references, and architecture details, please visit our official documentation:**
|
|
11
|
+
👉 **[docs.paynode.dev](https://docs.paynode.dev)**
|
|
12
|
+
|
|
13
|
+
## ⚡ Quick Start
|
|
14
|
+
|
|
15
|
+
### Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install paynode-sdk-python web3
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Agent Client (Payer)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from paynode_sdk import PayNodeAgentClient
|
|
25
|
+
|
|
26
|
+
agent = PayNodeAgentClient(
|
|
27
|
+
private_key="YOUR_AGENT_PRIVATE_KEY",
|
|
28
|
+
rpc_urls=["https://mainnet.base.org", "https://rpc.ankr.com/base"]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Automatically handles the 402 challenge, executes the Base L2 transaction, and gets the data.
|
|
32
|
+
response = agent.request_gate("https://api.merchant.com/premium-data", method="POST", json={"agent": "PythonAgent"})
|
|
33
|
+
|
|
34
|
+
print(response.json())
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 🚀 Run the Demo
|
|
38
|
+
|
|
39
|
+
The SDK includes a complete Merchant/Agent demo in the `examples/` directory.
|
|
40
|
+
|
|
41
|
+
### 1. Setup Environment
|
|
42
|
+
|
|
43
|
+
Copy the example environment file and fill in your keys:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cp .env.example .env
|
|
47
|
+
# Edit .env with your private key and RPC URLs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Run the Merchant Server (FastAPI)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python examples/fastapi_server.py
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Run the Agent Client
|
|
57
|
+
|
|
58
|
+
In another terminal:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python examples/agent_client.py
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The demo will perform a full loop: `402 Handshake -> On-chain Payment -> 200 Verification`.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 📦 Publishing to PyPI
|
|
69
|
+
|
|
70
|
+
To publish a new version of the SDK:
|
|
71
|
+
|
|
72
|
+
1. **Install build tools**:
|
|
73
|
+
```bash
|
|
74
|
+
pip install build twine
|
|
75
|
+
```
|
|
76
|
+
2. **Build the package**:
|
|
77
|
+
```bash
|
|
78
|
+
python -m build
|
|
79
|
+
```
|
|
80
|
+
3. **Upload to PyPI**:
|
|
81
|
+
```bash
|
|
82
|
+
python -m twine upload dist/*
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
_Built for the Autonomous AI Economy by PayNodeLabs._
|
|
@@ -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
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import requests
|
|
5
|
+
from eth_account.messages import encode_typed_data
|
|
6
|
+
from web3 import Web3
|
|
7
|
+
from requests.adapters import HTTPAdapter
|
|
8
|
+
from urllib3.util.retry import Retry
|
|
9
|
+
from .constants import PAYNODE_ROUTER_ADDRESS, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS
|
|
10
|
+
from .errors import PayNodeException, ErrorCode
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("paynode_sdk.client")
|
|
13
|
+
|
|
14
|
+
class PayNodeAgentClient:
|
|
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]
|
|
22
|
+
self.w3 = self._init_w3()
|
|
23
|
+
|
|
24
|
+
# Initialize account and discard private key string
|
|
25
|
+
self.account = self.w3.eth.account.from_key(private_key)
|
|
26
|
+
self.nonce_lock = threading.Lock()
|
|
27
|
+
|
|
28
|
+
# Setup session
|
|
29
|
+
self.session = requests.Session()
|
|
30
|
+
retry_strategy = Retry(
|
|
31
|
+
total=3,
|
|
32
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
33
|
+
allowed_methods=["HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS", "TRACE"]
|
|
34
|
+
)
|
|
35
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
36
|
+
self.session.mount("https://", adapter)
|
|
37
|
+
self.session.mount("http://", adapter)
|
|
38
|
+
|
|
39
|
+
def _init_w3(self):
|
|
40
|
+
"""Finds a working RPC from the list."""
|
|
41
|
+
for rpc in self.rpc_urls:
|
|
42
|
+
try:
|
|
43
|
+
w3 = Web3(Web3.HTTPProvider(rpc, request_kwargs={'timeout': 5}))
|
|
44
|
+
if w3.is_connected():
|
|
45
|
+
return w3
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.warning(f"⚠️ [PayNode-PY] RPC {rpc} failed: {str(e)}")
|
|
48
|
+
continue
|
|
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)
|
|
54
|
+
|
|
55
|
+
def get(self, url, **kwargs):
|
|
56
|
+
return self.request_gate(url, "GET", **kwargs)
|
|
57
|
+
|
|
58
|
+
def post(self, url, **kwargs):
|
|
59
|
+
return self.request_gate(url, "POST", **kwargs)
|
|
60
|
+
|
|
61
|
+
def _request_with_402_retry(self, method, url, max_retries=3, **kwargs):
|
|
62
|
+
for _ in range(max_retries):
|
|
63
|
+
response = self.session.request(method, url, **kwargs)
|
|
64
|
+
if response.status_code == 402:
|
|
65
|
+
logger.info("💡 [PayNode-PY] 402 Detected. Handling payment...")
|
|
66
|
+
try:
|
|
67
|
+
kwargs = self._handle_402(response.headers, **kwargs)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
if isinstance(e, PayNodeException): raise
|
|
70
|
+
raise PayNodeException(f"An unexpected error occurred: {str(e)}", ErrorCode.INTERNAL_ERROR)
|
|
71
|
+
continue
|
|
72
|
+
return response
|
|
73
|
+
return response
|
|
74
|
+
|
|
75
|
+
def _handle_402(self, headers, **kwargs):
|
|
76
|
+
router_addr = headers.get('x-paynode-contract')
|
|
77
|
+
merchant_addr = headers.get('x-paynode-merchant')
|
|
78
|
+
amount_raw = int(headers.get('x-paynode-amount', 0))
|
|
79
|
+
token_addr = headers.get('x-paynode-token-address')
|
|
80
|
+
order_id = headers.get('x-paynode-order-id')
|
|
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
|
|
90
|
+
try:
|
|
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
|
+
|
|
99
|
+
logger.info(f"✅ [PayNode-PY] Payment successful: {tx_hash}")
|
|
100
|
+
except Exception as e:
|
|
101
|
+
if isinstance(e, PayNodeException): raise
|
|
102
|
+
raise PayNodeException(f"On-chain transaction reverted or failed: {str(e)}", ErrorCode.TRANSACTION_FAILED)
|
|
103
|
+
|
|
104
|
+
retry_headers = kwargs.get('headers', {}).copy()
|
|
105
|
+
retry_headers.update({'x-paynode-receipt': tx_hash, 'x-paynode-order-id': order_id})
|
|
106
|
+
kwargs['headers'] = retry_headers
|
|
107
|
+
return kwargs
|
|
108
|
+
|
|
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"}
|
|
126
|
+
]
|
|
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
|
+
}
|
|
160
|
+
|
|
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}
|
|
170
|
+
|
|
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": "p", "type": "address"}, {"name": "t", "type": "address"}, {"name": "m", "type": "address"}, {"name": "a", "type": "uint256"}, {"name": "o", "type": "bytes32"}, {"name": "d", "type": "uint256"}, {"name": "v", "type": "uint8"}, {"name": "r", "type": "bytes32"}, {"name": "s", "type": "bytes32"}], "name": "payWithPermit", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
|
|
175
|
+
router = self.w3.eth.contract(address=Web3.to_checksum_address(router_addr), abi=router_abi)
|
|
176
|
+
order_id_bytes = self.w3.keccak(text=order_id)
|
|
177
|
+
|
|
178
|
+
current_gas_price = int(self.w3.eth.gas_price * 1.2)
|
|
179
|
+
with self.nonce_lock:
|
|
180
|
+
nonce = self.w3.eth.get_transaction_count(self.account.address, 'pending')
|
|
181
|
+
tx = router.functions.payWithPermit(
|
|
182
|
+
self.account.address,
|
|
183
|
+
Web3.to_checksum_address(token_addr),
|
|
184
|
+
Web3.to_checksum_address(merchant_addr),
|
|
185
|
+
amount,
|
|
186
|
+
order_id_bytes,
|
|
187
|
+
sig["deadline"], sig["v"], sig["r"], sig["s"]
|
|
188
|
+
).build_transaction({
|
|
189
|
+
'from': self.account.address,
|
|
190
|
+
'nonce': nonce,
|
|
191
|
+
'gas': 300000,
|
|
192
|
+
'gasPrice': current_gas_price
|
|
193
|
+
})
|
|
194
|
+
signed_tx = self.account.sign_transaction(tx)
|
|
195
|
+
tx_h = self.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
196
|
+
|
|
197
|
+
self.w3.eth.wait_for_transaction_receipt(tx_h, timeout=60)
|
|
198
|
+
return self.w3.to_hex(tx_h)
|
|
199
|
+
|
|
200
|
+
def _execute_pay(self, router_addr, token_addr, merchant_addr, amount, order_id):
|
|
201
|
+
"""Standard pay method (fallback)."""
|
|
202
|
+
router_abi = [{"inputs": [{"name": "t", "type": "address"}, {"name": "m", "type": "address"}, {"name": "a", "type": "uint256"}, {"name": "o", "type": "bytes32"}], "name": "pay", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
|
|
203
|
+
router = self.w3.eth.contract(address=Web3.to_checksum_address(router_addr), abi=router_abi)
|
|
204
|
+
order_id_bytes = self.w3.keccak(text=order_id)
|
|
205
|
+
current_gas_price = int(self.w3.eth.gas_price * 1.2)
|
|
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)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Generated by scripts/sync-config.py
|
|
2
|
+
PAYNODE_ROUTER_ADDRESS = "0x92e20164FC457a2aC35f53D06268168e6352b200"
|
|
3
|
+
PAYNODE_ROUTER_ADDRESS_SANDBOX = "0xB587Bc36aaCf65962eCd6Ba59e2DA76f2f575408"
|
|
4
|
+
BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
5
|
+
BASE_USDC_ADDRESS_SANDBOX = "0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798"
|
|
6
|
+
BASE_USDC_DECIMALS = 6
|
|
7
|
+
|
|
8
|
+
PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E"
|
|
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"]
|
|
14
|
+
|
|
15
|
+
ACCEPTED_TOKENS = {
|
|
16
|
+
8453: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
|
|
17
|
+
84532: ["0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798"]
|
|
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
|
+
]
|
|
@@ -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'
|
|
@@ -6,9 +6,12 @@ from .verifier import PayNodeVerifier
|
|
|
6
6
|
from .errors import ErrorCode
|
|
7
7
|
from .idempotency import IdempotencyStore
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
10
|
+
|
|
11
|
+
class PayNodeMiddleware(BaseHTTPMiddleware):
|
|
10
12
|
def __init__(
|
|
11
13
|
self,
|
|
14
|
+
app: Any,
|
|
12
15
|
rpc_url: str,
|
|
13
16
|
contract_address: str,
|
|
14
17
|
merchant_address: str,
|
|
@@ -20,6 +23,7 @@ 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
28
|
self.verifier = PayNodeVerifier(rpc_url, contract_address, chain_id, store=store)
|
|
25
29
|
self.merchant_address = merchant_address
|
|
@@ -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
|
|
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={
|
|
@@ -1,5 +1,5 @@
|
|
|
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
|
|
|
@@ -26,7 +26,15 @@ class PayNodeVerifier:
|
|
|
26
26
|
if not self.w3:
|
|
27
27
|
return {"isValid": False, "error": PayNodeException("Verifier Provider Missing", ErrorCode.RPC_ERROR)}
|
|
28
28
|
|
|
29
|
-
# 0.
|
|
29
|
+
# 0. Dust Exploit Check (Minimum Payment)
|
|
30
|
+
amount = int(expected.get("amount", 0))
|
|
31
|
+
if amount < MIN_PAYMENT_AMOUNT:
|
|
32
|
+
return {"isValid": False, "error": PayNodeException(
|
|
33
|
+
f"Payment amount {amount} is below the minimum threshold of {MIN_PAYMENT_AMOUNT}.",
|
|
34
|
+
ErrorCode.AMOUNT_TOO_LOW
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
# 1. Token Whitelist Check (Anti-FakeToken)
|
|
30
38
|
expected_token = expected.get("tokenAddress", "").lower()
|
|
31
39
|
if self.accepted_tokens and expected_token not in self.accepted_tokens:
|
|
32
40
|
return {"isValid": False, "error": PayNodeException(
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paynode-sdk-python
|
|
3
|
+
Version: 1.1.1
|
|
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
|
+
[](https://docs.paynode.dev)
|
|
24
|
+
[](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._
|
{paynode_sdk_python-1.1.0 → paynode_sdk_python-1.1.1}/paynode_sdk_python.egg-info/SOURCES.txt
RENAMED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
LICENSE
|
|
1
2
|
README.md
|
|
2
3
|
pyproject.toml
|
|
3
4
|
paynode_sdk/__init__.py
|
|
@@ -13,5 +14,4 @@ paynode_sdk_python.egg-info/SOURCES.txt
|
|
|
13
14
|
paynode_sdk_python.egg-info/dependency_links.txt
|
|
14
15
|
paynode_sdk_python.egg-info/requires.txt
|
|
15
16
|
paynode_sdk_python.egg-info/top_level.txt
|
|
16
|
-
tests/
|
|
17
|
-
tests/test_verifier.py
|
|
17
|
+
tests/test_client.py
|