agentspend 0.1.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.
- agentspend-0.1.0/.gitignore +4 -0
- agentspend-0.1.0/PKG-INFO +21 -0
- agentspend-0.1.0/README.md +3 -0
- agentspend-0.1.0/agentspend/__init__.py +23 -0
- agentspend-0.1.0/agentspend/core.py +416 -0
- agentspend-0.1.0/agentspend/django.py +153 -0
- agentspend-0.1.0/agentspend/fastapi.py +121 -0
- agentspend-0.1.0/agentspend/flask.py +90 -0
- agentspend-0.1.0/agentspend/py.typed +0 -0
- agentspend-0.1.0/agentspend/types.py +102 -0
- agentspend-0.1.0/pyproject.toml +26 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentspend
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for AgentSpend — card & crypto paywalls for AI agents
|
|
5
|
+
Project-URL: Homepage, https://agentspend.co
|
|
6
|
+
Project-URL: Repository, https://github.com/agentspend/agentspend
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: httpx>=0.25
|
|
10
|
+
Provides-Extra: django
|
|
11
|
+
Requires-Dist: django>=4.0; extra == 'django'
|
|
12
|
+
Provides-Extra: fastapi
|
|
13
|
+
Requires-Dist: fastapi>=0.100; extra == 'fastapi'
|
|
14
|
+
Requires-Dist: uvicorn>=0.20; extra == 'fastapi'
|
|
15
|
+
Provides-Extra: flask
|
|
16
|
+
Requires-Dist: flask>=2.0; extra == 'flask'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# agentspend
|
|
20
|
+
|
|
21
|
+
Python SDK for AgentSpend — card & crypto paywalls for AI agents.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from agentspend.core import AgentSpendClient
|
|
2
|
+
from agentspend.types import (
|
|
3
|
+
AgentSpendOptions,
|
|
4
|
+
ChargeOptions,
|
|
5
|
+
ChargeResponse,
|
|
6
|
+
PaywallOptions,
|
|
7
|
+
PaywallPaymentContext,
|
|
8
|
+
PaywallRequest,
|
|
9
|
+
PaywallResult,
|
|
10
|
+
AgentSpendChargeError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AgentSpendClient",
|
|
15
|
+
"AgentSpendOptions",
|
|
16
|
+
"ChargeOptions",
|
|
17
|
+
"ChargeResponse",
|
|
18
|
+
"PaywallOptions",
|
|
19
|
+
"PaywallPaymentContext",
|
|
20
|
+
"PaywallRequest",
|
|
21
|
+
"PaywallResult",
|
|
22
|
+
"AgentSpendChargeError",
|
|
23
|
+
]
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Core AgentSpend client — framework-agnostic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import uuid
|
|
9
|
+
import time
|
|
10
|
+
import random
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from agentspend.types import (
|
|
16
|
+
AgentSpendChargeError,
|
|
17
|
+
AgentSpendOptions,
|
|
18
|
+
ChargeOptions,
|
|
19
|
+
ChargeResponse,
|
|
20
|
+
PaywallOptions,
|
|
21
|
+
PaywallPaymentContext,
|
|
22
|
+
PaywallRequest,
|
|
23
|
+
PaywallResult,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DEFAULT_PLATFORM_API_BASE_URL = "https://api.agentspend.co"
|
|
27
|
+
|
|
28
|
+
USDC_BASE_ASSET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_platform_url(explicit: Optional[str]) -> str:
|
|
32
|
+
if explicit and explicit.strip():
|
|
33
|
+
return explicit.strip()
|
|
34
|
+
env = os.environ.get("AGENTSPEND_API_URL", "")
|
|
35
|
+
if env.strip():
|
|
36
|
+
return env.strip()
|
|
37
|
+
return DEFAULT_PLATFORM_API_BASE_URL
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _join_url(base: str, path: str) -> str:
|
|
41
|
+
base = base.rstrip("/")
|
|
42
|
+
path = path if path.startswith("/") else f"/{path}"
|
|
43
|
+
return f"{base}{path}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _best_effort_idempotency_key() -> str:
|
|
47
|
+
try:
|
|
48
|
+
return str(uuid.uuid4())
|
|
49
|
+
except Exception:
|
|
50
|
+
return f"auto_{int(time.time())}_{random.randint(0, 0xFFFFFFFF):08x}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _to_card_id(value: Any) -> Optional[str]:
|
|
54
|
+
if not isinstance(value, str):
|
|
55
|
+
return None
|
|
56
|
+
trimmed = value.strip()
|
|
57
|
+
return trimmed if trimmed.startswith("card_") else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _to_string_metadata(data: Any) -> dict[str, str]:
|
|
61
|
+
if not isinstance(data, dict):
|
|
62
|
+
return {}
|
|
63
|
+
result: dict[str, str] = {}
|
|
64
|
+
for key, value in data.items():
|
|
65
|
+
if isinstance(value, str):
|
|
66
|
+
result[key] = value
|
|
67
|
+
elif isinstance(value, (int, float)):
|
|
68
|
+
result[key] = str(value)
|
|
69
|
+
elif isinstance(value, bool):
|
|
70
|
+
result[key] = "true" if value else "false"
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _resolve_amount(amount: int | str | Any, body: Any) -> int:
|
|
75
|
+
if isinstance(amount, int):
|
|
76
|
+
return amount
|
|
77
|
+
if isinstance(amount, str):
|
|
78
|
+
if isinstance(body, dict):
|
|
79
|
+
raw = body.get(amount)
|
|
80
|
+
return raw if isinstance(raw, int) else 0
|
|
81
|
+
return 0
|
|
82
|
+
# callable
|
|
83
|
+
return amount(body)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _extract_card_id(headers: dict[str, Optional[str]], body: Any) -> Optional[str]:
|
|
87
|
+
card_id = _to_card_id(headers.get("x-card-id"))
|
|
88
|
+
if not card_id and isinstance(body, dict):
|
|
89
|
+
card_id = _to_card_id(body.get("card_id"))
|
|
90
|
+
return card_id
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _extract_payment_header(headers: dict[str, Optional[str]]) -> Optional[str]:
|
|
94
|
+
return headers.get("payment-signature") or headers.get("x-payment") or None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AgentSpendClient:
|
|
98
|
+
"""Framework-agnostic AgentSpend client.
|
|
99
|
+
|
|
100
|
+
Usage::
|
|
101
|
+
|
|
102
|
+
client = AgentSpendClient(AgentSpendOptions(service_api_key="sk_..."))
|
|
103
|
+
result = client.charge("card_...", ChargeOptions(amount_cents=500))
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, options: AgentSpendOptions):
|
|
107
|
+
if not options.service_api_key and not options.crypto:
|
|
108
|
+
raise AgentSpendChargeError(
|
|
109
|
+
"At least one of service_api_key or crypto config must be provided",
|
|
110
|
+
500,
|
|
111
|
+
)
|
|
112
|
+
self._options = options
|
|
113
|
+
self._base_url = _resolve_platform_url(options.platform_api_base_url)
|
|
114
|
+
self._cached_service_id: Optional[str] = None
|
|
115
|
+
self._network = options.crypto.network if options.crypto else "eip155:8453"
|
|
116
|
+
self._http = httpx.Client(timeout=30)
|
|
117
|
+
|
|
118
|
+
# -----------------------------------------------------------------
|
|
119
|
+
# charge()
|
|
120
|
+
# -----------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def charge(self, card_id_input: str, opts: ChargeOptions) -> ChargeResponse:
|
|
123
|
+
if not self._options.service_api_key:
|
|
124
|
+
raise AgentSpendChargeError("charge() requires service_api_key", 500)
|
|
125
|
+
|
|
126
|
+
card_id = _to_card_id(card_id_input)
|
|
127
|
+
if not card_id:
|
|
128
|
+
raise AgentSpendChargeError("card_id must start with card_", 400)
|
|
129
|
+
if not isinstance(opts.amount_cents, int) or opts.amount_cents <= 0:
|
|
130
|
+
raise AgentSpendChargeError("amount_cents must be a positive integer", 400)
|
|
131
|
+
|
|
132
|
+
payload: dict[str, Any] = {
|
|
133
|
+
"card_id": card_id,
|
|
134
|
+
"amount_cents": opts.amount_cents,
|
|
135
|
+
"currency": opts.currency,
|
|
136
|
+
"idempotency_key": opts.idempotency_key or _best_effort_idempotency_key(),
|
|
137
|
+
}
|
|
138
|
+
if opts.description:
|
|
139
|
+
payload["description"] = opts.description
|
|
140
|
+
if opts.metadata:
|
|
141
|
+
payload["metadata"] = opts.metadata
|
|
142
|
+
|
|
143
|
+
resp = self._http.post(
|
|
144
|
+
_join_url(self._base_url, "/v1/charge"),
|
|
145
|
+
json=payload,
|
|
146
|
+
headers={
|
|
147
|
+
"authorization": f"Bearer {self._options.service_api_key}",
|
|
148
|
+
"content-type": "application/json",
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
body = resp.json()
|
|
153
|
+
if resp.status_code >= 400:
|
|
154
|
+
msg = body.get("error", "AgentSpend charge failed") if isinstance(body, dict) else "AgentSpend charge failed"
|
|
155
|
+
raise AgentSpendChargeError(msg, resp.status_code, body)
|
|
156
|
+
|
|
157
|
+
return ChargeResponse(
|
|
158
|
+
charged=body.get("charged", True),
|
|
159
|
+
card_id=body.get("card_id", card_id),
|
|
160
|
+
amount_cents=body.get("amount_cents", opts.amount_cents),
|
|
161
|
+
currency=body.get("currency", opts.currency),
|
|
162
|
+
remaining_limit_cents=body.get("remaining_limit_cents", 0),
|
|
163
|
+
stripe_payment_intent_id=body.get("stripe_payment_intent_id", ""),
|
|
164
|
+
stripe_charge_id=body.get("stripe_charge_id", ""),
|
|
165
|
+
charge_attempt_id=body.get("charge_attempt_id", ""),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# -----------------------------------------------------------------
|
|
169
|
+
# process_paywall()
|
|
170
|
+
# -----------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def process_paywall(self, opts: PaywallOptions, request: PaywallRequest) -> PaywallResult:
|
|
173
|
+
if isinstance(opts.amount, int):
|
|
174
|
+
if opts.amount <= 0:
|
|
175
|
+
raise AgentSpendChargeError("amount must be a positive integer", 500)
|
|
176
|
+
|
|
177
|
+
body = request.body
|
|
178
|
+
effective_amount = _resolve_amount(opts.amount, body)
|
|
179
|
+
|
|
180
|
+
if not isinstance(effective_amount, int) or effective_amount <= 0:
|
|
181
|
+
return PaywallResult(
|
|
182
|
+
outcome="error",
|
|
183
|
+
status_code=400,
|
|
184
|
+
body={"error": "Could not determine payment amount from request"},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
currency = opts.currency
|
|
188
|
+
|
|
189
|
+
# Crypto payment
|
|
190
|
+
payment_header = _extract_payment_header(request.headers)
|
|
191
|
+
if payment_header:
|
|
192
|
+
return self._handle_crypto_payment(payment_header, effective_amount, currency)
|
|
193
|
+
|
|
194
|
+
# Card payment
|
|
195
|
+
card_id = _extract_card_id(request.headers, body)
|
|
196
|
+
if card_id:
|
|
197
|
+
return self._handle_card_payment(request, card_id, effective_amount, currency, body, opts)
|
|
198
|
+
|
|
199
|
+
# 402
|
|
200
|
+
return self._build_402_result(request.url, effective_amount, currency)
|
|
201
|
+
|
|
202
|
+
# -----------------------------------------------------------------
|
|
203
|
+
# Internal
|
|
204
|
+
# -----------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
def _get_service_id(self) -> Optional[str]:
|
|
207
|
+
if self._cached_service_id:
|
|
208
|
+
return self._cached_service_id
|
|
209
|
+
if not self._options.service_api_key:
|
|
210
|
+
return None
|
|
211
|
+
try:
|
|
212
|
+
resp = self._http.get(
|
|
213
|
+
_join_url(self._base_url, "/v1/service/me"),
|
|
214
|
+
headers={"authorization": f"Bearer {self._options.service_api_key}"},
|
|
215
|
+
)
|
|
216
|
+
if resp.status_code < 400:
|
|
217
|
+
data = resp.json()
|
|
218
|
+
self._cached_service_id = data.get("id")
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
return self._cached_service_id
|
|
222
|
+
|
|
223
|
+
def _resolve_pay_to_address(self) -> str:
|
|
224
|
+
if self._options.crypto and self._options.crypto.receiver_address:
|
|
225
|
+
return self._options.crypto.receiver_address
|
|
226
|
+
|
|
227
|
+
if self._options.service_api_key:
|
|
228
|
+
resp = self._http.post(
|
|
229
|
+
_join_url(self._base_url, "/v1/crypto/deposit-address"),
|
|
230
|
+
json={"amount_cents": 0, "currency": "usd"},
|
|
231
|
+
headers={
|
|
232
|
+
"authorization": f"Bearer {self._options.service_api_key}",
|
|
233
|
+
"content-type": "application/json",
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
if resp.status_code >= 400:
|
|
237
|
+
raise AgentSpendChargeError("Failed to resolve crypto deposit address", 502)
|
|
238
|
+
data = resp.json()
|
|
239
|
+
addr = data.get("deposit_address")
|
|
240
|
+
if not addr:
|
|
241
|
+
raise AgentSpendChargeError("No deposit address returned", 502)
|
|
242
|
+
return addr
|
|
243
|
+
|
|
244
|
+
raise AgentSpendChargeError("No crypto payTo address available", 500)
|
|
245
|
+
|
|
246
|
+
def _build_402_result(self, request_url: str, amount_cents: int, currency: str) -> PaywallResult:
|
|
247
|
+
service_id = self._get_service_id()
|
|
248
|
+
|
|
249
|
+
agentspend_block: dict[str, Any] = {}
|
|
250
|
+
if service_id:
|
|
251
|
+
agentspend_block = {"agentspend": {"service_id": service_id, "amount_cents": amount_cents}}
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
pay_to = self._resolve_pay_to_address()
|
|
255
|
+
payment_requirements = {
|
|
256
|
+
"scheme": "exact",
|
|
257
|
+
"network": self._network,
|
|
258
|
+
"amount": str(amount_cents),
|
|
259
|
+
"asset": USDC_BASE_ASSET,
|
|
260
|
+
"payTo": pay_to,
|
|
261
|
+
"maxTimeoutSeconds": 300,
|
|
262
|
+
"extra": {"name": "USD Coin", "version": "2"},
|
|
263
|
+
}
|
|
264
|
+
payment_required = {
|
|
265
|
+
"x402Version": 2,
|
|
266
|
+
"error": "Payment required",
|
|
267
|
+
"resource": {
|
|
268
|
+
"url": request_url,
|
|
269
|
+
"description": f"Payment of {amount_cents} cents",
|
|
270
|
+
"mimeType": "application/json",
|
|
271
|
+
},
|
|
272
|
+
"accepts": [payment_requirements],
|
|
273
|
+
}
|
|
274
|
+
header_value = base64.b64encode(json.dumps(payment_required).encode()).decode()
|
|
275
|
+
|
|
276
|
+
return PaywallResult(
|
|
277
|
+
outcome="payment_required",
|
|
278
|
+
status_code=402,
|
|
279
|
+
body={"error": "Payment required", "amount_cents": amount_cents, "currency": currency, **agentspend_block},
|
|
280
|
+
headers={"Payment-Required": header_value},
|
|
281
|
+
)
|
|
282
|
+
except Exception as exc:
|
|
283
|
+
print(f"[agentspend] Failed to resolve crypto payTo address — returning card-only 402: {exc}")
|
|
284
|
+
return PaywallResult(
|
|
285
|
+
outcome="payment_required",
|
|
286
|
+
status_code=402,
|
|
287
|
+
body={"error": "Payment required", "amount_cents": amount_cents, "currency": currency, **agentspend_block},
|
|
288
|
+
headers={},
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def _handle_card_payment(
|
|
292
|
+
self,
|
|
293
|
+
request: PaywallRequest,
|
|
294
|
+
card_id: str,
|
|
295
|
+
amount_cents: int,
|
|
296
|
+
currency: str,
|
|
297
|
+
body: Any,
|
|
298
|
+
opts: PaywallOptions,
|
|
299
|
+
) -> PaywallResult:
|
|
300
|
+
if not self._options.service_api_key:
|
|
301
|
+
return PaywallResult(outcome="error", status_code=500, body={"error": "Card payments require service_api_key"})
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
charge_result = self.charge(
|
|
305
|
+
card_id,
|
|
306
|
+
ChargeOptions(
|
|
307
|
+
amount_cents=amount_cents,
|
|
308
|
+
currency=currency,
|
|
309
|
+
description=opts.description,
|
|
310
|
+
metadata=_to_string_metadata(opts.metadata(body)) if opts.metadata else None,
|
|
311
|
+
idempotency_key=request.headers.get("x-request-id") or request.headers.get("idempotency-key"),
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
return PaywallResult(
|
|
315
|
+
outcome="charged",
|
|
316
|
+
payment_context=PaywallPaymentContext(
|
|
317
|
+
method="card",
|
|
318
|
+
amount_cents=amount_cents,
|
|
319
|
+
currency=currency,
|
|
320
|
+
card_id=card_id,
|
|
321
|
+
remaining_limit_cents=charge_result.remaining_limit_cents,
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
except AgentSpendChargeError as exc:
|
|
325
|
+
if exc.status_code == 403:
|
|
326
|
+
return self._build_402_result(request.url, amount_cents, currency)
|
|
327
|
+
if exc.status_code == 402:
|
|
328
|
+
return PaywallResult(outcome="error", status_code=402, body={"error": "Payment required", "details": exc.details})
|
|
329
|
+
return PaywallResult(outcome="error", status_code=exc.status_code, body={"error": str(exc), "details": exc.details})
|
|
330
|
+
except Exception:
|
|
331
|
+
return PaywallResult(outcome="error", status_code=500, body={"error": "Unexpected paywall failure"})
|
|
332
|
+
|
|
333
|
+
def _handle_crypto_payment(
|
|
334
|
+
self,
|
|
335
|
+
payment_header: str,
|
|
336
|
+
amount_cents: int,
|
|
337
|
+
currency: str,
|
|
338
|
+
) -> PaywallResult:
|
|
339
|
+
try:
|
|
340
|
+
payload_json = base64.b64decode(payment_header).decode("utf-8")
|
|
341
|
+
payment_payload = json.loads(payload_json)
|
|
342
|
+
except Exception:
|
|
343
|
+
return PaywallResult(outcome="error", status_code=400, body={"error": "Invalid payment payload encoding"})
|
|
344
|
+
|
|
345
|
+
accepted_pay_to = None
|
|
346
|
+
if isinstance(payment_payload, dict):
|
|
347
|
+
accepted = payment_payload.get("accepted", {})
|
|
348
|
+
if isinstance(accepted, dict):
|
|
349
|
+
accepted_pay_to = accepted.get("payTo")
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
pay_to = accepted_pay_to or self._resolve_pay_to_address()
|
|
353
|
+
except AgentSpendChargeError as exc:
|
|
354
|
+
return PaywallResult(outcome="error", status_code=exc.status_code, body={"error": exc.args[0], "details": exc.details})
|
|
355
|
+
|
|
356
|
+
# Note: Full x402 verification requires the @x402 libraries.
|
|
357
|
+
# For Python, crypto verification should be implemented via HTTP calls
|
|
358
|
+
# to the facilitator endpoint. This is a placeholder that demonstrates
|
|
359
|
+
# the protocol flow — production use requires facilitator integration.
|
|
360
|
+
payment_requirements = {
|
|
361
|
+
"scheme": "exact",
|
|
362
|
+
"network": self._network,
|
|
363
|
+
"amount": str(amount_cents),
|
|
364
|
+
"asset": USDC_BASE_ASSET,
|
|
365
|
+
"payTo": pay_to,
|
|
366
|
+
"maxTimeoutSeconds": 300,
|
|
367
|
+
"extra": {"name": "USD Coin", "version": "2"},
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
facilitator_url = "https://facilitator.openx402.ai"
|
|
371
|
+
if self._options.crypto and self._options.crypto.facilitator_url:
|
|
372
|
+
facilitator_url = self._options.crypto.facilitator_url
|
|
373
|
+
|
|
374
|
+
# Verify via facilitator
|
|
375
|
+
try:
|
|
376
|
+
verify_resp = self._http.post(
|
|
377
|
+
f"{facilitator_url.rstrip('/')}/verify",
|
|
378
|
+
json={"payload": payment_payload, "requirements": payment_requirements},
|
|
379
|
+
)
|
|
380
|
+
verify_data = verify_resp.json() if verify_resp.status_code < 500 else {}
|
|
381
|
+
if not verify_data.get("isValid"):
|
|
382
|
+
return PaywallResult(
|
|
383
|
+
outcome="error",
|
|
384
|
+
status_code=402,
|
|
385
|
+
body={"error": "Payment verification failed", "details": verify_data.get("invalidReason")},
|
|
386
|
+
)
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
return PaywallResult(outcome="error", status_code=500, body={"error": "Crypto payment processing failed", "details": str(exc)})
|
|
389
|
+
|
|
390
|
+
# Settle via facilitator
|
|
391
|
+
try:
|
|
392
|
+
settle_resp = self._http.post(
|
|
393
|
+
f"{facilitator_url.rstrip('/')}/settle",
|
|
394
|
+
json={"payload": payment_payload, "requirements": payment_requirements},
|
|
395
|
+
)
|
|
396
|
+
settle_data = settle_resp.json() if settle_resp.status_code < 500 else {}
|
|
397
|
+
if not settle_data.get("success"):
|
|
398
|
+
return PaywallResult(
|
|
399
|
+
outcome="error",
|
|
400
|
+
status_code=402,
|
|
401
|
+
body={"error": "Payment settlement failed", "details": settle_data.get("errorReason")},
|
|
402
|
+
)
|
|
403
|
+
except Exception as exc:
|
|
404
|
+
return PaywallResult(outcome="error", status_code=500, body={"error": "Crypto payment processing failed", "details": str(exc)})
|
|
405
|
+
|
|
406
|
+
return PaywallResult(
|
|
407
|
+
outcome="crypto_paid",
|
|
408
|
+
payment_context=PaywallPaymentContext(
|
|
409
|
+
method="crypto",
|
|
410
|
+
amount_cents=amount_cents,
|
|
411
|
+
currency=currency,
|
|
412
|
+
transaction_hash=settle_data.get("transaction"),
|
|
413
|
+
payer_address=verify_data.get("payer"),
|
|
414
|
+
network=self._network,
|
|
415
|
+
),
|
|
416
|
+
)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Django integration for AgentSpend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from django.http import HttpRequest, JsonResponse
|
|
9
|
+
|
|
10
|
+
from agentspend.core import AgentSpendClient
|
|
11
|
+
from agentspend.types import (
|
|
12
|
+
AgentSpendOptions,
|
|
13
|
+
ChargeOptions,
|
|
14
|
+
ChargeResponse,
|
|
15
|
+
PaywallOptions,
|
|
16
|
+
PaywallPaymentContext,
|
|
17
|
+
PaywallRequest,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_PAYMENT_CONTEXT_ATTR = "_agentspend_payment_context"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_payment_context(request: HttpRequest) -> PaywallPaymentContext | None:
|
|
24
|
+
"""Retrieve the payment context set by the paywall middleware."""
|
|
25
|
+
return getattr(request, _PAYMENT_CONTEXT_ATTR, None)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AgentSpendMiddleware:
|
|
29
|
+
"""Django middleware for AgentSpend paywalls.
|
|
30
|
+
|
|
31
|
+
Configure in settings.py::
|
|
32
|
+
|
|
33
|
+
AGENTSPEND_OPTIONS = AgentSpendOptions(service_api_key="sk_...")
|
|
34
|
+
AGENTSPEND_PAYWALL_ROUTES = {
|
|
35
|
+
"/paid/": PaywallOptions(amount=500),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Then use the ``agentspend_paywall`` decorator for per-view control,
|
|
39
|
+
or this middleware for route-based configuration.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, get_response: Callable[[HttpRequest], Any]):
|
|
43
|
+
self.get_response = get_response
|
|
44
|
+
# Import from Django settings
|
|
45
|
+
from django.conf import settings
|
|
46
|
+
|
|
47
|
+
options: AgentSpendOptions = getattr(settings, "AGENTSPEND_OPTIONS", None)
|
|
48
|
+
if options is None:
|
|
49
|
+
raise ValueError("AGENTSPEND_OPTIONS must be set in Django settings")
|
|
50
|
+
|
|
51
|
+
self._client = AgentSpendClient(options)
|
|
52
|
+
self._routes: dict[str, PaywallOptions] = getattr(settings, "AGENTSPEND_PAYWALL_ROUTES", {})
|
|
53
|
+
|
|
54
|
+
def __call__(self, request: HttpRequest) -> Any:
|
|
55
|
+
opts = self._routes.get(request.path)
|
|
56
|
+
if opts is None:
|
|
57
|
+
return self.get_response(request)
|
|
58
|
+
|
|
59
|
+
result = self._process(request, opts)
|
|
60
|
+
if result is not None:
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
return self.get_response(request)
|
|
64
|
+
|
|
65
|
+
def _process(self, request: HttpRequest, opts: PaywallOptions) -> Optional[JsonResponse]:
|
|
66
|
+
try:
|
|
67
|
+
body = json.loads(request.body) if request.body else {}
|
|
68
|
+
except (json.JSONDecodeError, ValueError):
|
|
69
|
+
body = {}
|
|
70
|
+
|
|
71
|
+
headers = {
|
|
72
|
+
"x-card-id": request.headers.get("x-card-id"),
|
|
73
|
+
"payment-signature": request.headers.get("payment-signature"),
|
|
74
|
+
"x-payment": request.headers.get("x-payment"),
|
|
75
|
+
"x-request-id": request.headers.get("x-request-id"),
|
|
76
|
+
"idempotency-key": request.headers.get("idempotency-key"),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
paywall_req = PaywallRequest(
|
|
80
|
+
url=request.build_absolute_uri(),
|
|
81
|
+
method=request.method,
|
|
82
|
+
headers=headers,
|
|
83
|
+
body=body,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
result = self._client.process_paywall(opts, paywall_req)
|
|
87
|
+
|
|
88
|
+
if result.outcome in ("charged", "crypto_paid"):
|
|
89
|
+
setattr(request, _PAYMENT_CONTEXT_ATTR, result.payment_context)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
if result.outcome == "payment_required":
|
|
93
|
+
resp = JsonResponse(result.body, status=result.status_code or 402)
|
|
94
|
+
for key, value in result.headers.items():
|
|
95
|
+
resp[key] = value
|
|
96
|
+
return resp
|
|
97
|
+
|
|
98
|
+
return JsonResponse(result.body, status=result.status_code or 500)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def agentspend_paywall(options: AgentSpendOptions, paywall_opts: PaywallOptions) -> Callable[..., Any]:
|
|
102
|
+
"""Decorator for individual Django views.
|
|
103
|
+
|
|
104
|
+
Usage::
|
|
105
|
+
|
|
106
|
+
from agentspend.django import agentspend_paywall, get_payment_context
|
|
107
|
+
|
|
108
|
+
@agentspend_paywall(AgentSpendOptions(service_api_key="sk_..."), PaywallOptions(amount=500))
|
|
109
|
+
def paid_view(request):
|
|
110
|
+
ctx = get_payment_context(request)
|
|
111
|
+
return JsonResponse({"paid": True, "method": ctx.method})
|
|
112
|
+
"""
|
|
113
|
+
client = AgentSpendClient(options)
|
|
114
|
+
|
|
115
|
+
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
116
|
+
def wrapper(request: HttpRequest, *args: Any, **kwargs: Any) -> Any:
|
|
117
|
+
try:
|
|
118
|
+
body = json.loads(request.body) if request.body else {}
|
|
119
|
+
except (json.JSONDecodeError, ValueError):
|
|
120
|
+
body = {}
|
|
121
|
+
|
|
122
|
+
headers = {
|
|
123
|
+
"x-card-id": request.headers.get("x-card-id"),
|
|
124
|
+
"payment-signature": request.headers.get("payment-signature"),
|
|
125
|
+
"x-payment": request.headers.get("x-payment"),
|
|
126
|
+
"x-request-id": request.headers.get("x-request-id"),
|
|
127
|
+
"idempotency-key": request.headers.get("idempotency-key"),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
paywall_req = PaywallRequest(
|
|
131
|
+
url=request.build_absolute_uri(),
|
|
132
|
+
method=request.method,
|
|
133
|
+
headers=headers,
|
|
134
|
+
body=body,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
result = client.process_paywall(paywall_opts, paywall_req)
|
|
138
|
+
|
|
139
|
+
if result.outcome in ("charged", "crypto_paid"):
|
|
140
|
+
setattr(request, _PAYMENT_CONTEXT_ATTR, result.payment_context)
|
|
141
|
+
return fn(request, *args, **kwargs)
|
|
142
|
+
|
|
143
|
+
if result.outcome == "payment_required":
|
|
144
|
+
resp = JsonResponse(result.body, status=result.status_code or 402)
|
|
145
|
+
for key, value in result.headers.items():
|
|
146
|
+
resp[key] = value
|
|
147
|
+
return resp
|
|
148
|
+
|
|
149
|
+
return JsonResponse(result.body, status=result.status_code or 500)
|
|
150
|
+
|
|
151
|
+
return wrapper
|
|
152
|
+
|
|
153
|
+
return decorator
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""FastAPI integration for AgentSpend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import Depends, Request
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
|
|
10
|
+
from agentspend.core import AgentSpendClient
|
|
11
|
+
from agentspend.types import (
|
|
12
|
+
AgentSpendOptions,
|
|
13
|
+
ChargeOptions,
|
|
14
|
+
ChargeResponse,
|
|
15
|
+
PaywallOptions,
|
|
16
|
+
PaywallPaymentContext,
|
|
17
|
+
PaywallRequest,
|
|
18
|
+
PaywallResult,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_PAYMENT_CONTEXT_ATTR = "_agentspend_payment_context"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_payment_context(request: Request) -> PaywallPaymentContext | None:
|
|
25
|
+
"""Retrieve the payment context set by the paywall dependency."""
|
|
26
|
+
return getattr(request.state, _PAYMENT_CONTEXT_ATTR, None)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AgentSpend:
|
|
30
|
+
"""FastAPI AgentSpend integration.
|
|
31
|
+
|
|
32
|
+
Usage::
|
|
33
|
+
|
|
34
|
+
from agentspend.fastapi import AgentSpend, get_payment_context
|
|
35
|
+
|
|
36
|
+
spend = AgentSpend(AgentSpendOptions(service_api_key="sk_..."))
|
|
37
|
+
|
|
38
|
+
@app.post("/paid", dependencies=[Depends(spend.paywall(PaywallOptions(amount=500)))])
|
|
39
|
+
async def paid_endpoint(request: Request):
|
|
40
|
+
ctx = get_payment_context(request)
|
|
41
|
+
return {"paid": True, "method": ctx.method}
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, options: AgentSpendOptions):
|
|
45
|
+
self._client = AgentSpendClient(options)
|
|
46
|
+
|
|
47
|
+
def charge(self, card_id: str, opts: ChargeOptions) -> ChargeResponse:
|
|
48
|
+
return self._client.charge(card_id, opts)
|
|
49
|
+
|
|
50
|
+
def paywall(self, opts: PaywallOptions):
|
|
51
|
+
"""Return a FastAPI dependency that gates a route behind a paywall."""
|
|
52
|
+
client = self._client
|
|
53
|
+
|
|
54
|
+
async def paywall_dependency(request: Request) -> Optional[PaywallPaymentContext]:
|
|
55
|
+
body: Any
|
|
56
|
+
try:
|
|
57
|
+
body = await request.json()
|
|
58
|
+
except Exception:
|
|
59
|
+
body = {}
|
|
60
|
+
|
|
61
|
+
headers = {
|
|
62
|
+
"x-card-id": request.headers.get("x-card-id"),
|
|
63
|
+
"payment-signature": request.headers.get("payment-signature"),
|
|
64
|
+
"x-payment": request.headers.get("x-payment"),
|
|
65
|
+
"x-request-id": request.headers.get("x-request-id"),
|
|
66
|
+
"idempotency-key": request.headers.get("idempotency-key"),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
paywall_req = PaywallRequest(
|
|
70
|
+
url=str(request.url),
|
|
71
|
+
method=request.method,
|
|
72
|
+
headers=headers,
|
|
73
|
+
body=body,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
result = client.process_paywall(opts, paywall_req)
|
|
77
|
+
|
|
78
|
+
if result.outcome in ("charged", "crypto_paid"):
|
|
79
|
+
setattr(request.state, _PAYMENT_CONTEXT_ATTR, result.payment_context)
|
|
80
|
+
return result.payment_context
|
|
81
|
+
|
|
82
|
+
if result.outcome == "payment_required":
|
|
83
|
+
raise _PaywallResponseException(
|
|
84
|
+
status_code=result.status_code or 402,
|
|
85
|
+
body=result.body or {},
|
|
86
|
+
headers=result.headers,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
raise _PaywallResponseException(
|
|
90
|
+
status_code=result.status_code or 500,
|
|
91
|
+
body=result.body or {},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return paywall_dependency
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _PaywallResponseException(Exception):
|
|
98
|
+
"""Internal exception to short-circuit FastAPI request processing."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, status_code: int, body: dict[str, Any], headers: dict[str, str] | None = None):
|
|
101
|
+
self.status_code = status_code
|
|
102
|
+
self.body = body
|
|
103
|
+
self.headers = headers or {}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def install_exception_handler(app: Any) -> None:
|
|
107
|
+
"""Install the AgentSpend exception handler on a FastAPI app.
|
|
108
|
+
|
|
109
|
+
Call this once during app startup::
|
|
110
|
+
|
|
111
|
+
from agentspend.fastapi import install_exception_handler
|
|
112
|
+
install_exception_handler(app)
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
@app.exception_handler(_PaywallResponseException)
|
|
116
|
+
async def _handle_paywall_exception(request: Request, exc: _PaywallResponseException) -> JSONResponse:
|
|
117
|
+
return JSONResponse(
|
|
118
|
+
status_code=exc.status_code,
|
|
119
|
+
content=exc.body,
|
|
120
|
+
headers=exc.headers,
|
|
121
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Flask integration for AgentSpend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from flask import Response as FlaskResponse, g, jsonify, request
|
|
9
|
+
|
|
10
|
+
from agentspend.core import AgentSpendClient
|
|
11
|
+
from agentspend.types import (
|
|
12
|
+
AgentSpendOptions,
|
|
13
|
+
ChargeOptions,
|
|
14
|
+
ChargeResponse,
|
|
15
|
+
PaywallOptions,
|
|
16
|
+
PaywallPaymentContext,
|
|
17
|
+
PaywallRequest,
|
|
18
|
+
PaywallResult,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_payment_context() -> PaywallPaymentContext | None:
|
|
23
|
+
"""Retrieve the payment context set by the paywall decorator."""
|
|
24
|
+
return getattr(g, "payment_context", None)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentSpend:
|
|
28
|
+
"""Flask AgentSpend integration.
|
|
29
|
+
|
|
30
|
+
Usage::
|
|
31
|
+
|
|
32
|
+
from agentspend.flask import AgentSpend
|
|
33
|
+
|
|
34
|
+
spend = AgentSpend(AgentSpendOptions(service_api_key="sk_..."))
|
|
35
|
+
|
|
36
|
+
@app.route("/paid", methods=["POST"])
|
|
37
|
+
@spend.paywall(PaywallOptions(amount=500))
|
|
38
|
+
def paid_endpoint():
|
|
39
|
+
ctx = get_payment_context()
|
|
40
|
+
return jsonify({"paid": True, "method": ctx.method})
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, options: AgentSpendOptions):
|
|
44
|
+
self._client = AgentSpendClient(options)
|
|
45
|
+
|
|
46
|
+
def charge(self, card_id: str, opts: ChargeOptions) -> ChargeResponse:
|
|
47
|
+
return self._client.charge(card_id, opts)
|
|
48
|
+
|
|
49
|
+
def paywall(self, opts: PaywallOptions) -> Callable[..., Any]:
|
|
50
|
+
"""Decorator that gates a Flask route behind a paywall."""
|
|
51
|
+
|
|
52
|
+
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
53
|
+
@wraps(fn)
|
|
54
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
55
|
+
headers = {
|
|
56
|
+
"x-card-id": request.headers.get("x-card-id"),
|
|
57
|
+
"payment-signature": request.headers.get("payment-signature"),
|
|
58
|
+
"x-payment": request.headers.get("x-payment"),
|
|
59
|
+
"x-request-id": request.headers.get("x-request-id"),
|
|
60
|
+
"idempotency-key": request.headers.get("idempotency-key"),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
body = request.get_json(silent=True) or {}
|
|
64
|
+
|
|
65
|
+
paywall_req = PaywallRequest(
|
|
66
|
+
url=request.url,
|
|
67
|
+
method=request.method,
|
|
68
|
+
headers=headers,
|
|
69
|
+
body=body,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
result = self._client.process_paywall(opts, paywall_req)
|
|
73
|
+
|
|
74
|
+
if result.outcome in ("charged", "crypto_paid"):
|
|
75
|
+
g.payment_context = result.payment_context
|
|
76
|
+
return fn(*args, **kwargs)
|
|
77
|
+
elif result.outcome == "payment_required":
|
|
78
|
+
resp = jsonify(result.body)
|
|
79
|
+
resp.status_code = result.status_code or 402
|
|
80
|
+
for key, value in result.headers.items():
|
|
81
|
+
resp.headers[key] = value
|
|
82
|
+
return resp
|
|
83
|
+
else:
|
|
84
|
+
resp = jsonify(result.body)
|
|
85
|
+
resp.status_code = result.status_code or 500
|
|
86
|
+
return resp
|
|
87
|
+
|
|
88
|
+
return wrapper
|
|
89
|
+
|
|
90
|
+
return decorator
|
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable, Optional, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AgentSpendChargeError(Exception):
|
|
8
|
+
"""Raised when a charge or paywall operation fails."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str, status_code: int, details: Any = None):
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
self.details = details
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AgentSpendOptions:
|
|
18
|
+
"""Configuration for the AgentSpend client."""
|
|
19
|
+
|
|
20
|
+
platform_api_base_url: Optional[str] = None
|
|
21
|
+
service_api_key: Optional[str] = None
|
|
22
|
+
crypto: Optional[CryptoConfig] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CryptoConfig:
|
|
27
|
+
"""Crypto / x402 configuration."""
|
|
28
|
+
|
|
29
|
+
receiver_address: Optional[str] = None
|
|
30
|
+
network: str = "eip155:8453"
|
|
31
|
+
facilitator_url: str = "https://facilitator.openx402.ai"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ChargeOptions:
|
|
36
|
+
amount_cents: int
|
|
37
|
+
currency: str = "usd"
|
|
38
|
+
description: Optional[str] = None
|
|
39
|
+
metadata: Optional[dict[str, str]] = None
|
|
40
|
+
idempotency_key: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ChargeResponse:
|
|
45
|
+
charged: bool
|
|
46
|
+
card_id: str
|
|
47
|
+
amount_cents: int
|
|
48
|
+
currency: str
|
|
49
|
+
remaining_limit_cents: int
|
|
50
|
+
stripe_payment_intent_id: str
|
|
51
|
+
stripe_charge_id: str
|
|
52
|
+
charge_attempt_id: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class PaywallOptions:
|
|
57
|
+
"""Paywall configuration.
|
|
58
|
+
|
|
59
|
+
amount can be:
|
|
60
|
+
- int: fixed price in cents (e.g. 500 = $5.00)
|
|
61
|
+
- str: body field name to read amount from (e.g. "amount_cents")
|
|
62
|
+
- callable: custom dynamic pricing ``(body) -> int``
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
amount: Union[int, str, Callable[[Any], int]]
|
|
66
|
+
currency: str = "usd"
|
|
67
|
+
description: Optional[str] = None
|
|
68
|
+
metadata: Optional[Callable[[Any], dict[str, Any]]] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class PaywallPaymentContext:
|
|
73
|
+
method: str # "card" | "crypto"
|
|
74
|
+
amount_cents: int
|
|
75
|
+
currency: str
|
|
76
|
+
card_id: Optional[str] = None
|
|
77
|
+
remaining_limit_cents: Optional[int] = None
|
|
78
|
+
transaction_hash: Optional[str] = None
|
|
79
|
+
payer_address: Optional[str] = None
|
|
80
|
+
network: Optional[str] = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class PaywallRequest:
|
|
85
|
+
url: str
|
|
86
|
+
method: str
|
|
87
|
+
headers: dict[str, Optional[str]]
|
|
88
|
+
body: Any
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class PaywallResult:
|
|
93
|
+
"""Result from processPaywall.
|
|
94
|
+
|
|
95
|
+
outcome is one of: "charged", "crypto_paid", "payment_required", "error"
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
outcome: str
|
|
99
|
+
payment_context: Optional[PaywallPaymentContext] = None
|
|
100
|
+
status_code: Optional[int] = None
|
|
101
|
+
body: Optional[dict[str, Any]] = None
|
|
102
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentspend"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for AgentSpend — card & crypto paywalls for AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.25",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
flask = ["flask>=2.0"]
|
|
18
|
+
fastapi = ["fastapi>=0.100", "uvicorn>=0.20"]
|
|
19
|
+
django = ["django>=4.0"]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://agentspend.co"
|
|
23
|
+
Repository = "https://github.com/agentspend/agentspend"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["agentspend"]
|