solvapay 0.1.0.dev4__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.
- solvapay/__init__.py +26 -0
- solvapay/_currencies.py +170 -0
- solvapay/_generated/__init__.py +1 -0
- solvapay/_generated/models.py +3643 -0
- solvapay/_generated/operations.py +1894 -0
- solvapay/_http.py +243 -0
- solvapay/_js.py +114 -0
- solvapay/_tax_ids.py +442 -0
- solvapay/agents/__init__.py +67 -0
- solvapay/agents/calls.py +302 -0
- solvapay/agents/identity.py +293 -0
- solvapay/agents/mcp.py +614 -0
- solvapay/agents/oauth.py +105 -0
- solvapay/business_details.py +124 -0
- solvapay/client.py +107 -0
- solvapay/errors.py +33 -0
- solvapay/mcp/__init__.py +90 -0
- solvapay/mcp/app.py +243 -0
- solvapay/mcp/auth.py +604 -0
- solvapay/mcp/bootstrap.py +494 -0
- solvapay/mcp/intent.py +474 -0
- solvapay/mcp/mcp-app.html +119 -0
- solvapay/mcp/narrate.py +1019 -0
- solvapay/mcp/response.py +220 -0
- solvapay/mcp/server.py +343 -0
- solvapay/mcp/tools.py +273 -0
- solvapay/mcp/transport.py +550 -0
- solvapay/money.py +61 -0
- solvapay/paywall/__init__.py +25 -0
- solvapay/paywall/decide.py +450 -0
- solvapay/paywall/gate.py +176 -0
- solvapay/paywall/state.py +371 -0
- solvapay/pricing.py +281 -0
- solvapay/py.typed +0 -0
- solvapay/solvapay.py +173 -0
- solvapay/webhooks.py +93 -0
- solvapay-0.1.0.dev4.dist-info/METADATA +183 -0
- solvapay-0.1.0.dev4.dist-info/RECORD +39 -0
- solvapay-0.1.0.dev4.dist-info/WHEEL +4 -0
solvapay/solvapay.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""``create_solvapay()``: the SDK's entry point.
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
from solvapay import create_solvapay
|
|
5
|
+
|
|
6
|
+
solvapay = create_solvapay() # reads SOLVAPAY_SECRET_KEY
|
|
7
|
+
|
|
8
|
+
@solvapay.payable(product="prd_…")
|
|
9
|
+
async def summarise(text: str, *, customer_ref: str) -> str:
|
|
10
|
+
...
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Configuration comes from arguments, else the environment, the same
|
|
14
|
+
variables the TypeScript SDK reads:
|
|
15
|
+
|
|
16
|
+
- ``SOLVAPAY_SECRET_KEY`` (required): the merchant's secret key.
|
|
17
|
+
- ``SOLVAPAY_API_BASE_URL``: defaults to ``https://api.solvapay.com``.
|
|
18
|
+
- ``SOLVAPAY_PRODUCT_REF``: the product a paid call is for, when not given.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import functools
|
|
24
|
+
import inspect
|
|
25
|
+
import os
|
|
26
|
+
from collections.abc import Awaitable, Callable
|
|
27
|
+
from typing import Any, ParamSpec, TypeVar
|
|
28
|
+
|
|
29
|
+
from ._http import DEFAULT_API_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_SECONDS
|
|
30
|
+
from .client import AsyncApiClient
|
|
31
|
+
from .errors import SolvaPayError
|
|
32
|
+
from .paywall.decide import (
|
|
33
|
+
DEFAULT_LIMITS_CACHE_TTL_SECONDS,
|
|
34
|
+
FreeLimit,
|
|
35
|
+
Paywall,
|
|
36
|
+
PaywallDecision,
|
|
37
|
+
PaywallError,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
P = ParamSpec("P")
|
|
41
|
+
R = TypeVar("R")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SolvaPay:
|
|
45
|
+
"""The API client and the paywall for one merchant key."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, *, api: AsyncApiClient, product_ref: str | None = None, **paywall: Any):
|
|
48
|
+
self.api = api
|
|
49
|
+
self.product_ref = product_ref
|
|
50
|
+
self.paywall = Paywall(api, **paywall)
|
|
51
|
+
|
|
52
|
+
def require_product(self, product: str | None) -> str:
|
|
53
|
+
"""The product for a paid call: the one given, else ``SOLVAPAY_PRODUCT_REF``."""
|
|
54
|
+
resolved = product or self.product_ref
|
|
55
|
+
if not resolved:
|
|
56
|
+
raise SolvaPayError(
|
|
57
|
+
"No product: pass product=… or set SOLVAPAY_PRODUCT_REF.",
|
|
58
|
+
code="missing_product",
|
|
59
|
+
)
|
|
60
|
+
return resolved
|
|
61
|
+
|
|
62
|
+
async def gate(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
customer_ref: str,
|
|
66
|
+
product: str | None = None,
|
|
67
|
+
meter: str | None = None,
|
|
68
|
+
free_limit: FreeLimit | None = None,
|
|
69
|
+
tool_name: str | None = None,
|
|
70
|
+
) -> PaywallDecision:
|
|
71
|
+
"""``allow`` or ``gate`` for one paid call, for a handler you run yourself.
|
|
72
|
+
|
|
73
|
+
After an ``allow``, record the outcome with ``paywall.run_allow`` (or
|
|
74
|
+
call ``paywall.track_usage`` yourself).
|
|
75
|
+
"""
|
|
76
|
+
return await self.paywall.decide(
|
|
77
|
+
customer_ref=customer_ref,
|
|
78
|
+
product=self.require_product(product),
|
|
79
|
+
meter=meter,
|
|
80
|
+
free_limit=free_limit,
|
|
81
|
+
tool_name=tool_name,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def payable(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
product: str | None = None,
|
|
88
|
+
meter: str | None = None,
|
|
89
|
+
free_limit: FreeLimit | None = None,
|
|
90
|
+
customer_ref_arg: str = "customer_ref",
|
|
91
|
+
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
|
|
92
|
+
"""Makes an async function paid. Raises ``PaywallError`` (with ``.gate``) when gated.
|
|
93
|
+
|
|
94
|
+
The customer is read from the keyword argument named ``customer_ref_arg``.
|
|
95
|
+
"""
|
|
96
|
+
resolved_product = self.require_product(product)
|
|
97
|
+
|
|
98
|
+
def wrap(handler: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
|
99
|
+
if not inspect.iscoroutinefunction(handler):
|
|
100
|
+
raise SolvaPayError("payable() wraps async functions", code="invalid_argument")
|
|
101
|
+
|
|
102
|
+
@functools.wraps(handler)
|
|
103
|
+
async def paid(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
104
|
+
customer_ref = kwargs.get(customer_ref_arg)
|
|
105
|
+
if not isinstance(customer_ref, str) or not customer_ref:
|
|
106
|
+
raise SolvaPayError(
|
|
107
|
+
f"{handler.__name__}() was called without {customer_ref_arg}=…",
|
|
108
|
+
code="identity_required",
|
|
109
|
+
status=401,
|
|
110
|
+
)
|
|
111
|
+
decision = await self.paywall.decide(
|
|
112
|
+
customer_ref=customer_ref,
|
|
113
|
+
product=resolved_product,
|
|
114
|
+
meter=meter,
|
|
115
|
+
free_limit=free_limit,
|
|
116
|
+
tool_name=handler.__name__,
|
|
117
|
+
)
|
|
118
|
+
if decision.gate is not None:
|
|
119
|
+
raise PaywallError(decision.gate)
|
|
120
|
+
return await self.paywall.run_allow(
|
|
121
|
+
decision,
|
|
122
|
+
lambda: handler(*args, **kwargs),
|
|
123
|
+
product=resolved_product,
|
|
124
|
+
meter=meter,
|
|
125
|
+
free_limit=free_limit,
|
|
126
|
+
tool_name=handler.__name__,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return paid
|
|
130
|
+
|
|
131
|
+
return wrap
|
|
132
|
+
|
|
133
|
+
async def aclose(self) -> None:
|
|
134
|
+
await self.api.aclose()
|
|
135
|
+
|
|
136
|
+
async def __aenter__(self) -> SolvaPay:
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
async def __aexit__(self, *_: object) -> None:
|
|
140
|
+
await self.aclose()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def create_solvapay(
|
|
144
|
+
*,
|
|
145
|
+
api_key: str | None = None,
|
|
146
|
+
api_base_url: str | None = None,
|
|
147
|
+
product_ref: str | None = None,
|
|
148
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
149
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
150
|
+
limits_cache_ttl: float = DEFAULT_LIMITS_CACHE_TTL_SECONDS,
|
|
151
|
+
api: AsyncApiClient | None = None,
|
|
152
|
+
) -> SolvaPay:
|
|
153
|
+
"""A ``SolvaPay`` from arguments, else the environment. Fails without a secret key."""
|
|
154
|
+
if api is None:
|
|
155
|
+
key = api_key or os.environ.get("SOLVAPAY_SECRET_KEY")
|
|
156
|
+
if not key:
|
|
157
|
+
raise SolvaPayError(
|
|
158
|
+
"A SolvaPay secret key is required: pass api_key=… or set SOLVAPAY_SECRET_KEY.",
|
|
159
|
+
code="missing_secret",
|
|
160
|
+
)
|
|
161
|
+
api = AsyncApiClient(
|
|
162
|
+
api_key=key,
|
|
163
|
+
api_base_url=api_base_url
|
|
164
|
+
or os.environ.get("SOLVAPAY_API_BASE_URL")
|
|
165
|
+
or DEFAULT_API_BASE_URL,
|
|
166
|
+
timeout=timeout,
|
|
167
|
+
max_retries=max_retries,
|
|
168
|
+
)
|
|
169
|
+
return SolvaPay(
|
|
170
|
+
api=api,
|
|
171
|
+
product_ref=product_ref or os.environ.get("SOLVAPAY_PRODUCT_REF") or None,
|
|
172
|
+
limits_cache_ttl=limits_cache_ttl,
|
|
173
|
+
)
|
solvapay/webhooks.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Webhook signature verification, matching ``@solvapay/server``'s ``verifyWebhook``.
|
|
2
|
+
|
|
3
|
+
SolvaPay signs a delivery as HMAC-SHA256 over ``"{t}.{raw body}"``, keyed by
|
|
4
|
+
the whole signing secret (``whsec_…``, UTF-8), and sends it in the
|
|
5
|
+
``SV-Signature`` header as ``t={unix seconds},v1={lowercase hex}``.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from solvapay import verify_webhook
|
|
9
|
+
|
|
10
|
+
event = verify_webhook(
|
|
11
|
+
body=request.body, # the raw bytes or text, as received
|
|
12
|
+
signature=request.headers["sv-signature"],
|
|
13
|
+
secret=os.environ["SOLVAPAY_WEBHOOK_SECRET"],
|
|
14
|
+
)
|
|
15
|
+
```
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import hmac
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
import time
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from typing import Any, Final
|
|
27
|
+
|
|
28
|
+
from .errors import SolvaPayError
|
|
29
|
+
|
|
30
|
+
WEBHOOK_TOLERANCE_SECONDS: Final = 300
|
|
31
|
+
|
|
32
|
+
WEBHOOK_ERROR_CODES: Final = {
|
|
33
|
+
"missing": "webhook_signature_missing",
|
|
34
|
+
"malformed": "webhook_signature_malformed",
|
|
35
|
+
"timestamp": "webhook_timestamp_out_of_tolerance",
|
|
36
|
+
"invalid": "webhook_signature_invalid",
|
|
37
|
+
"payload": "webhook_payload_invalid",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _reject_constant(name: str) -> Any:
|
|
42
|
+
# JSON has no NaN or Infinity; json.loads accepts them, JSON.parse does not.
|
|
43
|
+
raise ValueError(f"{name} is not valid JSON")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def verify_webhook(
|
|
47
|
+
*,
|
|
48
|
+
body: str | bytes,
|
|
49
|
+
signature: str | None,
|
|
50
|
+
secret: str,
|
|
51
|
+
now: datetime | None = None,
|
|
52
|
+
) -> Any:
|
|
53
|
+
"""Verifies a delivery and returns the parsed event.
|
|
54
|
+
|
|
55
|
+
Raises ``SolvaPayError`` whose ``code`` is one of ``WEBHOOK_ERROR_CODES``.
|
|
56
|
+
``now`` is the time to check the signature's timestamp against
|
|
57
|
+
(default: the current time); a timestamp more than 300 seconds away,
|
|
58
|
+
either way, is rejected.
|
|
59
|
+
"""
|
|
60
|
+
raw = body if isinstance(body, bytes) else body.encode("utf-8")
|
|
61
|
+
if not signature:
|
|
62
|
+
raise SolvaPayError("Missing webhook signature", code=WEBHOOK_ERROR_CODES["missing"])
|
|
63
|
+
|
|
64
|
+
parts = [part.strip() for part in signature.split(",")]
|
|
65
|
+
t_part = next((p for p in parts if p.startswith("t=")), None)
|
|
66
|
+
v1_part = next((p for p in parts if p.startswith("v1=")), None)
|
|
67
|
+
if t_part is None or v1_part is None:
|
|
68
|
+
raise SolvaPayError("Malformed webhook signature", code=WEBHOOK_ERROR_CODES["malformed"])
|
|
69
|
+
raw_timestamp, received = t_part[2:], v1_part[3:]
|
|
70
|
+
if not re.fullmatch(r"\d+", raw_timestamp, flags=re.ASCII) or not received:
|
|
71
|
+
raise SolvaPayError("Malformed webhook signature", code=WEBHOOK_ERROR_CODES["malformed"])
|
|
72
|
+
|
|
73
|
+
now_seconds = int(now.timestamp()) if now is not None else int(time.time())
|
|
74
|
+
if abs(now_seconds - int(raw_timestamp)) > WEBHOOK_TOLERANCE_SECONDS:
|
|
75
|
+
raise SolvaPayError(
|
|
76
|
+
"Webhook signature timestamp is more than "
|
|
77
|
+
f"{WEBHOOK_TOLERANCE_SECONDS} seconds from now",
|
|
78
|
+
code=WEBHOOK_ERROR_CODES["timestamp"],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
expected = hmac.new(
|
|
82
|
+
secret.encode("utf-8"), raw_timestamp.encode("ascii") + b"." + raw, hashlib.sha256
|
|
83
|
+
).hexdigest()
|
|
84
|
+
# compare_digest takes ASCII text only; anything else cannot be a hex digest.
|
|
85
|
+
if not received.isascii() or not hmac.compare_digest(expected, received):
|
|
86
|
+
raise SolvaPayError("Invalid webhook signature", code=WEBHOOK_ERROR_CODES["invalid"])
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
return json.loads(raw, parse_constant=_reject_constant)
|
|
90
|
+
except ValueError as error:
|
|
91
|
+
raise SolvaPayError(
|
|
92
|
+
"Invalid webhook payload: body is not valid JSON", code=WEBHOOK_ERROR_CODES["payload"]
|
|
93
|
+
) from error
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: solvapay
|
|
3
|
+
Version: 0.1.0.dev4
|
|
4
|
+
Summary: SolvaPay for Python: paywalls, usage billing, webhooks and paid MCP tools.
|
|
5
|
+
Project-URL: Homepage, https://solvapay.com
|
|
6
|
+
Project-URL: Documentation, https://docs.solvapay.com
|
|
7
|
+
Project-URL: Source, https://github.com/solvapay/solvapay-sdk/tree/main/python
|
|
8
|
+
Author: SolvaPay
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,billing,mcp,payments,paywall,solvapay
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx<1,>=0.27
|
|
21
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
22
|
+
Provides-Extra: agents
|
|
23
|
+
Requires-Dist: pyjwt[crypto]<3,>=2.9; extra == 'agents'
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Requires-Dist: mcp<3,>=2.3; extra == 'mcp'
|
|
26
|
+
Requires-Dist: starlette>=0.40; extra == 'mcp'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# solvapay
|
|
30
|
+
|
|
31
|
+
SolvaPay for Python: paywalls, usage billing, webhooks and paid MCP tools.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install solvapay # API client, paywall, webhooks
|
|
35
|
+
pip install 'solvapay[mcp]' # paid tools for the official MCP Python SDK
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Python 3.10+. Typed (`py.typed`), async-first, with blocking twins for the API client.
|
|
39
|
+
|
|
40
|
+
## Configure
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export SOLVAPAY_SECRET_KEY=sk_sandbox_... # required
|
|
44
|
+
export SOLVAPAY_PRODUCT_REF=prd_... # the product paid calls bill against
|
|
45
|
+
export SOLVAPAY_API_BASE_URL=... # optional; defaults to https://api.solvapay.com
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run `npx solvapay init` to sign in and write these to `.env`.
|
|
49
|
+
|
|
50
|
+
## Make a function paid
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from solvapay import PaywallError, create_solvapay
|
|
54
|
+
|
|
55
|
+
solvapay = create_solvapay()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@solvapay.payable()
|
|
59
|
+
async def summarise(text: str, *, customer_ref: str) -> str:
|
|
60
|
+
return text[:100]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
summary = await summarise("...", customer_ref="user_123")
|
|
65
|
+
except PaywallError as error:
|
|
66
|
+
print(error.gate["message"]) # what the customer should see, with a checkout link
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`customer_ref` is your own user id: SolvaPay creates the customer on first use.
|
|
70
|
+
Each call is checked against the customer's plan, and billed when it succeeds.
|
|
71
|
+
|
|
72
|
+
## Paid MCP tools
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from solvapay import create_solvapay
|
|
76
|
+
from solvapay.mcp import ResponseContext, create_solvapay_mcp_server
|
|
77
|
+
|
|
78
|
+
mcp = create_solvapay_mcp_server(
|
|
79
|
+
solvapay=create_solvapay(),
|
|
80
|
+
public_base_url="https://research.example.com",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def quote(symbol: str, ctx: ResponseContext):
|
|
85
|
+
return ctx.respond({"symbol": symbol, "price": 101.5})
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
mcp.register_payable("quote", handler=quote, description="Latest price for a symbol.")
|
|
89
|
+
|
|
90
|
+
app = mcp.http_app() # serve with uvicorn; the MCP endpoint is /mcp
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Customers sign in with SolvaPay over OAuth, and every bearer token is verified
|
|
94
|
+
with SolvaPay. A call over the customer's limit returns the paywall: a message
|
|
95
|
+
naming the next step, with a checkout link. See
|
|
96
|
+
[`examples/python-stock-research-mcp`](../examples/python-stock-research-mcp).
|
|
97
|
+
|
|
98
|
+
The server also comes with SolvaPay's built-in tools, as the TypeScript server
|
|
99
|
+
does: `account` shows the customer's plan, balance and usage with a checkout,
|
|
100
|
+
top-up or account link (`view`: `checkout`, `account` or `topup`), and
|
|
101
|
+
`activate_plan` activates a plan by `planRef`. The `/upgrade`,
|
|
102
|
+
`/manage_account`, `/topup` and `/activate_plan` prompts and the
|
|
103
|
+
`docs://solvapay/overview.md` resource come with them. Pass `views=[...]` to
|
|
104
|
+
choose the views, or `views=[]` to leave `account` out.
|
|
105
|
+
|
|
106
|
+
### The checkout widget
|
|
107
|
+
|
|
108
|
+
On hosts that support MCP Apps (ChatGPT, Claude), `account` also opens
|
|
109
|
+
SolvaPay's checkout and account widget in the chat, the same one the TypeScript
|
|
110
|
+
SDK serves. It ships in this package (`solvapay/mcp/mcp-app.html`) and is
|
|
111
|
+
served at `ui://<name>/mcp-app.html` (`resource_uri=` to change it). Text-only
|
|
112
|
+
hosts get the same text as before; `mode: "text"` asks for text on any host.
|
|
113
|
+
|
|
114
|
+
The widget pays, tops up, cancels and reactivates renewals through six tools
|
|
115
|
+
only it calls: `create_payment_intent`, `process_payment`,
|
|
116
|
+
`create_hosted_session`, `set_renewal`, `attach_business_details` and
|
|
117
|
+
`get_history`. They are marked for the app (`_meta.ui.visibility: ["app"]`), so
|
|
118
|
+
MCP Apps hosts keep them from the model. For a host that lists them anyway,
|
|
119
|
+
`hide_tools_by_audience=["ui"]` leaves them out of `tools/list`; ChatGPT still
|
|
120
|
+
gets them, since its widget can only call listed tools.
|
|
121
|
+
|
|
122
|
+
Its Content Security Policy allows Stripe and the SolvaPay API. Add domains with
|
|
123
|
+
`csp={"connectDomains": [...], "resourceDomains": [...], "frameDomains": [...]}`.
|
|
124
|
+
|
|
125
|
+
### Paid by agents
|
|
126
|
+
|
|
127
|
+
`solvapay.agents.mcp.create_agent_mcp_route` serves an MCP server that SolvaPay
|
|
128
|
+
agents pay for per call: each paid tool call is authorized with SolvaPay before
|
|
129
|
+
it runs and completed at the cost the upstream reports. Install
|
|
130
|
+
`solvapay[agents,mcp]`.
|
|
131
|
+
|
|
132
|
+
## Verify webhooks
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from solvapay import SolvaPayError, verify_webhook
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
event = verify_webhook(
|
|
139
|
+
body=raw_body, # bytes or str, exactly as received
|
|
140
|
+
signature=request.headers["sv-signature"],
|
|
141
|
+
secret=os.environ["SOLVAPAY_WEBHOOK_SECRET"],
|
|
142
|
+
)
|
|
143
|
+
except SolvaPayError as error:
|
|
144
|
+
... # error.code: webhook_signature_invalid, webhook_timestamp_out_of_tolerance, ...
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## The API
|
|
148
|
+
|
|
149
|
+
Every SolvaPay API operation, generated from the API spec:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from solvapay import AsyncApiClient
|
|
153
|
+
|
|
154
|
+
async with AsyncApiClient(api_key="sk_sandbox_...") as api:
|
|
155
|
+
customer = await api.get_customer(reference="cus_123")
|
|
156
|
+
balance = await api.get_customer_balance(reference="cus_123")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`ApiClient` is the blocking twin. Errors are `SolvaPayError` with a stable `code`
|
|
160
|
+
and the HTTP `status`; branch on those, not on the message.
|
|
161
|
+
|
|
162
|
+
## Same behaviour as the TypeScript SDK
|
|
163
|
+
|
|
164
|
+
The paywall, its messages, the `account` and `activate_plan` narration, what
|
|
165
|
+
the widget's tools answer and webhook verification are checked against the
|
|
166
|
+
TypeScript SDK on every change by a shared conformance suite
|
|
167
|
+
([`conformance/`](../conformance)). The API client is generated from the same
|
|
168
|
+
spec as the TypeScript types ([`openapi/`](../openapi)).
|
|
169
|
+
|
|
170
|
+
## Versions
|
|
171
|
+
|
|
172
|
+
`solvapay` has its own version line, starting at 0.1.0. Its numbers don't
|
|
173
|
+
follow the TypeScript packages: Python 0.3.0 and `@solvapay/server` 2.12.0 can
|
|
174
|
+
behave the same. What the two share is the conformance suite (the version this
|
|
175
|
+
package passes is pinned in [`conformance.lock`](conformance.lock)) and the API
|
|
176
|
+
spec.
|
|
177
|
+
|
|
178
|
+
Until 1.0.0, a minor release (0.x → 0.y) may change the API. Previews of the
|
|
179
|
+
next release are published from the `dev` branch as `<next>.devN`:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pip install --pre solvapay
|
|
183
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
solvapay/__init__.py,sha256=p39j5ZvKms5SRKhchZHusWaRaK2n-9SKHP1WUaCb-tc,755
|
|
2
|
+
solvapay/_currencies.py,sha256=GdDf4FG2s5_OEmd2Gv8hOmfbk_epImkBT4juaxkNAGc,3417
|
|
3
|
+
solvapay/_http.py,sha256=WK6OT90Mro6wpsKZlh8t75T9wQf4qgCqB2VaVaGJr0w,8404
|
|
4
|
+
solvapay/_js.py,sha256=u-C1Y5YqLDWR2iSnC0YyifYCUdqB6AjTRsKuirVH4Q0,3489
|
|
5
|
+
solvapay/_tax_ids.py,sha256=D0TVr3Gk4Qy6nNG-wAS7X-QFxZ6Cl20oJU6-uEJlXk8,11234
|
|
6
|
+
solvapay/business_details.py,sha256=qmRhTc_xwvT53ZXz-J6RNBI_s7Uj7mnXEkqvJtLwqNg,4343
|
|
7
|
+
solvapay/client.py,sha256=be8XfsHLRDo1YY4KLgvcbDVibTquZ0lCY4OgE1BhTKg,2753
|
|
8
|
+
solvapay/errors.py,sha256=QovGghgtkGs4s1ipFlpoD0lKZwnwKKkbXt0iF9h6L5A,821
|
|
9
|
+
solvapay/money.py,sha256=D9EIJfGUYj0w45wmkEeZadeBI8vQ2nY9lsSi2YQ4mmA,2157
|
|
10
|
+
solvapay/pricing.py,sha256=2EWL2HZe5ZUYvlaOO2onLwyuswYJUiFMLdsvY0sdkOw,10375
|
|
11
|
+
solvapay/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
solvapay/solvapay.py,sha256=OW5XNGBRfJOPy2nDADp-c7UBOx09UaHxoKUqjutsxbo,5867
|
|
13
|
+
solvapay/webhooks.py,sha256=uF1RCiNo0vlNHYTbm_6jEjWZd5J5echgu2Xgg8EGP3Y,3445
|
|
14
|
+
solvapay/_generated/__init__.py,sha256=ocuVKIf-7sVBn7gvvPlDy1_hkPNm8UKRSzbqOjE5y-Y,92
|
|
15
|
+
solvapay/_generated/models.py,sha256=7ISI0RixUpotXYO-rzUKdzHSANBuB3Noow8c3w82Y8s,108970
|
|
16
|
+
solvapay/_generated/operations.py,sha256=xZ5chS9Dr8GHfvdo-FtrWxxNmOBycnhULnrTflEkxhI,57992
|
|
17
|
+
solvapay/agents/__init__.py,sha256=7YB_yxDQOpoOoh6cgBzGjaVJzdeOGpveyCojnvCyHYU,1425
|
|
18
|
+
solvapay/agents/calls.py,sha256=mVfaTGilCCiyijR7AzcH5qaZf-VMFM_pNALGaQBPj3Q,11473
|
|
19
|
+
solvapay/agents/identity.py,sha256=TzSSuw2mynOPRk1fsvW4aDax5Cnj8TqUTv2UUCs1v_M,11090
|
|
20
|
+
solvapay/agents/mcp.py,sha256=2Duibu9OuKH3gmNbpcNd_JxpGEmll1EVZOYJTJDDLg8,23079
|
|
21
|
+
solvapay/agents/oauth.py,sha256=_dZObCLSMKDQ0b99lgewFnqbWVZZ-1oi49XKhvz4ywY,4280
|
|
22
|
+
solvapay/mcp/__init__.py,sha256=HP7jierJQsUgk5mZO_CpLF89n6BkeVEkNEpP9piajUc,2301
|
|
23
|
+
solvapay/mcp/app.py,sha256=mukkCleekLGhnB0VfBqwtRFr0-RU-IiwOI4JvwnJMow,9241
|
|
24
|
+
solvapay/mcp/auth.py,sha256=izhI8JEiZFFAb259AsiOZQk1PSMkMSwLxJnmbzTynEE,23479
|
|
25
|
+
solvapay/mcp/bootstrap.py,sha256=T_NkpcMejFy1oC5des_McghFS4Khyp05_vDQ0AVOtHU,18196
|
|
26
|
+
solvapay/mcp/intent.py,sha256=ccumggLaMOj4tAUZVraua5dlp56EU8qc3zYNqThF8-Q,19196
|
|
27
|
+
solvapay/mcp/mcp-app.html,sha256=4IbDS93ib2B7c-Ehym2god2ZvAIvq-U217cvPzxfj9o,848783
|
|
28
|
+
solvapay/mcp/narrate.py,sha256=kDrsqSqPxrybPGO9Pr0OBNt81rNbVYwhGWxyGS87mDs,36358
|
|
29
|
+
solvapay/mcp/response.py,sha256=kiSFWAa0IXlvd-EaUaCGBkE56fh9Di8smzLR5EK-1qA,7497
|
|
30
|
+
solvapay/mcp/server.py,sha256=LupoijalrC6Cy0JHeijeXWhUAUKyIWNdawMmP6XkKEc,12901
|
|
31
|
+
solvapay/mcp/tools.py,sha256=FBRtIcFXQvCB-wZLCxgn1ad9cxB0_4tacFsDcq0MDeM,9847
|
|
32
|
+
solvapay/mcp/transport.py,sha256=4XYyhqqboOfbHVVUPMoi_U1dSeMOJUKKHKr7TS4kDXM,21477
|
|
33
|
+
solvapay/paywall/__init__.py,sha256=aIxL6iS3DLkwOcewOu0XMIJM8DCzjQastIbmme2oH6g,591
|
|
34
|
+
solvapay/paywall/decide.py,sha256=mcEG563R621B63e4D9OnV4TQpwr79DN9JERWvE6elsE,16697
|
|
35
|
+
solvapay/paywall/gate.py,sha256=oFNFpOvhYg5m1gRgLsttqfdSV7YvEtOzvj3cSPzQk7E,6001
|
|
36
|
+
solvapay/paywall/state.py,sha256=xqwziDE9EisPpAcC9to0EjG4flE1ijXaC62KohdZGwA,14135
|
|
37
|
+
solvapay-0.1.0.dev4.dist-info/METADATA,sha256=kcm5FkyqbD3TQcmPPrRegQxC6syi0V2MtswVbk7U440,6738
|
|
38
|
+
solvapay-0.1.0.dev4.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
39
|
+
solvapay-0.1.0.dev4.dist-info/RECORD,,
|