agentpayments-python 0.1.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.
- agentpayments_python/__init__.py +30 -0
- agentpayments_python/challenge.py +151 -0
- agentpayments_python/constants.json +27 -0
- agentpayments_python/cookies.py +37 -0
- agentpayments_python/crawler.py +74 -0
- agentpayments_python/crypto.py +43 -0
- agentpayments_python/detection.py +28 -0
- agentpayments_python/django_adapter.py +232 -0
- agentpayments_python/fastapi_adapter.py +198 -0
- agentpayments_python/flask_adapter.py +167 -0
- agentpayments_python/grant_store.py +86 -0
- agentpayments_python/platform_client.py +117 -0
- agentpayments_python/ratelimit.py +43 -0
- agentpayments_python/solana.py +196 -0
- agentpayments_python/x402.py +84 -0
- agentpayments_python-0.1.0.dist-info/METADATA +152 -0
- agentpayments_python-0.1.0.dist-info/RECORD +20 -0
- agentpayments_python-0.1.0.dist-info/WHEEL +5 -0
- agentpayments_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- agentpayments_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
import threading
|
|
5
|
+
import time as _time
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
PAYMENT_CACHE_TTL = 10 * 60 # 10 minutes in seconds
|
|
14
|
+
NEGATIVE_CACHE_TTL = 30 # 30 seconds for negative results
|
|
15
|
+
PAYMENT_CACHE_MAX = 1000
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _PaymentCache:
|
|
19
|
+
"""Caches both positive (True) and negative (False) payment results with per-entry TTLs."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, max_size: int = PAYMENT_CACHE_MAX):
|
|
22
|
+
self.max_size = max_size
|
|
23
|
+
# stores (value: bool, ts: float, ttl: int)
|
|
24
|
+
self._cache: OrderedDict[str, tuple[bool, float, int]] = OrderedDict()
|
|
25
|
+
self._lock = threading.Lock()
|
|
26
|
+
|
|
27
|
+
def get(self, key: str):
|
|
28
|
+
"""Returns True, False, or None (not cached / expired)."""
|
|
29
|
+
with self._lock:
|
|
30
|
+
entry = self._cache.get(key)
|
|
31
|
+
if entry is None:
|
|
32
|
+
return None
|
|
33
|
+
value, ts, ttl = entry
|
|
34
|
+
if _time.time() - ts > ttl:
|
|
35
|
+
del self._cache[key]
|
|
36
|
+
return None
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
def set(self, key: str, value: bool, ttl: int) -> None:
|
|
40
|
+
with self._lock:
|
|
41
|
+
if len(self._cache) >= self.max_size:
|
|
42
|
+
self._cache.popitem(last=False)
|
|
43
|
+
self._cache[key] = (value, _time.time(), ttl)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_payment_cache = _PaymentCache()
|
|
47
|
+
|
|
48
|
+
BASE58_RE = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$")
|
|
49
|
+
|
|
50
|
+
_constants = json.loads((Path(__file__).resolve().parent / "constants.json").read_text())
|
|
51
|
+
MIN_PAYMENT_MICRO = round(_constants["MIN_PAYMENT"] * 1_000_000) # integer micro-USDC threshold
|
|
52
|
+
USDC_MINT_DEVNET = _constants["USDC_MINT_DEVNET"]
|
|
53
|
+
USDC_MINT_MAINNET = _constants["USDC_MINT_MAINNET"]
|
|
54
|
+
RPC_DEVNET = _constants["RPC_DEVNET"]
|
|
55
|
+
RPC_MAINNET = _constants["RPC_MAINNET"]
|
|
56
|
+
MEMO_PROGRAM = _constants["MEMO_PROGRAM"]
|
|
57
|
+
MIN_PAYMENT = _constants["MIN_PAYMENT"]
|
|
58
|
+
MAX_TRANSACTIONS_PER_VERIFY = _constants["MAX_TRANSACTIONS_PER_VERIFY"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _rpc_call(rpc_url: str, method: str, params: list, retries: int = 2, backoff: float = 0.3) -> dict:
|
|
62
|
+
last_error: Exception = RuntimeError("RPC call failed before any attempt")
|
|
63
|
+
for attempt in range(retries + 1):
|
|
64
|
+
if attempt > 0:
|
|
65
|
+
import time as _sleep_time
|
|
66
|
+
_sleep_time.sleep(backoff * attempt)
|
|
67
|
+
try:
|
|
68
|
+
resp = requests.post(rpc_url, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=30)
|
|
69
|
+
# Only retry on 5xx (transient server errors); 4xx are permanent.
|
|
70
|
+
if resp.status_code >= 500:
|
|
71
|
+
last_error = requests.HTTPError(f"RPC {method} failed: {resp.status_code}", response=resp)
|
|
72
|
+
continue
|
|
73
|
+
resp.raise_for_status()
|
|
74
|
+
return resp.json()
|
|
75
|
+
except requests.HTTPError:
|
|
76
|
+
raise # permanent 4xx — don't retry
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
last_error = exc
|
|
79
|
+
raise last_error
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _rpc_call_with_fallback(rpc_urls: list[str], method: str, params: list, **kwargs) -> dict:
|
|
83
|
+
"""Try each RPC URL in order; move to the next on network/5xx failure."""
|
|
84
|
+
last_error: Exception = RuntimeError("No RPC URLs provided")
|
|
85
|
+
for url in rpc_urls:
|
|
86
|
+
try:
|
|
87
|
+
return _rpc_call(url, method, params, **kwargs)
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
last_error = exc
|
|
90
|
+
if len(rpc_urls) > 1:
|
|
91
|
+
logger.warning("[gate] RPC endpoint failed, trying fallback: url=%s error=%s", url, exc)
|
|
92
|
+
raise last_error
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_valid_solana_address(address: str) -> bool:
|
|
96
|
+
return bool(address and BASE58_RE.match(address))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def verify_payment_on_chain(agent_key: str, wallet_address: str, rpc_url, usdc_mint: str) -> bool:
|
|
100
|
+
"""Verify payment on-chain. rpc_url may be a string or list of strings (fallback URLs)."""
|
|
101
|
+
# Normalise to list so _rpc_call_with_fallback always gets a list.
|
|
102
|
+
rpc_urls: list[str] = rpc_url if isinstance(rpc_url, list) else [rpc_url]
|
|
103
|
+
|
|
104
|
+
cached = _payment_cache.get(agent_key)
|
|
105
|
+
if cached is True:
|
|
106
|
+
return True
|
|
107
|
+
if cached is False:
|
|
108
|
+
return False # negative cached — skip RPC until TTL expires
|
|
109
|
+
if not is_valid_solana_address(wallet_address):
|
|
110
|
+
logger.error("[gate] Invalid wallet address: %s", wallet_address)
|
|
111
|
+
return False
|
|
112
|
+
try:
|
|
113
|
+
# commitment: 'finalized' — confirmed blocks can be rolled back (rare but possible).
|
|
114
|
+
# Finalized adds ~10-20 s latency vs confirmed but guarantees irreversibility.
|
|
115
|
+
ata_data = _rpc_call_with_fallback(rpc_urls, "getTokenAccountsByOwner", [wallet_address, {"mint": usdc_mint}, {"encoding": "jsonParsed", "commitment": "finalized"}])
|
|
116
|
+
token_accounts = [a["pubkey"] for a in ata_data.get("result", {}).get("value", [])]
|
|
117
|
+
# Only transfers landing in one of the vendor's USDC token accounts count as
|
|
118
|
+
# payment. Token accounts are mint-bound, so membership also guarantees the
|
|
119
|
+
# token is USDC for plain `transfer` instructions (which carry no mint field).
|
|
120
|
+
vendor_usdc_accounts = set(token_accounts)
|
|
121
|
+
if not vendor_usdc_accounts:
|
|
122
|
+
return False # vendor has no USDC account yet — no payment possible
|
|
123
|
+
|
|
124
|
+
addresses_to_scan = [wallet_address] + token_accounts
|
|
125
|
+
seen = set()
|
|
126
|
+
all_signatures = []
|
|
127
|
+
|
|
128
|
+
for addr in addresses_to_scan:
|
|
129
|
+
sigs_data = _rpc_call_with_fallback(rpc_urls, "getSignaturesForAddress", [addr, {"limit": 100, "commitment": "finalized"}])
|
|
130
|
+
for sig in sigs_data.get("result", []):
|
|
131
|
+
if sig["signature"] not in seen:
|
|
132
|
+
seen.add(sig["signature"])
|
|
133
|
+
all_signatures.append(sig)
|
|
134
|
+
|
|
135
|
+
tx_call_count = 0
|
|
136
|
+
for sig_info in all_signatures:
|
|
137
|
+
if tx_call_count >= MAX_TRANSACTIONS_PER_VERIFY:
|
|
138
|
+
logger.warning("[gate] getTransaction cap reached (key=%s..., cap=%d)", agent_key[:12], MAX_TRANSACTIONS_PER_VERIFY)
|
|
139
|
+
break
|
|
140
|
+
if sig_info.get("err"):
|
|
141
|
+
continue
|
|
142
|
+
tx_call_count += 1
|
|
143
|
+
|
|
144
|
+
tx_data = _rpc_call_with_fallback(rpc_urls, "getTransaction", [sig_info["signature"], {"encoding": "jsonParsed", "commitment": "finalized", "maxSupportedTransactionVersion": 0}])
|
|
145
|
+
tx = tx_data.get("result")
|
|
146
|
+
if not tx:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
instructions = tx.get("transaction", {}).get("message", {}).get("instructions", [])
|
|
150
|
+
inner_instructions = tx.get("meta", {}).get("innerInstructions", [])
|
|
151
|
+
all_ix = list(instructions)
|
|
152
|
+
for group in inner_instructions:
|
|
153
|
+
all_ix.extend(group.get("instructions", []))
|
|
154
|
+
|
|
155
|
+
has_memo = False
|
|
156
|
+
has_payment = False
|
|
157
|
+
|
|
158
|
+
for ix in all_ix:
|
|
159
|
+
program = ix.get("program", "")
|
|
160
|
+
program_id = ix.get("programId", "")
|
|
161
|
+
if program == "spl-memo" or program_id == MEMO_PROGRAM:
|
|
162
|
+
parsed = ix.get("parsed", "")
|
|
163
|
+
memo_text = parsed if isinstance(parsed, str) else str(parsed)
|
|
164
|
+
if agent_key in memo_text:
|
|
165
|
+
has_memo = True
|
|
166
|
+
|
|
167
|
+
if program == "spl-token":
|
|
168
|
+
parsed = ix.get("parsed", {})
|
|
169
|
+
tx_type = parsed.get("type", "")
|
|
170
|
+
if tx_type in ("transfer", "transferChecked"):
|
|
171
|
+
info = parsed.get("info", {})
|
|
172
|
+
# Payment must be delivered to one of the vendor's USDC token accounts.
|
|
173
|
+
if info.get("destination") not in vendor_usdc_accounts:
|
|
174
|
+
continue
|
|
175
|
+
if tx_type == "transferChecked" and info.get("mint") != usdc_mint:
|
|
176
|
+
continue
|
|
177
|
+
# Integer base-unit comparison — avoids float precision issues at
|
|
178
|
+
# the payment threshold. tokenAmount.amount and amount are both
|
|
179
|
+
# integer strings in micro-USDC (base units).
|
|
180
|
+
token_amount = info.get("tokenAmount") or {}
|
|
181
|
+
amount_str = token_amount.get("amount") or info.get("amount", "0")
|
|
182
|
+
try:
|
|
183
|
+
amount_micro = int(amount_str)
|
|
184
|
+
except (ValueError, TypeError):
|
|
185
|
+
amount_micro = 0
|
|
186
|
+
if amount_micro >= MIN_PAYMENT_MICRO:
|
|
187
|
+
has_payment = True
|
|
188
|
+
|
|
189
|
+
if has_memo and has_payment:
|
|
190
|
+
_payment_cache.set(agent_key, True, PAYMENT_CACHE_TTL)
|
|
191
|
+
return True
|
|
192
|
+
except Exception:
|
|
193
|
+
logger.exception("[gate] Solana RPC error")
|
|
194
|
+
|
|
195
|
+
_payment_cache.set(agent_key, False, NEGATIVE_CACHE_TTL)
|
|
196
|
+
return False
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
x402 protocol compatibility helpers.
|
|
3
|
+
|
|
4
|
+
Builds x402-standard PaymentRequirements objects and the X-PAYMENT-REQUIRED
|
|
5
|
+
header value so that x402-aware AI agent clients can parse the payment
|
|
6
|
+
requirements from our 402 responses.
|
|
7
|
+
|
|
8
|
+
Spec: https://github.com/x402-foundation/x402/blob/main/specs/schemes/exact/scheme_exact_svm.md
|
|
9
|
+
"""
|
|
10
|
+
import base64
|
|
11
|
+
import json as _json
|
|
12
|
+
import math
|
|
13
|
+
from pathlib import Path as _Path
|
|
14
|
+
|
|
15
|
+
_constants = _json.loads((_Path(__file__).resolve().parent / "constants.json").read_text())
|
|
16
|
+
|
|
17
|
+
USDC_DECIMALS: int = _constants["USDC_DECIMALS"]
|
|
18
|
+
X402_VERSION: int = _constants["X402_VERSION"]
|
|
19
|
+
SOLANA_CHAIN_ID_MAINNET: str = _constants["SOLANA_CHAIN_ID_MAINNET"]
|
|
20
|
+
SOLANA_CHAIN_ID_DEVNET: str = _constants["SOLANA_CHAIN_ID_DEVNET"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_payment_requirements(
|
|
24
|
+
*,
|
|
25
|
+
wallet_address: str,
|
|
26
|
+
mint: str,
|
|
27
|
+
min_payment: float,
|
|
28
|
+
debug: bool,
|
|
29
|
+
agent_key: str = "",
|
|
30
|
+
resource: str = "",
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""
|
|
33
|
+
Build an x402-standard PaymentRequirements dict for the Solana exact scheme.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
wallet_address: Merchant wallet public key (payTo).
|
|
37
|
+
mint: USDC mint address.
|
|
38
|
+
min_payment: Human-readable amount (e.g. 0.01 for 0.01 USDC).
|
|
39
|
+
debug: True → use devnet chain ID.
|
|
40
|
+
agent_key: If set, included as extra.memo so x402 clients know
|
|
41
|
+
which key to reference in their transaction memo.
|
|
42
|
+
resource: URL path of the gated resource (optional).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
PaymentRequirements dict per the x402 SVM exact scheme spec.
|
|
46
|
+
"""
|
|
47
|
+
chain_id = SOLANA_CHAIN_ID_DEVNET if debug else SOLANA_CHAIN_ID_MAINNET
|
|
48
|
+
base_units = str(math.floor(min_payment * (10 ** USDC_DECIMALS) + 0.5)) # round half-up
|
|
49
|
+
req: dict = {
|
|
50
|
+
"scheme": "exact",
|
|
51
|
+
"network": chain_id,
|
|
52
|
+
"amount": base_units,
|
|
53
|
+
"asset": mint,
|
|
54
|
+
"payTo": wallet_address,
|
|
55
|
+
"maxTimeoutSeconds": 300,
|
|
56
|
+
"extra": {
|
|
57
|
+
"name": "USDC",
|
|
58
|
+
"decimals": USDC_DECIMALS,
|
|
59
|
+
**({"memo": agent_key} if agent_key else {}),
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
if resource:
|
|
63
|
+
req["resource"] = resource
|
|
64
|
+
return req
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def payment_required_header(payment_requirements: dict) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Return the value for the X-PAYMENT-REQUIRED response header.
|
|
70
|
+
The spec requires the PaymentRequirements to be base64-encoded JSON.
|
|
71
|
+
"""
|
|
72
|
+
return base64.b64encode(_json.dumps(payment_requirements).encode()).decode()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def enrich_402_body(body: dict, payment_requirements: dict) -> dict:
|
|
76
|
+
"""
|
|
77
|
+
Prepend x402Version and accepts[] to a 402 response body dict,
|
|
78
|
+
keeping all existing fields for backward compatibility.
|
|
79
|
+
"""
|
|
80
|
+
return {
|
|
81
|
+
"x402Version": X402_VERSION,
|
|
82
|
+
"accepts": [payment_requirements],
|
|
83
|
+
**body,
|
|
84
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentpayments-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AgentPayments gate for Python web frameworks — charge AI agents USDC on Solana before they can access your API
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/adambrzosko/AgentPayments
|
|
7
|
+
Project-URL: Documentation, https://github.com/adambrzosko/AgentPayments/tree/main/sdk/python
|
|
8
|
+
Project-URL: Repository, https://github.com/adambrzosko/AgentPayments
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/adambrzosko/AgentPayments/issues
|
|
10
|
+
Keywords: agentpayments,ai-agents,solana,usdc,payments,middleware,x402
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: requests>=2.31
|
|
24
|
+
Provides-Extra: django
|
|
25
|
+
Requires-Dist: Django>=5.0; extra == "django"
|
|
26
|
+
Provides-Extra: fastapi
|
|
27
|
+
Requires-Dist: fastapi>=0.110; extra == "fastapi"
|
|
28
|
+
Requires-Dist: starlette>=0.37; extra == "fastapi"
|
|
29
|
+
Provides-Extra: flask
|
|
30
|
+
Requires-Dist: flask>=3.0; extra == "flask"
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: Django>=5.0; extra == "all"
|
|
33
|
+
Requires-Dist: fastapi>=0.110; extra == "all"
|
|
34
|
+
Requires-Dist: starlette>=0.37; extra == "all"
|
|
35
|
+
Requires-Dist: flask>=3.0; extra == "all"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# agentpayments-python
|
|
39
|
+
|
|
40
|
+
Python adapters for the AgentPayments gate. Supports Django, FastAPI/Starlette, and Flask.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install agentpayments-python
|
|
46
|
+
# or, in this monorepo:
|
|
47
|
+
# pip install -e sdk/python
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Django
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# settings.py
|
|
54
|
+
MIDDLEWARE = [
|
|
55
|
+
"django.middleware.security.SecurityMiddleware",
|
|
56
|
+
"agentpayments_python.django_adapter.GateMiddleware",
|
|
57
|
+
# ... other middleware
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# Required settings
|
|
61
|
+
CHALLENGE_SECRET = os.environ["CHALLENGE_SECRET"]
|
|
62
|
+
HOME_WALLET_ADDRESS = os.environ["HOME_WALLET_ADDRESS"]
|
|
63
|
+
|
|
64
|
+
# Optional settings
|
|
65
|
+
SOLANA_RPC_URL = os.environ.get("SOLANA_RPC_URL")
|
|
66
|
+
USDC_MINT = os.environ.get("USDC_MINT")
|
|
67
|
+
DEBUG = True # True = devnet, False = mainnet
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## FastAPI
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from fastapi import FastAPI, Request
|
|
74
|
+
from agentpayments_python.fastapi_adapter import (
|
|
75
|
+
AgentPaymentsASGIMiddleware,
|
|
76
|
+
challenge_verify_endpoint,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
app = FastAPI()
|
|
80
|
+
|
|
81
|
+
app.add_middleware(
|
|
82
|
+
AgentPaymentsASGIMiddleware,
|
|
83
|
+
challenge_secret=os.environ["CHALLENGE_SECRET"],
|
|
84
|
+
home_wallet_address=os.environ["HOME_WALLET_ADDRESS"],
|
|
85
|
+
debug=True,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@app.post("/__challenge/verify")
|
|
89
|
+
async def verify(request: Request):
|
|
90
|
+
return await challenge_verify_endpoint(
|
|
91
|
+
request,
|
|
92
|
+
challenge_secret=os.environ["CHALLENGE_SECRET"],
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Flask
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from flask import Flask
|
|
100
|
+
from agentpayments_python.flask_adapter import register_agentpayments
|
|
101
|
+
|
|
102
|
+
app = Flask(__name__)
|
|
103
|
+
register_agentpayments(
|
|
104
|
+
app,
|
|
105
|
+
challenge_secret=os.environ["CHALLENGE_SECRET"],
|
|
106
|
+
home_wallet_address=os.environ["HOME_WALLET_ADDRESS"],
|
|
107
|
+
debug=True,
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Configuration
|
|
112
|
+
|
|
113
|
+
| Parameter | Required | Default | Description |
|
|
114
|
+
|---|---|---|---|
|
|
115
|
+
| `challenge_secret` | Yes (production) | `'default-secret-change-me'` | HMAC secret for signing cookies, nonces, and agent keys. |
|
|
116
|
+
| `home_wallet_address` | Yes | `''` | Solana wallet address to receive USDC payments. |
|
|
117
|
+
| `solana_rpc_url` | No | Auto (devnet/mainnet) | Custom Solana RPC endpoint. |
|
|
118
|
+
| `usdc_mint` | No | Auto (devnet/mainnet) | Custom USDC mint address. |
|
|
119
|
+
| `debug` | No | `True` | `True` = devnet. `False` = mainnet + strict mode. |
|
|
120
|
+
|
|
121
|
+
Django reads these from `settings.*` (e.g., `settings.CHALLENGE_SECRET`). FastAPI and Flask accept them as constructor arguments.
|
|
122
|
+
|
|
123
|
+
## Security Features
|
|
124
|
+
|
|
125
|
+
- **Timing-safe HMAC comparison** — uses `hmac.compare_digest()` for all signature checks
|
|
126
|
+
- **Payment verification cache** — 10-minute TTL, 1000-entry max (thread-safe)
|
|
127
|
+
- **Rate limiting** — 20 challenge verifications per minute per IP (thread-safe)
|
|
128
|
+
- **Input size limits** — key (64 chars), nonce (128), return URL (2048), fingerprint (128)
|
|
129
|
+
- **Wallet address validation** — base58 format, 32-44 chars, validated at init
|
|
130
|
+
- **Default secret detection** — warns in debug, raises `RuntimeError` in production
|
|
131
|
+
- **Secure cookies** — Django auto-detects HTTPS via `request.is_secure()`
|
|
132
|
+
|
|
133
|
+
## Module Structure
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
agentpayments_python/
|
|
137
|
+
__init__.py
|
|
138
|
+
django_adapter.py Django middleware (GateMiddleware)
|
|
139
|
+
fastapi_adapter.py FastAPI/Starlette ASGI middleware
|
|
140
|
+
flask_adapter.py Flask integration (before_request hook)
|
|
141
|
+
challenge.py Shared challenge HTML generation
|
|
142
|
+
cookies.py Cookie creation and validation
|
|
143
|
+
crypto.py HMAC signing and agent key management
|
|
144
|
+
detection.py Browser detection and public path checks
|
|
145
|
+
solana.py On-chain payment verification + caching
|
|
146
|
+
ratelimit.py Shared IP-based rate limiter
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Notes
|
|
150
|
+
- Constants loaded from `sdk/constants.json` via `pathlib`.
|
|
151
|
+
- Logging uses Python stdlib `logging` module.
|
|
152
|
+
- All shared modules are framework-agnostic; adapters are thin wiring.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
agentpayments_python/__init__.py,sha256=-_ZaZGRDrd1kueJ5wvc4A4P_wt6QOchSG9gLK_BKlH4,952
|
|
2
|
+
agentpayments_python/challenge.py,sha256=feWqlTcQ1LMONuwFBUOQBQOw5tsO1f7PirZZN_S3e3A,6386
|
|
3
|
+
agentpayments_python/constants.json,sha256=ZlhvktuiaziGsxedn4PugWi2RXhliK3uCWAIkAeU6Qg,979
|
|
4
|
+
agentpayments_python/cookies.py,sha256=0xO2_e4R4FpOprBcGWqY84E_eJppgJCkMGLCElZOQ90,1244
|
|
5
|
+
agentpayments_python/crawler.py,sha256=G06nF6qFmyA5nDvMW18rfh44JxxJMKc2GCO0DXiZuoY,2514
|
|
6
|
+
agentpayments_python/crypto.py,sha256=81xNcH1E468v26BimEQpK6n42bkqyz4q6eXhLLqQ6QY,1315
|
|
7
|
+
agentpayments_python/detection.py,sha256=8_RqNwF5C3TYnJkB2rO6Bs6vhBvgW4MNS3u7WTw1TCM,1292
|
|
8
|
+
agentpayments_python/django_adapter.py,sha256=aSCnzIAK9P-Ph0djssg1aYPRpGDiOgsRvJTopEJN7WQ,12516
|
|
9
|
+
agentpayments_python/fastapi_adapter.py,sha256=AUdDLEGyk9D_rqpmzAAwTHvAcxVVzawrTyrBpKg7RA0,11779
|
|
10
|
+
agentpayments_python/flask_adapter.py,sha256=r6l94yID5pmCv64mFf7mj25UwmHhtsiLQYcJQvXfkMQ,10116
|
|
11
|
+
agentpayments_python/grant_store.py,sha256=wqRsSdbpzd_TGy6a6LJRtNOMbjVBJQm7H3qpIVHEeuw,2643
|
|
12
|
+
agentpayments_python/platform_client.py,sha256=PgjqriOKxfUM0_hEdwKrWRN34M3QcvSfCsv-APMiwW0,3996
|
|
13
|
+
agentpayments_python/ratelimit.py,sha256=Q6uxFRbYbf7D8qykZ9ieVaSCU8FkXFYPS59XmkrnE_k,1580
|
|
14
|
+
agentpayments_python/solana.py,sha256=TxJndushbLEM3oJtOBl6tzY8NG3pkVsbIU3wYLmw91U,8707
|
|
15
|
+
agentpayments_python/x402.py,sha256=IAqWhjqo7rDX0Bf0_sDi4RlFE7PwwvY3WN39fjT7kjw,2775
|
|
16
|
+
agentpayments_python-0.1.0.dist-info/licenses/LICENSE,sha256=PMKDe4kanHRVI7QQezG4GeluZkMljhVO-tFSFNUiYS4,1069
|
|
17
|
+
agentpayments_python-0.1.0.dist-info/METADATA,sha256=BFzUDa3Jujc5JSM0v9xq7OQqppuEbrUpZO-CaUz_ETI,5327
|
|
18
|
+
agentpayments_python-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
19
|
+
agentpayments_python-0.1.0.dist-info/top_level.txt,sha256=DxGMGvTYQVdoBQ6pBbbsJXy5BZhbSjNGF_uSO3git4w,21
|
|
20
|
+
agentpayments_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Brzosko
|
|
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 @@
|
|
|
1
|
+
agentpayments_python
|