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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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