agentpayments-python 0.1.1__tar.gz → 0.2.0__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.
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/PKG-INFO +41 -1
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/README.md +40 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/detection.py +11 -3
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/django_adapter.py +34 -12
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/fastapi_adapter.py +31 -15
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/flask_adapter.py +28 -13
- agentpayments_python-0.2.0/agentpayments_python/grant_store.py +130 -0
- agentpayments_python-0.2.0/agentpayments_python/pricing.py +70 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/solana.py +29 -12
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/x402.py +24 -2
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python.egg-info/PKG-INFO +41 -1
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python.egg-info/SOURCES.txt +1 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/pyproject.toml +1 -1
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/tests/test_core.py +337 -5
- agentpayments_python-0.1.1/agentpayments_python/grant_store.py +0 -86
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/LICENSE +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/__init__.py +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/challenge.py +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/constants.json +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/cookies.py +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/crawler.py +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/crypto.py +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/platform_client.py +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/ratelimit.py +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python.egg-info/dependency_links.txt +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python.egg-info/requires.txt +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python.egg-info/top_level.txt +0 -0
- {agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentpayments-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: AgentPayments gate for Python web frameworks — charge AI agents USDC on Solana before they can access your API
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/adambrzosko/AgentPayments
|
|
@@ -116,6 +116,10 @@ register_agentpayments(
|
|
|
116
116
|
| `home_wallet_address` | Yes | `''` | Solana wallet address to receive USDC payments. |
|
|
117
117
|
| `solana_rpc_url` | No | Auto (devnet/mainnet) | Custom Solana RPC endpoint. |
|
|
118
118
|
| `usdc_mint` | No | Auto (devnet/mainnet) | Custom USDC mint address. |
|
|
119
|
+
| `min_payment` | No | `0.01` | Minimum USDC payment required. Ignored when `pricing_tiers` is set (the lowest tier's `min_amount` becomes the floor). |
|
|
120
|
+
| `access_duration` | No | `None` (forever) | Seconds a successful payment grants access for. Requires `grant_store` to actually persist. |
|
|
121
|
+
| `pricing_tiers` | No | `None` | Payment-amount → access mapping: `[{"min_amount": ..., "duration_seconds": ..., "name": ...}]`. Overrides `min_payment`/`access_duration`. |
|
|
122
|
+
| `routes` | No | `None` | Per-route overrides for a single middleware instance: `[{"path_prefix": ..., "min_payment": ..., "access_duration": ..., "pricing_tiers": ...}]`, matched by longest `path_prefix`. |
|
|
119
123
|
| `debug` | No | `True` | `True` = devnet. `False` = mainnet + strict mode. |
|
|
120
124
|
| `api_key` | No | `None` | AgentPayments hosted-platform API key (`ap_live_...`). When set, agent keys are issued and metered via the platform instead of self-signed locally. See **Hosted Platform Mode** below. |
|
|
121
125
|
| `platform_url` | No | AgentPayments-hosted URL | Override for a self-hosted platform API. |
|
|
@@ -126,6 +130,40 @@ Django reads these from `settings.*` (e.g., `settings.CHALLENGE_SECRET`, `settin
|
|
|
126
130
|
|
|
127
131
|
Setting `api_key` switches agent-key issuance from local (`ag_...`) to platform-issued (`agp_...`), and — when the platform account has an on-chain fee configured — every 402 response's `payment` dict gains a `platform_fee` field describing a second required USDC transfer, to be sent in the **same Solana transaction** as the vendor payment. Missing that second transfer is treated as an unpaid request, same as any other invalid payment. This is opt-in per vendor account and has no effect on self-hosted deployments (no `api_key`). The standards-compliant `accepts[]`/`X-PAYMENT-REQUIRED` x402 fields are untouched — they still describe only the vendor leg.
|
|
128
132
|
|
|
133
|
+
## Pricing & Access Model
|
|
134
|
+
|
|
135
|
+
By default one `min_payment` buys indefinite access. Three parameters layer on top, identical across Django/FastAPI/Flask (Django via `settings.AGENTPAYMENTS_*`, the others as constructor kwargs):
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
# FastAPI / Flask
|
|
139
|
+
register_agentpayments( # or AgentPaymentsASGIMiddleware(...)
|
|
140
|
+
app, ...,
|
|
141
|
+
grant_store=FileGrantStore("./data/grants.json"), # required for duration/tiers to persist
|
|
142
|
+
access_duration=86400, # seconds; a paid key is only good for 24h, then re-verifies
|
|
143
|
+
pricing_tiers=[
|
|
144
|
+
{"min_amount": 0.01, "duration_seconds": 3600, "name": "hourly"},
|
|
145
|
+
{"min_amount": 0.05, "duration_seconds": 86400, "name": "daily"},
|
|
146
|
+
{"min_amount": 0.50, "duration_seconds": None, "name": "lifetime"},
|
|
147
|
+
],
|
|
148
|
+
routes=[
|
|
149
|
+
{"path_prefix": "/api/premium", "min_payment": 0.05},
|
|
150
|
+
{"path_prefix": "/api/basic", "min_payment": 0.01},
|
|
151
|
+
],
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# Django settings.py
|
|
157
|
+
AGENTPAYMENTS_GRANT_STORE = FileGrantStore("/var/data/agp_grants.json")
|
|
158
|
+
AGENTPAYMENTS_ACCESS_DURATION = 86400
|
|
159
|
+
AGENTPAYMENTS_PRICING_TIERS = [...] # same shape as above
|
|
160
|
+
AGENTPAYMENTS_ROUTES = [...]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`pricing_tiers` overrides `min_payment`/`access_duration` when set — the gate finds the highest tier the actual on-chain payment clears and grants that tier's duration; the lowest tier's `min_amount` becomes the floor price for any access at all. Every non-floor tier also appears as its own entry in the 402 response's x402 `accepts[]` array. `routes` is matched by longest `path_prefix` on a path-segment boundary; unmatched requests fall back to the top-level `min_payment`/`access_duration`/`pricing_tiers`.
|
|
164
|
+
|
|
165
|
+
**Revocation**: `MemoryGrantStore`/`FileGrantStore` (`agentpayments_python.grant_store`) both gained a `revoke(agent_key)` method — call it from your own admin view to cut off a specific paid key early. The adapters need no changes to respect this: `has()` already returns `False` for a revoked (or expired) grant. A grants file written by an older SDK version (a plain JSON array of key strings) is still read correctly as a set of permanent grants.
|
|
166
|
+
|
|
129
167
|
## Security Features
|
|
130
168
|
|
|
131
169
|
- **Timing-safe HMAC comparison** — uses `hmac.compare_digest()` for all signature checks
|
|
@@ -150,6 +188,8 @@ agentpayments_python/
|
|
|
150
188
|
detection.py Browser detection and public path checks
|
|
151
189
|
solana.py On-chain payment verification + caching
|
|
152
190
|
ratelimit.py Shared IP-based rate limiter
|
|
191
|
+
grant_store.py Durable paid-key persistence (expiry, revocation)
|
|
192
|
+
pricing.py Pricing-tier / access-duration / per-route resolution
|
|
153
193
|
```
|
|
154
194
|
|
|
155
195
|
## Notes
|
|
@@ -79,6 +79,10 @@ register_agentpayments(
|
|
|
79
79
|
| `home_wallet_address` | Yes | `''` | Solana wallet address to receive USDC payments. |
|
|
80
80
|
| `solana_rpc_url` | No | Auto (devnet/mainnet) | Custom Solana RPC endpoint. |
|
|
81
81
|
| `usdc_mint` | No | Auto (devnet/mainnet) | Custom USDC mint address. |
|
|
82
|
+
| `min_payment` | No | `0.01` | Minimum USDC payment required. Ignored when `pricing_tiers` is set (the lowest tier's `min_amount` becomes the floor). |
|
|
83
|
+
| `access_duration` | No | `None` (forever) | Seconds a successful payment grants access for. Requires `grant_store` to actually persist. |
|
|
84
|
+
| `pricing_tiers` | No | `None` | Payment-amount → access mapping: `[{"min_amount": ..., "duration_seconds": ..., "name": ...}]`. Overrides `min_payment`/`access_duration`. |
|
|
85
|
+
| `routes` | No | `None` | Per-route overrides for a single middleware instance: `[{"path_prefix": ..., "min_payment": ..., "access_duration": ..., "pricing_tiers": ...}]`, matched by longest `path_prefix`. |
|
|
82
86
|
| `debug` | No | `True` | `True` = devnet. `False` = mainnet + strict mode. |
|
|
83
87
|
| `api_key` | No | `None` | AgentPayments hosted-platform API key (`ap_live_...`). When set, agent keys are issued and metered via the platform instead of self-signed locally. See **Hosted Platform Mode** below. |
|
|
84
88
|
| `platform_url` | No | AgentPayments-hosted URL | Override for a self-hosted platform API. |
|
|
@@ -89,6 +93,40 @@ Django reads these from `settings.*` (e.g., `settings.CHALLENGE_SECRET`, `settin
|
|
|
89
93
|
|
|
90
94
|
Setting `api_key` switches agent-key issuance from local (`ag_...`) to platform-issued (`agp_...`), and — when the platform account has an on-chain fee configured — every 402 response's `payment` dict gains a `platform_fee` field describing a second required USDC transfer, to be sent in the **same Solana transaction** as the vendor payment. Missing that second transfer is treated as an unpaid request, same as any other invalid payment. This is opt-in per vendor account and has no effect on self-hosted deployments (no `api_key`). The standards-compliant `accepts[]`/`X-PAYMENT-REQUIRED` x402 fields are untouched — they still describe only the vendor leg.
|
|
91
95
|
|
|
96
|
+
## Pricing & Access Model
|
|
97
|
+
|
|
98
|
+
By default one `min_payment` buys indefinite access. Three parameters layer on top, identical across Django/FastAPI/Flask (Django via `settings.AGENTPAYMENTS_*`, the others as constructor kwargs):
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# FastAPI / Flask
|
|
102
|
+
register_agentpayments( # or AgentPaymentsASGIMiddleware(...)
|
|
103
|
+
app, ...,
|
|
104
|
+
grant_store=FileGrantStore("./data/grants.json"), # required for duration/tiers to persist
|
|
105
|
+
access_duration=86400, # seconds; a paid key is only good for 24h, then re-verifies
|
|
106
|
+
pricing_tiers=[
|
|
107
|
+
{"min_amount": 0.01, "duration_seconds": 3600, "name": "hourly"},
|
|
108
|
+
{"min_amount": 0.05, "duration_seconds": 86400, "name": "daily"},
|
|
109
|
+
{"min_amount": 0.50, "duration_seconds": None, "name": "lifetime"},
|
|
110
|
+
],
|
|
111
|
+
routes=[
|
|
112
|
+
{"path_prefix": "/api/premium", "min_payment": 0.05},
|
|
113
|
+
{"path_prefix": "/api/basic", "min_payment": 0.01},
|
|
114
|
+
],
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
# Django settings.py
|
|
120
|
+
AGENTPAYMENTS_GRANT_STORE = FileGrantStore("/var/data/agp_grants.json")
|
|
121
|
+
AGENTPAYMENTS_ACCESS_DURATION = 86400
|
|
122
|
+
AGENTPAYMENTS_PRICING_TIERS = [...] # same shape as above
|
|
123
|
+
AGENTPAYMENTS_ROUTES = [...]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`pricing_tiers` overrides `min_payment`/`access_duration` when set — the gate finds the highest tier the actual on-chain payment clears and grants that tier's duration; the lowest tier's `min_amount` becomes the floor price for any access at all. Every non-floor tier also appears as its own entry in the 402 response's x402 `accepts[]` array. `routes` is matched by longest `path_prefix` on a path-segment boundary; unmatched requests fall back to the top-level `min_payment`/`access_duration`/`pricing_tiers`.
|
|
127
|
+
|
|
128
|
+
**Revocation**: `MemoryGrantStore`/`FileGrantStore` (`agentpayments_python.grant_store`) both gained a `revoke(agent_key)` method — call it from your own admin view to cut off a specific paid key early. The adapters need no changes to respect this: `has()` already returns `False` for a revoked (or expired) grant. A grants file written by an older SDK version (a plain JSON array of key strings) is still read correctly as a set of permanent grants.
|
|
129
|
+
|
|
92
130
|
## Security Features
|
|
93
131
|
|
|
94
132
|
- **Timing-safe HMAC comparison** — uses `hmac.compare_digest()` for all signature checks
|
|
@@ -113,6 +151,8 @@ agentpayments_python/
|
|
|
113
151
|
detection.py Browser detection and public path checks
|
|
114
152
|
solana.py On-chain payment verification + caching
|
|
115
153
|
ratelimit.py Shared IP-based rate limiter
|
|
154
|
+
grant_store.py Durable paid-key persistence (expiry, revocation)
|
|
155
|
+
pricing.py Pricing-tier / access-duration / per-route resolution
|
|
116
156
|
```
|
|
117
157
|
|
|
118
158
|
## Notes
|
|
@@ -18,10 +18,18 @@ _BOT_UA_RE = _re.compile(r"bot|crawl|spider|slurp|mediapartners|adsbot", _re.IGN
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def is_browser_from_headers(headers: dict) -> bool:
|
|
21
|
-
# Primary signal: Fetch metadata headers
|
|
22
|
-
|
|
21
|
+
# Primary signal: Fetch metadata headers, but only the values set exclusively for
|
|
22
|
+
# top-level navigations. These values are not reachable through the fetch()/XHR
|
|
23
|
+
# APIs, so this doesn't just check for *presence* of the header: Node's built-in
|
|
24
|
+
# fetch() (undici) unconditionally sends `sec-fetch-mode: cors` on every outgoing
|
|
25
|
+
# request (not overridable, it's a forbidden header), which previously
|
|
26
|
+
# misclassified fetch()-based agents as browsers and served them the HTML
|
|
27
|
+
# challenge instead of the 402 JSON they need to read to pay.
|
|
28
|
+
if headers.get("sec-fetch-mode") == "navigate" or headers.get("sec-fetch-dest") == "document":
|
|
23
29
|
return True
|
|
24
|
-
# Fallback: UA heuristic for older browsers that don't send Sec-Fetch
|
|
30
|
+
# Fallback: UA heuristic for older browsers that don't send Sec-Fetch-*, and for
|
|
31
|
+
# real in-page browser fetch()/XHR calls (Sec-Fetch-Mode: cors) which still carry
|
|
32
|
+
# a genuine browser User-Agent.
|
|
25
33
|
ua = headers.get("user-agent") or headers.get("User-Agent") or ""
|
|
26
34
|
if ua and not _BOT_UA_RE.search(ua) and _BROWSER_UA_RE.search(ua):
|
|
27
35
|
return True
|
{agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/django_adapter.py
RENAMED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import logging
|
|
4
|
+
import time as _time
|
|
2
5
|
|
|
3
6
|
from django.conf import settings
|
|
4
7
|
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
|
@@ -11,9 +14,10 @@ from .crypto import generate_agent_key, is_valid_agent_key
|
|
|
11
14
|
from .detection import is_browser_from_headers, is_public_path
|
|
12
15
|
from .ratelimit import _challenge_limiter, _agent_key_limiter, _challenge_issue_limiter
|
|
13
16
|
from .crawler import is_verified_crawler
|
|
14
|
-
from .solana import MIN_PAYMENT, RPC_DEVNET, RPC_MAINNET, USDC_MINT_DEVNET, USDC_MINT_MAINNET, is_valid_solana_address,
|
|
15
|
-
from .x402 import build_payment_object, build_payment_requirements, enrich_402_body, payment_required_header
|
|
17
|
+
from .solana import MIN_PAYMENT, RPC_DEVNET, RPC_MAINNET, USDC_MINT_DEVNET, USDC_MINT_MAINNET, is_valid_solana_address, _scan_for_payment
|
|
18
|
+
from .x402 import build_payment_object, build_payment_requirements, enrich_402_body, payment_required_header, tier_x402_opts_list
|
|
16
19
|
from .platform_client import HOSTED_KEY_PREFIX, PlatformClient, is_valid_hosted_key
|
|
20
|
+
from .pricing import normalize_price_config, build_routes_table, resolve_route_config, resolve_tier
|
|
17
21
|
|
|
18
22
|
import json as _json
|
|
19
23
|
from pathlib import Path as _Path
|
|
@@ -26,10 +30,11 @@ MAX_POW_LENGTH = _constants["MAX_POW_LENGTH"]
|
|
|
26
30
|
logger = logging.getLogger("agentpayments")
|
|
27
31
|
|
|
28
32
|
|
|
29
|
-
def _payment_required_response(body: dict, *, wallet_address: str, mint: str, min_payment: float, debug: bool, agent_key: str = "", resource: str = "") -> JsonResponse:
|
|
33
|
+
def _payment_required_response(body: dict, *, wallet_address: str, mint: str, min_payment: float, debug: bool, agent_key: str = "", resource: str = "", extra_tier_opts: list[dict] | None = None) -> JsonResponse:
|
|
30
34
|
"""Return a 402 JsonResponse enriched with x402-standard fields and header."""
|
|
31
35
|
pay_req = build_payment_requirements(wallet_address=wallet_address, mint=mint, min_payment=min_payment, debug=debug, agent_key=agent_key, resource=resource)
|
|
32
|
-
|
|
36
|
+
extra_reqs = [build_payment_requirements(**opts) for opts in (extra_tier_opts or [])]
|
|
37
|
+
resp = JsonResponse(enrich_402_body(body, pay_req, extra_reqs), status=402, json_dumps_params={"indent": 2})
|
|
33
38
|
resp["X-PAYMENT-REQUIRED"] = payment_required_header(pay_req)
|
|
34
39
|
return resp
|
|
35
40
|
|
|
@@ -69,6 +74,16 @@ class GateMiddleware:
|
|
|
69
74
|
self.rpc_url = rpc_url
|
|
70
75
|
self.usdc_mint = usdc_mint
|
|
71
76
|
self.network = "devnet" if debug else "mainnet-beta"
|
|
77
|
+
# Price/duration/tier resolution is static for this server — precompute
|
|
78
|
+
# once rather than per-request. Configure via settings.py:
|
|
79
|
+
# AGENTPAYMENTS_MIN_PAYMENT, AGENTPAYMENTS_ACCESS_DURATION,
|
|
80
|
+
# AGENTPAYMENTS_PRICING_TIERS, AGENTPAYMENTS_ROUTES
|
|
81
|
+
min_payment = getattr(settings, "AGENTPAYMENTS_MIN_PAYMENT", MIN_PAYMENT)
|
|
82
|
+
access_duration = getattr(settings, "AGENTPAYMENTS_ACCESS_DURATION", None)
|
|
83
|
+
pricing_tiers = getattr(settings, "AGENTPAYMENTS_PRICING_TIERS", None)
|
|
84
|
+
routes = getattr(settings, "AGENTPAYMENTS_ROUTES", None)
|
|
85
|
+
self.base_price_config = normalize_price_config(min_payment, access_duration, pricing_tiers)
|
|
86
|
+
self.routes_table = build_routes_table(routes, min_payment, access_duration, pricing_tiers)
|
|
72
87
|
self.pow_difficulty = getattr(settings, "POW_DIFFICULTY", POW_DIFFICULTY)
|
|
73
88
|
self.verify_crawlers = getattr(settings, "AGENTPAYMENTS_VERIFY_CRAWLERS", True)
|
|
74
89
|
# Optional grant store for durable paid-key persistence. Set
|
|
@@ -81,6 +96,7 @@ class GateMiddleware:
|
|
|
81
96
|
network = self.network
|
|
82
97
|
|
|
83
98
|
pathname = request.path
|
|
99
|
+
price_config = resolve_route_config(pathname, self.routes_table, self.base_price_config)
|
|
84
100
|
if is_public_path(pathname):
|
|
85
101
|
return self.get_response(request)
|
|
86
102
|
|
|
@@ -128,21 +144,22 @@ class GateMiddleware:
|
|
|
128
144
|
new_key = generate_agent_key(secret)
|
|
129
145
|
if fee_info:
|
|
130
146
|
no_key_instructions = (
|
|
131
|
-
f'Send {
|
|
147
|
+
f'Send {price_config["min_payment"]} USDC on Solana {network} to {wallet_address} with memo "{new_key}", '
|
|
132
148
|
f'AND in the SAME transaction send the platform fee (see platform_fee below) to {fee_info["wallet"]}. '
|
|
133
149
|
f'Then include the header X-Agent-Key: {new_key} on all subsequent requests.'
|
|
134
150
|
)
|
|
135
151
|
else:
|
|
136
|
-
no_key_instructions = f'Send {
|
|
152
|
+
no_key_instructions = f'Send {price_config["min_payment"]} USDC on Solana {network} to {wallet_address} with memo "{new_key}". Then include the header X-Agent-Key: {new_key} on all subsequent requests.'
|
|
137
153
|
return _payment_required_response(
|
|
138
154
|
{
|
|
139
155
|
"error": "payment_required",
|
|
140
156
|
"message": "Access requires a paid API key. A key has been generated for you below. Send a USDC payment on Solana with this key as the memo to activate it, then retry your request with the X-Agent-Key header.",
|
|
141
157
|
"your_key": new_key,
|
|
142
|
-
"payment": build_payment_object(network=network, min_payment=
|
|
158
|
+
"payment": build_payment_object(network=network, min_payment=price_config["min_payment"], wallet_address=wallet_address, memo=new_key, fee_info=fee_info, instructions=no_key_instructions),
|
|
143
159
|
},
|
|
144
|
-
wallet_address=wallet_address, mint=self.usdc_mint, min_payment=
|
|
160
|
+
wallet_address=wallet_address, mint=self.usdc_mint, min_payment=price_config["min_payment"],
|
|
145
161
|
debug=self.debug, agent_key=new_key, resource=pathname,
|
|
162
|
+
extra_tier_opts=tier_x402_opts_list(price_config["pricing_tiers"], {"wallet_address": wallet_address, "mint": self.usdc_mint, "debug": self.debug, "agent_key": new_key, "resource": pathname}),
|
|
146
163
|
)
|
|
147
164
|
|
|
148
165
|
# Validate the key. Platform-issued (agp_) use verificationSecret;
|
|
@@ -170,19 +187,24 @@ class GateMiddleware:
|
|
|
170
187
|
if self.grant_store and self.grant_store.has(agent_key):
|
|
171
188
|
return self.get_response(request)
|
|
172
189
|
|
|
173
|
-
|
|
190
|
+
scan_result = _scan_for_payment(agent_key, wallet_address, self.rpc_url, self.usdc_mint, min_payment=price_config["min_payment"], fee_info=fee_info)
|
|
191
|
+
paid = scan_result["paid"]
|
|
174
192
|
if paid and self.grant_store:
|
|
175
|
-
|
|
193
|
+
tier = resolve_tier(scan_result["amount_paid"], price_config["pricing_tiers"])
|
|
194
|
+
duration_seconds = tier["duration_seconds"] if tier else price_config["access_duration"]
|
|
195
|
+
expires_at = (_time.time() + duration_seconds) if duration_seconds else None
|
|
196
|
+
self.grant_store.add(agent_key, expires_at=expires_at, tier=(tier["name"] if tier else None))
|
|
176
197
|
if not paid:
|
|
177
198
|
return _payment_required_response(
|
|
178
199
|
{
|
|
179
200
|
"error": "payment_required",
|
|
180
201
|
"message": "Key is valid but payment has not been verified on-chain yet.",
|
|
181
202
|
"your_key": agent_key,
|
|
182
|
-
"payment": build_payment_object(network=network, min_payment=
|
|
203
|
+
"payment": build_payment_object(network=network, min_payment=price_config["min_payment"], wallet_address=wallet_address, memo=agent_key, fee_info=fee_info),
|
|
183
204
|
},
|
|
184
|
-
wallet_address=wallet_address, mint=self.usdc_mint, min_payment=
|
|
205
|
+
wallet_address=wallet_address, mint=self.usdc_mint, min_payment=price_config["min_payment"],
|
|
185
206
|
debug=self.debug, agent_key=agent_key, resource=pathname,
|
|
207
|
+
extra_tier_opts=tier_x402_opts_list(price_config["pricing_tiers"], {"wallet_address": wallet_address, "mint": self.usdc_mint, "debug": self.debug, "agent_key": agent_key, "resource": pathname}),
|
|
186
208
|
)
|
|
187
209
|
|
|
188
210
|
return self.get_response(request)
|
{agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/fastapi_adapter.py
RENAMED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
|
|
3
5
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
@@ -10,9 +12,11 @@ from .crypto import generate_agent_key, is_valid_agent_key
|
|
|
10
12
|
from .detection import is_browser_from_headers, is_public_path
|
|
11
13
|
from .ratelimit import _challenge_limiter, _agent_key_limiter, _challenge_issue_limiter
|
|
12
14
|
from .crawler import is_verified_crawler
|
|
13
|
-
from .solana import MIN_PAYMENT, RPC_DEVNET, RPC_MAINNET, USDC_MINT_DEVNET, USDC_MINT_MAINNET, is_valid_solana_address,
|
|
14
|
-
from .x402 import build_payment_object, build_payment_requirements, enrich_402_body, payment_required_header
|
|
15
|
+
from .solana import MIN_PAYMENT, RPC_DEVNET, RPC_MAINNET, USDC_MINT_DEVNET, USDC_MINT_MAINNET, is_valid_solana_address, _scan_for_payment
|
|
16
|
+
from .x402 import build_payment_object, build_payment_requirements, enrich_402_body, payment_required_header, tier_x402_opts_list
|
|
15
17
|
from .platform_client import HOSTED_KEY_PREFIX, PlatformClient, is_valid_hosted_key
|
|
18
|
+
from .pricing import normalize_price_config, build_routes_table, resolve_route_config, resolve_tier
|
|
19
|
+
import time as _time
|
|
16
20
|
|
|
17
21
|
import json as _json
|
|
18
22
|
from pathlib import Path as _Path
|
|
@@ -23,11 +27,12 @@ MAX_FP_LENGTH = _constants["MAX_FP_LENGTH"]
|
|
|
23
27
|
MAX_POW_LENGTH = _constants["MAX_POW_LENGTH"]
|
|
24
28
|
|
|
25
29
|
|
|
26
|
-
def _payment_required_response(body: dict, *, wallet_address: str, mint: str, min_payment: float, debug: bool, agent_key: str = "", resource: str = "") -> JSONResponse:
|
|
30
|
+
def _payment_required_response(body: dict, *, wallet_address: str, mint: str, min_payment: float, debug: bool, agent_key: str = "", resource: str = "", extra_tier_opts: list[dict] | None = None) -> JSONResponse:
|
|
27
31
|
"""Return a Starlette JSONResponse (402) enriched with x402-standard fields and header."""
|
|
28
32
|
pay_req = build_payment_requirements(wallet_address=wallet_address, mint=mint, min_payment=min_payment, debug=debug, agent_key=agent_key, resource=resource)
|
|
33
|
+
extra_reqs = [build_payment_requirements(**opts) for opts in (extra_tier_opts or [])]
|
|
29
34
|
return JSONResponse(
|
|
30
|
-
content=enrich_402_body(body, pay_req),
|
|
35
|
+
content=enrich_402_body(body, pay_req, extra_reqs),
|
|
31
36
|
status_code=402,
|
|
32
37
|
headers={"X-PAYMENT-REQUIRED": payment_required_header(pay_req)},
|
|
33
38
|
)
|
|
@@ -41,7 +46,7 @@ def _client_ip(request: Request) -> str:
|
|
|
41
46
|
|
|
42
47
|
|
|
43
48
|
class AgentPaymentsASGIMiddleware(BaseHTTPMiddleware):
|
|
44
|
-
def __init__(self, app, *, challenge_secret: str, home_wallet_address: str, debug: bool = True, solana_rpc_url=None, usdc_mint: str = "", pow_difficulty: int = POW_DIFFICULTY, verify_crawlers: bool = True, grant_store=None, require_https: bool = None, api_key: str = None, platform_url: str = None):
|
|
49
|
+
def __init__(self, app, *, challenge_secret: str, home_wallet_address: str, debug: bool = True, solana_rpc_url=None, usdc_mint: str = "", min_payment: float = MIN_PAYMENT, access_duration: float | None = None, pricing_tiers: list[dict] | None = None, routes: list[dict] | None = None, pow_difficulty: int = POW_DIFFICULTY, verify_crawlers: bool = True, grant_store=None, require_https: bool = None, api_key: str = None, platform_url: str = None):
|
|
45
50
|
super().__init__(app)
|
|
46
51
|
if challenge_secret == "default-secret-change-me":
|
|
47
52
|
import logging
|
|
@@ -58,6 +63,10 @@ class AgentPaymentsASGIMiddleware(BaseHTTPMiddleware):
|
|
|
58
63
|
raw_rpc = solana_rpc_url or (RPC_DEVNET if debug else RPC_MAINNET)
|
|
59
64
|
self.solana_rpc_url = raw_rpc if isinstance(raw_rpc, list) else [raw_rpc]
|
|
60
65
|
self.usdc_mint = usdc_mint or (USDC_MINT_DEVNET if debug else USDC_MINT_MAINNET)
|
|
66
|
+
# Price/duration/tier resolution is static for this middleware instance —
|
|
67
|
+
# precompute once rather than per-request.
|
|
68
|
+
self.base_price_config = normalize_price_config(min_payment, access_duration, pricing_tiers)
|
|
69
|
+
self.routes_table = build_routes_table(routes, min_payment, access_duration, pricing_tiers)
|
|
61
70
|
self.pow_difficulty = pow_difficulty
|
|
62
71
|
self.verify_crawlers = verify_crawlers
|
|
63
72
|
self.grant_store = grant_store
|
|
@@ -66,6 +75,7 @@ class AgentPaymentsASGIMiddleware(BaseHTTPMiddleware):
|
|
|
66
75
|
|
|
67
76
|
async def dispatch(self, request: Request, call_next):
|
|
68
77
|
path = request.url.path
|
|
78
|
+
price_config = resolve_route_config(path, self.routes_table, self.base_price_config)
|
|
69
79
|
if is_public_path(path):
|
|
70
80
|
return await call_next(request)
|
|
71
81
|
|
|
@@ -112,21 +122,22 @@ class AgentPaymentsASGIMiddleware(BaseHTTPMiddleware):
|
|
|
112
122
|
new_key = generate_agent_key(self.challenge_secret)
|
|
113
123
|
if fee_info:
|
|
114
124
|
no_key_instructions = (
|
|
115
|
-
f'Send {
|
|
125
|
+
f'Send {price_config["min_payment"]} USDC on Solana {network} to {self.home_wallet_address} with memo "{new_key}", '
|
|
116
126
|
f'AND in the SAME transaction send the platform fee (see platform_fee below) to {fee_info["wallet"]}. '
|
|
117
127
|
f'Then include the header X-Agent-Key: {new_key} on all subsequent requests.'
|
|
118
128
|
)
|
|
119
129
|
else:
|
|
120
|
-
no_key_instructions = f'Send {
|
|
130
|
+
no_key_instructions = f'Send {price_config["min_payment"]} USDC on Solana {network} to {self.home_wallet_address} with memo "{new_key}". Then include the header X-Agent-Key: {new_key} on all subsequent requests.'
|
|
121
131
|
return _payment_required_response(
|
|
122
132
|
{
|
|
123
133
|
"error": "payment_required",
|
|
124
134
|
"message": "Access requires a paid API key. A key has been generated for you below. Send a USDC payment on Solana with this key as the memo to activate it, then retry your request with the X-Agent-Key header.",
|
|
125
135
|
"your_key": new_key,
|
|
126
|
-
"payment": build_payment_object(network=network, min_payment=
|
|
136
|
+
"payment": build_payment_object(network=network, min_payment=price_config["min_payment"], wallet_address=self.home_wallet_address, memo=new_key, fee_info=fee_info, instructions=no_key_instructions),
|
|
127
137
|
},
|
|
128
|
-
wallet_address=self.home_wallet_address, mint=self.usdc_mint, min_payment=
|
|
138
|
+
wallet_address=self.home_wallet_address, mint=self.usdc_mint, min_payment=price_config["min_payment"],
|
|
129
139
|
debug=self.debug, agent_key=new_key, resource=path,
|
|
140
|
+
extra_tier_opts=tier_x402_opts_list(price_config["pricing_tiers"], {"wallet_address": self.home_wallet_address, "mint": self.usdc_mint, "debug": self.debug, "agent_key": new_key, "resource": path}),
|
|
130
141
|
)
|
|
131
142
|
|
|
132
143
|
if agent_key.startswith(HOSTED_KEY_PREFIX):
|
|
@@ -153,23 +164,28 @@ class AgentPaymentsASGIMiddleware(BaseHTTPMiddleware):
|
|
|
153
164
|
if self.grant_store and self.grant_store.has(agent_key):
|
|
154
165
|
return await call_next(request)
|
|
155
166
|
|
|
156
|
-
#
|
|
167
|
+
# _scan_for_payment is synchronous (uses requests). Run it in a
|
|
157
168
|
# thread-pool executor so it doesn't block the async event loop.
|
|
158
|
-
|
|
159
|
-
None, lambda:
|
|
169
|
+
scan_result = await loop.run_in_executor(
|
|
170
|
+
None, lambda: _scan_for_payment(agent_key, self.home_wallet_address, self.solana_rpc_url, self.usdc_mint, min_payment=price_config["min_payment"], fee_info=fee_info)
|
|
160
171
|
)
|
|
172
|
+
paid = scan_result["paid"]
|
|
161
173
|
if paid and self.grant_store:
|
|
162
|
-
|
|
174
|
+
tier = resolve_tier(scan_result["amount_paid"], price_config["pricing_tiers"])
|
|
175
|
+
duration_seconds = tier["duration_seconds"] if tier else price_config["access_duration"]
|
|
176
|
+
expires_at = (_time.time() + duration_seconds) if duration_seconds else None
|
|
177
|
+
self.grant_store.add(agent_key, expires_at=expires_at, tier=(tier["name"] if tier else None))
|
|
163
178
|
if not paid:
|
|
164
179
|
return _payment_required_response(
|
|
165
180
|
{
|
|
166
181
|
"error": "payment_required",
|
|
167
182
|
"message": "Key is valid but payment has not been verified on-chain yet.",
|
|
168
183
|
"your_key": agent_key,
|
|
169
|
-
"payment": build_payment_object(network=network, min_payment=
|
|
184
|
+
"payment": build_payment_object(network=network, min_payment=price_config["min_payment"], wallet_address=self.home_wallet_address, memo=agent_key, fee_info=fee_info),
|
|
170
185
|
},
|
|
171
|
-
wallet_address=self.home_wallet_address, mint=self.usdc_mint, min_payment=
|
|
186
|
+
wallet_address=self.home_wallet_address, mint=self.usdc_mint, min_payment=price_config["min_payment"],
|
|
172
187
|
debug=self.debug, agent_key=agent_key, resource=path,
|
|
188
|
+
extra_tier_opts=tier_x402_opts_list(price_config["pricing_tiers"], {"wallet_address": self.home_wallet_address, "mint": self.usdc_mint, "debug": self.debug, "agent_key": agent_key, "resource": path}),
|
|
173
189
|
)
|
|
174
190
|
|
|
175
191
|
return await call_next(request)
|
{agentpayments_python-0.1.1 → agentpayments_python-0.2.0}/agentpayments_python/flask_adapter.py
RENAMED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from flask import jsonify, make_response, redirect, request, Response as FlaskResponse
|
|
2
4
|
import json as _flask_json
|
|
5
|
+
import time as _time
|
|
3
6
|
|
|
4
7
|
from .challenge import POW_DIFFICULTY, challenge_html, make_nonce, validate_challenge_submission
|
|
5
8
|
from .cookies import COOKIE_MAX_AGE, COOKIE_NAME, is_valid_cookie_value, make_cookie
|
|
@@ -7,9 +10,10 @@ from .crypto import generate_agent_key, is_valid_agent_key
|
|
|
7
10
|
from .detection import is_browser_from_headers, is_public_path
|
|
8
11
|
from .ratelimit import _challenge_limiter, _agent_key_limiter, _challenge_issue_limiter
|
|
9
12
|
from .crawler import is_verified_crawler
|
|
10
|
-
from .solana import MIN_PAYMENT, RPC_DEVNET, RPC_MAINNET, USDC_MINT_DEVNET, USDC_MINT_MAINNET, is_valid_solana_address,
|
|
11
|
-
from .x402 import build_payment_object, build_payment_requirements, enrich_402_body, payment_required_header
|
|
13
|
+
from .solana import MIN_PAYMENT, RPC_DEVNET, RPC_MAINNET, USDC_MINT_DEVNET, USDC_MINT_MAINNET, is_valid_solana_address, _scan_for_payment
|
|
14
|
+
from .x402 import build_payment_object, build_payment_requirements, enrich_402_body, payment_required_header, tier_x402_opts_list
|
|
12
15
|
from .platform_client import HOSTED_KEY_PREFIX, PlatformClient, is_valid_hosted_key
|
|
16
|
+
from .pricing import normalize_price_config, build_routes_table, resolve_route_config, resolve_tier
|
|
13
17
|
|
|
14
18
|
import json as _json
|
|
15
19
|
from pathlib import Path as _Path
|
|
@@ -20,10 +24,11 @@ MAX_FP_LENGTH = _constants["MAX_FP_LENGTH"]
|
|
|
20
24
|
MAX_POW_LENGTH = _constants["MAX_POW_LENGTH"]
|
|
21
25
|
|
|
22
26
|
|
|
23
|
-
def _payment_required_flask(body: dict, *, wallet_address: str, mint: str, min_payment: float, debug: bool, agent_key: str = "", resource: str = ""):
|
|
27
|
+
def _payment_required_flask(body: dict, *, wallet_address: str, mint: str, min_payment: float, debug: bool, agent_key: str = "", resource: str = "", extra_tier_opts: list[dict] | None = None):
|
|
24
28
|
"""Return a Flask 402 response enriched with x402-standard fields and header."""
|
|
25
29
|
pay_req = build_payment_requirements(wallet_address=wallet_address, mint=mint, min_payment=min_payment, debug=debug, agent_key=agent_key, resource=resource)
|
|
26
|
-
|
|
30
|
+
extra_reqs = [build_payment_requirements(**opts) for opts in (extra_tier_opts or [])]
|
|
31
|
+
enriched = enrich_402_body(body, pay_req, extra_reqs)
|
|
27
32
|
resp = make_response(_flask_json.dumps(enriched, indent=2), 402)
|
|
28
33
|
resp.headers["Content-Type"] = "application/json"
|
|
29
34
|
resp.headers["X-PAYMENT-REQUIRED"] = payment_required_header(pay_req)
|
|
@@ -34,7 +39,7 @@ def _client_ip() -> str:
|
|
|
34
39
|
return request.headers.get("X-Forwarded-For", "").split(",")[0].strip() or request.remote_addr or "unknown"
|
|
35
40
|
|
|
36
41
|
|
|
37
|
-
def register_agentpayments(app, *, challenge_secret: str, home_wallet_address: str, debug: bool = True, solana_rpc_url=None, usdc_mint: str = "", pow_difficulty: int = POW_DIFFICULTY, verify_crawlers: bool = True, grant_store=None, require_https: bool = None, api_key: str = None, platform_url: str = None):
|
|
42
|
+
def register_agentpayments(app, *, challenge_secret: str, home_wallet_address: str, debug: bool = True, solana_rpc_url=None, usdc_mint: str = "", min_payment: float = MIN_PAYMENT, access_duration: float | None = None, pricing_tiers: list[dict] | None = None, routes: list[dict] | None = None, pow_difficulty: int = POW_DIFFICULTY, verify_crawlers: bool = True, grant_store=None, require_https: bool = None, api_key: str = None, platform_url: str = None):
|
|
38
43
|
if challenge_secret == "default-secret-change-me":
|
|
39
44
|
import logging
|
|
40
45
|
logger = logging.getLogger("agentpayments")
|
|
@@ -49,10 +54,14 @@ def register_agentpayments(app, *, challenge_secret: str, home_wallet_address: s
|
|
|
49
54
|
mint = usdc_mint or (USDC_MINT_DEVNET if debug else USDC_MINT_MAINNET)
|
|
50
55
|
_require_https = (not debug) if require_https is None else require_https
|
|
51
56
|
_platform_client = PlatformClient(api_key, platform_url) if api_key else None
|
|
57
|
+
# Price/duration/tier resolution is static for this app — precompute once.
|
|
58
|
+
base_price_config = normalize_price_config(min_payment, access_duration, pricing_tiers)
|
|
59
|
+
routes_table = build_routes_table(routes, min_payment, access_duration, pricing_tiers)
|
|
52
60
|
|
|
53
61
|
@app.before_request
|
|
54
62
|
def _gate():
|
|
55
63
|
path = request.path
|
|
64
|
+
price_config = resolve_route_config(path, routes_table, base_price_config)
|
|
56
65
|
if is_public_path(path):
|
|
57
66
|
return None
|
|
58
67
|
if path == "/__challenge/verify" and request.method == "POST":
|
|
@@ -94,21 +103,22 @@ def register_agentpayments(app, *, challenge_secret: str, home_wallet_address: s
|
|
|
94
103
|
new_key = generate_agent_key(challenge_secret)
|
|
95
104
|
if fee_info:
|
|
96
105
|
no_key_instructions = (
|
|
97
|
-
f'Send {
|
|
106
|
+
f'Send {price_config["min_payment"]} USDC on Solana {network} to {home_wallet_address} with memo "{new_key}", '
|
|
98
107
|
f'AND in the SAME transaction send the platform fee (see platform_fee below) to {fee_info["wallet"]}. '
|
|
99
108
|
f'Then include the header X-Agent-Key: {new_key} on all subsequent requests.'
|
|
100
109
|
)
|
|
101
110
|
else:
|
|
102
|
-
no_key_instructions = f'Send {
|
|
111
|
+
no_key_instructions = f'Send {price_config["min_payment"]} USDC on Solana {network} to {home_wallet_address} with memo "{new_key}". Then include the header X-Agent-Key: {new_key} on all subsequent requests.'
|
|
103
112
|
return _payment_required_flask(
|
|
104
113
|
{
|
|
105
114
|
"error": "payment_required",
|
|
106
115
|
"message": "Access requires a paid API key. A key has been generated for you below. Send a USDC payment on Solana with this key as the memo to activate it, then retry your request with the X-Agent-Key header.",
|
|
107
116
|
"your_key": new_key,
|
|
108
|
-
"payment": build_payment_object(network=network, min_payment=
|
|
117
|
+
"payment": build_payment_object(network=network, min_payment=price_config["min_payment"], wallet_address=home_wallet_address, memo=new_key, fee_info=fee_info, instructions=no_key_instructions),
|
|
109
118
|
},
|
|
110
|
-
wallet_address=home_wallet_address, mint=mint, min_payment=
|
|
119
|
+
wallet_address=home_wallet_address, mint=mint, min_payment=price_config["min_payment"],
|
|
111
120
|
debug=debug, agent_key=new_key, resource=path,
|
|
121
|
+
extra_tier_opts=tier_x402_opts_list(price_config["pricing_tiers"], {"wallet_address": home_wallet_address, "mint": mint, "debug": debug, "agent_key": new_key, "resource": path}),
|
|
112
122
|
)
|
|
113
123
|
if key.startswith(HOSTED_KEY_PREFIX):
|
|
114
124
|
if not _platform_client:
|
|
@@ -129,19 +139,24 @@ def register_agentpayments(app, *, challenge_secret: str, home_wallet_address: s
|
|
|
129
139
|
return jsonify({"error": "server_error", "message": "Payment verification unavailable."}), 500
|
|
130
140
|
if grant_store and grant_store.has(key):
|
|
131
141
|
return None
|
|
132
|
-
|
|
142
|
+
scan_result = _scan_for_payment(key, home_wallet_address, rpc_url, mint, min_payment=price_config["min_payment"], fee_info=fee_info)
|
|
143
|
+
paid = scan_result["paid"]
|
|
133
144
|
if paid and grant_store:
|
|
134
|
-
|
|
145
|
+
tier = resolve_tier(scan_result["amount_paid"], price_config["pricing_tiers"])
|
|
146
|
+
duration_seconds = tier["duration_seconds"] if tier else price_config["access_duration"]
|
|
147
|
+
expires_at = (_time.time() + duration_seconds) if duration_seconds else None
|
|
148
|
+
grant_store.add(key, expires_at=expires_at, tier=(tier["name"] if tier else None))
|
|
135
149
|
if not paid:
|
|
136
150
|
return _payment_required_flask(
|
|
137
151
|
{
|
|
138
152
|
"error": "payment_required",
|
|
139
153
|
"message": "Key is valid but payment has not been verified on-chain yet.",
|
|
140
154
|
"your_key": key,
|
|
141
|
-
"payment": build_payment_object(network=network, min_payment=
|
|
155
|
+
"payment": build_payment_object(network=network, min_payment=price_config["min_payment"], wallet_address=home_wallet_address, memo=key, fee_info=fee_info),
|
|
142
156
|
},
|
|
143
|
-
wallet_address=home_wallet_address, mint=mint, min_payment=
|
|
157
|
+
wallet_address=home_wallet_address, mint=mint, min_payment=price_config["min_payment"],
|
|
144
158
|
debug=debug, agent_key=key, resource=path,
|
|
159
|
+
extra_tier_opts=tier_x402_opts_list(price_config["pricing_tiers"], {"wallet_address": home_wallet_address, "mint": mint, "debug": debug, "agent_key": key, "resource": path}),
|
|
145
160
|
)
|
|
146
161
|
return None
|
|
147
162
|
|