solvapay-python 0.6.0__tar.gz → 0.7.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.
- solvapay_python-0.7.0/CHANGELOG.md +45 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/PKG-INFO +11 -4
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/README.md +10 -3
- solvapay_python-0.7.0/examples/marketplace/.env.example +15 -0
- solvapay_python-0.7.0/examples/marketplace/.streamlit/config.toml +16 -0
- solvapay_python-0.7.0/examples/marketplace/PLAN.md +108 -0
- solvapay_python-0.7.0/examples/marketplace/README.md +67 -0
- solvapay_python-0.7.0/examples/marketplace/agents.py +124 -0
- solvapay_python-0.7.0/examples/marketplace/app.py +259 -0
- solvapay_python-0.7.0/examples/marketplace/demo_customers.py +27 -0
- solvapay_python-0.7.0/examples/marketplace/requirements.txt +4 -0
- solvapay_python-0.7.0/examples/marketplace/sdk_gateway.py +85 -0
- solvapay_python-0.7.0/examples/marketplace/ui_components.py +155 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/pyproject.toml +1 -1
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/__init__.py +1 -1
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/_async_client.py +7 -3
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/_http.py +2 -2
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/client.py +7 -3
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/models.py +31 -5
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/paywall.py +20 -2
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/paywall_state.py +67 -1
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_customer.py +52 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_lifecycle.py +20 -4
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_paywall_state.py +75 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/uv.lock +2 -2
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/.github/workflows/ci.yml +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/.github/workflows/publish.yml +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/.gitignore +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/.python-version +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/LICENSE +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/fastmcp-paywall/.env.example +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/fastmcp-paywall/.gitignore +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/fastmcp-paywall/README.md +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/fastmcp-paywall/claim.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/fastmcp-paywall/pyproject.toml +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/fastmcp-paywall/server.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/fastmcp-paywall/uv.lock +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/langchain-paywall/.env.example +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/langchain-paywall/.gitignore +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/langchain-paywall/README.md +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/langchain-paywall/agent.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/examples/langchain-paywall/pyproject.toml +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/_config.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/events.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/exceptions.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/fastapi.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/langchain.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/src/solvapay/webhooks.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/__init__.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/conftest.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_admin.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_async_client.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_checkout.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_config.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_http.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_invariants.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_langchain.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_limits.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_paywall.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_webhook_events.py +0 -0
- {solvapay_python-0.6.0 → solvapay_python-0.7.0}/tests/test_webhooks.py +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.7.0 — 2026-05-17
|
|
4
|
+
|
|
5
|
+
Real-API alignment after testing against the SolvaPay sandbox revealed wire-format mismatches in three places. All existing public APIs continue to work; field aliases tolerate both old and new shapes.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **`Customer.customer_ref`** now accepts `reference` (real API) in addition to `customerRef` via `validation_alias=AliasChoices(...)`. Existing callers using `customerRef` keep working.
|
|
9
|
+
- **`Customer.ensure_customer()`** (sync + async) reads `reference` from the API response; falls back to `customerRef` for backward compatibility. Raises if neither is present.
|
|
10
|
+
- **`BalanceResponse`** rewritten to mirror real API shape:
|
|
11
|
+
- new fields: `credits: int`, `display_currency: str`, `credits_per_minor_unit: int`, `display_exchange_rate: float`
|
|
12
|
+
- `balance: float` and `currency: str` are now computed properties for backward compatibility (`balance = credits / credits_per_minor_unit / 100`)
|
|
13
|
+
- `plan` field removed (real API does not return it)
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **`solvapay.paywall_state.gate(client, *, customer_ref, product_ref, plan_ref=None)`** — one-call helper that runs `check_limits` + enriches the response with `create_checkout_session` (when checkout URL missing) and `get_customer` (when plan info missing) before classifying with `decide()`. Use this in product UX where you want one call to yield a fully actionable `GateDecision`.
|
|
17
|
+
- **`paywall.require` + `paywall.require_async`**: when blocked and the `LimitResponse` lacks a checkout URL, now automatically calls `create_checkout_session` and surfaces the resulting URL on `PaywallRequired.checkout_url`.
|
|
18
|
+
- **`examples/marketplace/`** — Streamlit marketplace demo of 4 paywalled AI agents (Google Gemini), running against real SolvaPay sandbox. Shows both integration styles: explicit `check_limits` + `decide()` and the `@paywall.require` decorator.
|
|
19
|
+
|
|
20
|
+
### Internal
|
|
21
|
+
- User-Agent bumped to `solvapay-python/0.7.0`
|
|
22
|
+
- 125 tests (up from 121), `mypy --strict` clean, `ruff` clean
|
|
23
|
+
|
|
24
|
+
## 0.6.0
|
|
25
|
+
- Admin endpoints (products, plans, merchant config)
|
|
26
|
+
- GitHub Actions trusted-publish to PyPI
|
|
27
|
+
|
|
28
|
+
## 0.5.0
|
|
29
|
+
- `paywall_state` pure-function state classifier (ACTIVATION_REQUIRED / TOPUP_REQUIRED / UPGRADE_REQUIRED)
|
|
30
|
+
- LangChain `monetize_tool` wrapper
|
|
31
|
+
|
|
32
|
+
## 0.4.0
|
|
33
|
+
- Full async client surface
|
|
34
|
+
- 5 lifecycle operations
|
|
35
|
+
- 13 typed webhook event classes with discriminated union parsing
|
|
36
|
+
|
|
37
|
+
## 0.3.0
|
|
38
|
+
- FastMCP example: AI agent with two paywalled tools
|
|
39
|
+
|
|
40
|
+
## 0.2.0
|
|
41
|
+
- `@paywall.require` decorator
|
|
42
|
+
- FastAPI webhook router
|
|
43
|
+
|
|
44
|
+
## 0.1.0
|
|
45
|
+
- Sync client, HMAC-SHA256 webhook verification, Pydantic v2 models, CI on 3 Python versions
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solvapay-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Community Python SDK for SolvaPay (agent-native payment rails)
|
|
5
5
|
Project-URL: Homepage, https://github.com/dhruv-sanan/solvapay-python
|
|
6
6
|
Project-URL: Issues, https://github.com/dhruv-sanan/solvapay-python/issues
|
|
@@ -28,20 +28,26 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
|
|
29
29
|
# solvapay-python
|
|
30
30
|
|
|
31
|
+
[](https://pypi.org/project/solvapay-python/)
|
|
32
|
+
|
|
31
33
|
Community Python SDK for [SolvaPay](https://solvapay.com) — payment rails for the agentic economy.
|
|
32
34
|
|
|
33
|
-
> **Status:** v0.
|
|
35
|
+
> **Status:** v0.6, community-maintained. Available on PyPI. Pending official adoption.
|
|
34
36
|
> Mirrors the most-used surface of [@solvapay/core](https://github.com/solvapay/solvapay-sdk).
|
|
35
37
|
|
|
36
38
|
Python is the dominant language for agent frameworks (LangChain, FastMCP, CrewAI, AutoGen). SolvaPay's official SDK is TypeScript-only. This SDK brings first-class Python support so agent developers can gate tools behind paywalls without switching ecosystems.
|
|
37
39
|
|
|
38
|
-
>
|
|
40
|
+
> **New in v0.6:** Admin endpoints (products, plans, merchant, platform config). Published to PyPI.
|
|
41
|
+
> **v0.5:** Paywall state classifier (`paywall_state` module) and LangChain `monetize_tool` decorator — gate any LangChain tool behind a SolvaPay paywall with one line.
|
|
39
42
|
> **v0.4:** Async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events.
|
|
40
43
|
|
|
41
44
|
## Install
|
|
42
45
|
|
|
43
46
|
```bash
|
|
44
|
-
pip install
|
|
47
|
+
pip install solvapay-python
|
|
48
|
+
# with optional extras:
|
|
49
|
+
pip install solvapay-python[langchain]
|
|
50
|
+
pip install solvapay-python[fastapi]
|
|
45
51
|
```
|
|
46
52
|
|
|
47
53
|
## Quickstart
|
|
@@ -251,6 +257,7 @@ async def handle_webhook(request: Request) -> dict:
|
|
|
251
257
|
- v0.3 — FastMCP paywall demo (`examples/fastmcp-paywall/`) ✅
|
|
252
258
|
- v0.4 — async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events ✅
|
|
253
259
|
- v0.5 — paywall state classifier, LangChain `monetize_tool` decorator ✅
|
|
260
|
+
- v0.6 — admin endpoints (products, plans, merchant, platform config), PyPI publish ✅
|
|
254
261
|
|
|
255
262
|
## Contributing
|
|
256
263
|
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
# solvapay-python
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/solvapay-python/)
|
|
4
|
+
|
|
3
5
|
Community Python SDK for [SolvaPay](https://solvapay.com) — payment rails for the agentic economy.
|
|
4
6
|
|
|
5
|
-
> **Status:** v0.
|
|
7
|
+
> **Status:** v0.6, community-maintained. Available on PyPI. Pending official adoption.
|
|
6
8
|
> Mirrors the most-used surface of [@solvapay/core](https://github.com/solvapay/solvapay-sdk).
|
|
7
9
|
|
|
8
10
|
Python is the dominant language for agent frameworks (LangChain, FastMCP, CrewAI, AutoGen). SolvaPay's official SDK is TypeScript-only. This SDK brings first-class Python support so agent developers can gate tools behind paywalls without switching ecosystems.
|
|
9
11
|
|
|
10
|
-
>
|
|
12
|
+
> **New in v0.6:** Admin endpoints (products, plans, merchant, platform config). Published to PyPI.
|
|
13
|
+
> **v0.5:** Paywall state classifier (`paywall_state` module) and LangChain `monetize_tool` decorator — gate any LangChain tool behind a SolvaPay paywall with one line.
|
|
11
14
|
> **v0.4:** Async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events.
|
|
12
15
|
|
|
13
16
|
## Install
|
|
14
17
|
|
|
15
18
|
```bash
|
|
16
|
-
pip install
|
|
19
|
+
pip install solvapay-python
|
|
20
|
+
# with optional extras:
|
|
21
|
+
pip install solvapay-python[langchain]
|
|
22
|
+
pip install solvapay-python[fastapi]
|
|
17
23
|
```
|
|
18
24
|
|
|
19
25
|
## Quickstart
|
|
@@ -223,6 +229,7 @@ async def handle_webhook(request: Request) -> dict:
|
|
|
223
229
|
- v0.3 — FastMCP paywall demo (`examples/fastmcp-paywall/`) ✅
|
|
224
230
|
- v0.4 — async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events ✅
|
|
225
231
|
- v0.5 — paywall state classifier, LangChain `monetize_tool` decorator ✅
|
|
232
|
+
- v0.6 — admin endpoints (products, plans, merchant, platform config), PyPI publish ✅
|
|
226
233
|
|
|
227
234
|
## Contributing
|
|
228
235
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# --- SolvaPay sandbox ---
|
|
2
|
+
SOLVAPAY_API_BASE_URL=https://api.solvapay.com
|
|
3
|
+
SOLVAPAY_SECRET_KEY=sk_sandbox_REPLACE_ME
|
|
4
|
+
SOLVAPAY_PRODUCT_REF=prd_0QKI8NHF
|
|
5
|
+
|
|
6
|
+
# Customer A: existing sandbox customer subscribed to the product above (OK path).
|
|
7
|
+
SOLVAPAY_DEMO_CUSTOMER_REF=cus_CI5SGXJF
|
|
8
|
+
|
|
9
|
+
# Customer B: pre-existing sandbox customer with no subscription.
|
|
10
|
+
# SolvaPay returns withinLimits=false -> paywall_state.decide() classifies the gate.
|
|
11
|
+
SOLVAPAY_DEMO_BLOCKED_CUSTOMER_REF=cus_YARKQDEN
|
|
12
|
+
|
|
13
|
+
# --- Google Gemini (real LLM calls) ---
|
|
14
|
+
GEMINI_API_KEY=REPLACE_ME
|
|
15
|
+
GEMINI_MODEL=gemini-2.0-flash
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[theme]
|
|
2
|
+
base = "dark"
|
|
3
|
+
primaryColor = "#7c5cff"
|
|
4
|
+
backgroundColor = "#0e1117"
|
|
5
|
+
secondaryBackgroundColor = "#161a22"
|
|
6
|
+
textColor = "#e6e9ef"
|
|
7
|
+
font = "sans serif"
|
|
8
|
+
|
|
9
|
+
[server]
|
|
10
|
+
headless = true
|
|
11
|
+
|
|
12
|
+
[browser]
|
|
13
|
+
gatherUsageStats = false
|
|
14
|
+
|
|
15
|
+
[client]
|
|
16
|
+
toolbarMode = "minimal"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Scope A — v0.7.0 SDK fixes + Marketplace Demo
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
- Real SolvaPay sandbox compatibility (SDK shape bugs fixed)
|
|
5
|
+
- Demo shows OK + UPGRADE_REQUIRED with real checkout URL
|
|
6
|
+
- v0.7.0 shipped + 90s LinkedIn demo recorded
|
|
7
|
+
|
|
8
|
+
## Real-API findings (recorded 2026-05-16)
|
|
9
|
+
|
|
10
|
+
| Endpoint | SDK expects | Real API returns |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `GET /v1/sdk/customers/{ref}` | `customerRef` | `reference` |
|
|
13
|
+
| `GET /v1/sdk/customers?externalRef=` | `customerRef` | `reference` |
|
|
14
|
+
| `POST /v1/sdk/customers` | `customerRef` | `reference` (assumed; mirrors GET) |
|
|
15
|
+
| `GET /v1/sdk/customers/{ref}/balance` | `{balance, currency, plan}` | `{customerRef, credits, displayCurrency, creditsPerMinorUnit, displayExchangeRate}` |
|
|
16
|
+
| `POST /v1/sdk/limits` | `{withinLimits, remaining, plan, creditBalance, checkoutUrl, ...}` | `{withinLimits, remaining, meterName}` (no plan, no checkout_url) |
|
|
17
|
+
|
|
18
|
+
## Workstreams
|
|
19
|
+
|
|
20
|
+
### A. SDK patches (me) — v0.7.0
|
|
21
|
+
1. **`models.py`**
|
|
22
|
+
- `Customer`: alias `customerRef` → `reference` for `customer_ref` field (or rename field to `reference`)
|
|
23
|
+
- `BalanceResponse`: drop `balance/currency/plan`; add `credits: int`, `display_currency: str` (alias `displayCurrency`), `credits_per_minor_unit: int` (alias `creditsPerMinorUnit`). Add `balance` property converting credits to display units for backwards-compat.
|
|
24
|
+
- `LimitResponse`: keep optional fields tolerant; ensure missing `plan`, `creditBalance`, `checkoutUrl` parse to `None` (already partially the case — verify).
|
|
25
|
+
2. **`client.py` + `_async_client.py`**
|
|
26
|
+
- `ensure_customer`: replace `existing.get("customerRef")` / `existing["customerRef"]` / `created["customerRef"]` with `reference` (lines 127, 128, 143 sync; 111, 112, 127 async)
|
|
27
|
+
3. **`paywall.py`**
|
|
28
|
+
- When `within_limits == False` and `limits.checkout_url is None`, automatically call `create_checkout_session(customer_ref=..., product_ref=...)` and surface the resulting URL in both `PaywallRequired.checkout_url` and the raised banner. Mirror in async path.
|
|
29
|
+
4. **`paywall_state.py`**
|
|
30
|
+
- Stays pure. `decide()` works on whatever `LimitResponse` it gets. New helper `decide_with_checkout(client, limits, *, customer_ref, product_ref)` for callers that want the checkout URL materialized.
|
|
31
|
+
5. **Tests**
|
|
32
|
+
- Update `tests/conftest.py` fixtures to use real shape
|
|
33
|
+
- Add regression test using captured real-API payloads
|
|
34
|
+
- Verify `mypy --strict` + `ruff` pass
|
|
35
|
+
6. **Versioning**
|
|
36
|
+
- Bump `pyproject.toml` to `0.7.0`
|
|
37
|
+
- Update `__init__.py` __version__ if present
|
|
38
|
+
- Update User-Agent in `_http.py` to `solvapay-python/0.7.0`
|
|
39
|
+
- Append CHANGELOG entry
|
|
40
|
+
|
|
41
|
+
### B. Sandbox dashboard config (user) — parallel
|
|
42
|
+
1. On product `prd_0QKI8NHF`: add **Pro** pricing option (recurring, high reqs/month e.g. 10000) — NOT default
|
|
43
|
+
2. Subscribe Alice (`cus_CI5SGXJF`) to Pro
|
|
44
|
+
3. Leave Bob (`cus_YARKQDEN`) on Free (10 reqs/month default)
|
|
45
|
+
4. Pre-record: drain Bob's free reqs via the demo itself OR via direct API
|
|
46
|
+
|
|
47
|
+
### C. Marketplace demo wiring (me)
|
|
48
|
+
1. Use real `client.ensure_customer` for Bob (already created → idempotent)
|
|
49
|
+
2. `check_and_decide`: real `check_limits` → `decide()` → if blocked, mint real `create_checkout_session` URL
|
|
50
|
+
3. Real `track_usage` per call
|
|
51
|
+
4. Sidebar shows real `remaining` from `check_limits` + plan name from Customer purchases
|
|
52
|
+
5. 4 agents call real Google Gemini
|
|
53
|
+
6. "Show the SDK call" expander prints actual Python with real values
|
|
54
|
+
7. Reset button: re-bootstrap fresh blocked customer via `ensure_customer(timestamp-ref)` — for clean retakes
|
|
55
|
+
|
|
56
|
+
### D. video.md Snippet 4 (me)
|
|
57
|
+
- Write 90s script for marketplace walkthrough
|
|
58
|
+
- Update delivery checklist
|
|
59
|
+
|
|
60
|
+
## File list
|
|
61
|
+
|
|
62
|
+
**SDK:**
|
|
63
|
+
- `src/solvapay/models.py`
|
|
64
|
+
- `src/solvapay/client.py`
|
|
65
|
+
- `src/solvapay/_async_client.py`
|
|
66
|
+
- `src/solvapay/paywall.py`
|
|
67
|
+
- `src/solvapay/_http.py` (UA version bump)
|
|
68
|
+
- `pyproject.toml` (version 0.7.0)
|
|
69
|
+
- `tests/conftest.py` + impacted test files
|
|
70
|
+
- `CHANGELOG.md` (if present)
|
|
71
|
+
|
|
72
|
+
**Marketplace:**
|
|
73
|
+
- `examples/marketplace/app.py`
|
|
74
|
+
- `examples/marketplace/sdk_gateway.py`
|
|
75
|
+
- `examples/marketplace/agents.py`
|
|
76
|
+
- `examples/marketplace/demo_customers.py`
|
|
77
|
+
- `examples/marketplace/ui_components.py`
|
|
78
|
+
- `examples/marketplace/README.md`
|
|
79
|
+
- delete `examples/marketplace/_smoke.py` before commit
|
|
80
|
+
|
|
81
|
+
**Docs:**
|
|
82
|
+
- `/Users/dhruvsanan/Desktop/open-source/video.md` (Snippet 4)
|
|
83
|
+
|
|
84
|
+
## Execution sequence
|
|
85
|
+
1. ✅ Write PLAN.md + update video.md Snippet 4
|
|
86
|
+
2. Patch SDK models + client + async client
|
|
87
|
+
3. Patch paywall.py (auto-mint checkout URL on block)
|
|
88
|
+
4. Update tests + run pytest + mypy + ruff
|
|
89
|
+
5. Bump version + CHANGELOG
|
|
90
|
+
6. Rewire marketplace gateway/app/sidebar against patched SDK
|
|
91
|
+
7. Re-run `_smoke.py` against real sandbox — verify all paths produce expected typed states
|
|
92
|
+
8. Streamlit local end-to-end run
|
|
93
|
+
9. Once Alice on Pro: record 90s clip
|
|
94
|
+
10. Delete `_smoke.py`, commit, push, tag v0.7.0, publish on PyPI
|
|
95
|
+
|
|
96
|
+
## Open decisions
|
|
97
|
+
- Customer model field name: keep `customer_ref` with new alias OR rename to `reference`. **Decision: keep `customer_ref` with alias change** — preserves public API for existing users.
|
|
98
|
+
- `BalanceResponse.balance` semantics: `credits / credits_per_minor_unit` gives display-unit amount. Keep `balance: float` as computed property for backward compat. Real users see "$1000.00" not "100000 credits".
|
|
99
|
+
- Plan info source: real `/limits` doesn't return plan; pull plan from `client.get_customer(ref).purchases[0].planSnapshot` if needed for sidebar.
|
|
100
|
+
|
|
101
|
+
## Risk register
|
|
102
|
+
| Risk | Mitigation |
|
|
103
|
+
|---|---|
|
|
104
|
+
| SolvaPay API returns different shape for `POST /customers` than GET | Test create explicitly during patches; tolerate both shapes |
|
|
105
|
+
| Pro plan subscribe flow not exposed in dashboard → can't subscribe Alice without API call | User flags blocker; I add `subscribe` helper or use checkout-session flow |
|
|
106
|
+
| `track_usage` fails for Bob (0 credits, Free plan) → can't drain him via API | Drain via UI (record Bob using 10 calls live = the demo itself) |
|
|
107
|
+
| `mypy --strict` fails after BalanceResponse rewrite | Add explicit type hints + computed property typing |
|
|
108
|
+
| Existing tests pass against old mock shape but break against real shape | Update fixtures to mirror real shape (captured in this PLAN.md table) |
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Agent Marketplace — a real SolvaPay SDK demo
|
|
2
|
+
|
|
3
|
+
Streamlit marketplace of paywalled AI agents. Every paywall decision hits the
|
|
4
|
+
real **SolvaPay sandbox**. Every agent run hits a real **Gemini LLM**.
|
|
5
|
+
|
|
6
|
+
## What's real
|
|
7
|
+
|
|
8
|
+
- `solvapay-python` SDK — unmodified, against `https://api.solvapay.com`
|
|
9
|
+
- `check_limits` → real
|
|
10
|
+
- `track_usage` → real (sandbox metering ticks)
|
|
11
|
+
- `get_customer_balance` → real
|
|
12
|
+
- `ensure_customer` → real (bootstraps Customer B on first run)
|
|
13
|
+
- `paywall_state.decide()` → real typed state machine on real responses
|
|
14
|
+
- `@paywall.require` decorator → real, raises `PaywallRequired` from sandbox
|
|
15
|
+
- Two real sandbox customers:
|
|
16
|
+
- **Alice** (`SOLVAPAY_DEMO_CUSTOMER_REF`) — pre-existing, subscribed
|
|
17
|
+
- **Bob** (`SOLVAPAY_DEMO_BLOCKED_CUSTOMER_REF`) — created by the app via
|
|
18
|
+
`ensure_customer()` on first boot; no plan assigned → SolvaPay returns
|
|
19
|
+
`withinLimits=false`
|
|
20
|
+
- One real product (`SOLVAPAY_PRODUCT_REF`) — all 4 agents bill against it
|
|
21
|
+
- Real LLM calls via Google Gemini
|
|
22
|
+
|
|
23
|
+
## What's "fake"
|
|
24
|
+
|
|
25
|
+
- The 4 per-agent display prices ($0.02 / $0.05 / $0.10 / $0.03) are cosmetic
|
|
26
|
+
for the marketplace look. Real metering is whatever your SolvaPay product
|
|
27
|
+
config dictates. The sidebar discloses this.
|
|
28
|
+
|
|
29
|
+
That's it. No HTTP mocking. No fake customers.
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install -r requirements.txt
|
|
35
|
+
pip install -e ../.. # local SDK (editable)
|
|
36
|
+
cp .env.example .env # fill in:
|
|
37
|
+
# SOLVAPAY_SECRET_KEY (sandbox)
|
|
38
|
+
# SOLVAPAY_PRODUCT_REF (your product)
|
|
39
|
+
# SOLVAPAY_DEMO_CUSTOMER_REF (subscribed customer)
|
|
40
|
+
# GEMINI_API_KEY
|
|
41
|
+
streamlit run app.py
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
First boot calls `client.ensure_customer(...)` to create Customer B in your
|
|
45
|
+
sandbox. Subsequent boots find the same customer (idempotent).
|
|
46
|
+
|
|
47
|
+
## What the demo shows
|
|
48
|
+
|
|
49
|
+
1. Alice runs an agent → SDK approves → LLM responds → usage tracked.
|
|
50
|
+
2. Switch to Bob → click any agent → SDK blocks → `paywall_state.decide()`
|
|
51
|
+
returns a typed `GateDecision` → UI branches on the enum.
|
|
52
|
+
3. The "Show the SDK call" panel under each gated result prints the real
|
|
53
|
+
Python that produced the state — including the real `checkout_url`
|
|
54
|
+
SolvaPay returned. Click it; it opens the real sandbox checkout.
|
|
55
|
+
|
|
56
|
+
Two integration styles visible side-by-side:
|
|
57
|
+
|
|
58
|
+
- **Explicit** — Web Researcher / Text Analyst / Image Describer use
|
|
59
|
+
`client.check_limits(...)` + `paywall_state.decide(...)`.
|
|
60
|
+
- **Decorator** — Code Reviewer's runner is wrapped with
|
|
61
|
+
`@paywall.require(product=..., customer_ref_arg="customer_ref")`.
|
|
62
|
+
Catches `PaywallRequired` and re-uses the explicit path to populate the
|
|
63
|
+
same typed banner.
|
|
64
|
+
|
|
65
|
+
## Cost
|
|
66
|
+
|
|
67
|
+
Each full demo cycle costs <$0.01 in Gemini tokens. SolvaPay sandbox is free.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Marketplace agent catalog. Each agent calls a real LLM (Google Gemini).
|
|
2
|
+
|
|
3
|
+
All 4 agents bill the same SolvaPay product (the one in your sandbox). They
|
|
4
|
+
differ only by system prompt + display price — the marketplace framing is
|
|
5
|
+
cosmetic; real metering is per-call against `SOLVAPAY_PRODUCT_REF`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Callable
|
|
14
|
+
|
|
15
|
+
from google import genai
|
|
16
|
+
from google.genai import types
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger("marketplace.llm")
|
|
19
|
+
|
|
20
|
+
_client: genai.Client | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_client() -> genai.Client:
|
|
24
|
+
global _client
|
|
25
|
+
if _client is None:
|
|
26
|
+
_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
|
|
27
|
+
return _client
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _model() -> str:
|
|
31
|
+
return os.environ.get("GEMINI_MODEL", "gemini-2.0-flash")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _llm(system: str, user: str, max_tokens: int = 600, *, agent_name: str = "unknown") -> str:
|
|
35
|
+
"""Single-turn LLM call. Returns the model's text response."""
|
|
36
|
+
model = _model()
|
|
37
|
+
log.info("LLM CALL → model=%s | agent=%s | input_chars=%d", model, agent_name, len(user))
|
|
38
|
+
response = _get_client().models.generate_content(
|
|
39
|
+
model=model,
|
|
40
|
+
contents=user,
|
|
41
|
+
config=types.GenerateContentConfig(
|
|
42
|
+
system_instruction=system,
|
|
43
|
+
max_output_tokens=max_tokens,
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
text = (response.text or "(no response)").strip()
|
|
47
|
+
log.info("LLM DONE ✓ model=%s | agent=%s | output_chars=%d", model, agent_name, len(text))
|
|
48
|
+
return text
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# --- agent runners ---------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
def _run_web_researcher(query: str) -> str:
|
|
54
|
+
return _llm(
|
|
55
|
+
system=(
|
|
56
|
+
"You are a concise research assistant. Given a topic, return 3 bullet points "
|
|
57
|
+
"of factual, well-known information about it. No fluff. Use markdown."
|
|
58
|
+
),
|
|
59
|
+
user=query.strip() or "Stockholm fintech startups in agent payments",
|
|
60
|
+
agent_name="web_researcher",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _run_text_analyst(text: str) -> str:
|
|
65
|
+
return _llm(
|
|
66
|
+
system=(
|
|
67
|
+
"You are a text analyst. Given a piece of text, return: (1) sentiment with a "
|
|
68
|
+
"confidence score 0-1, (2) 2-3 key themes, (3) one suggested action. "
|
|
69
|
+
"Markdown bullets only."
|
|
70
|
+
),
|
|
71
|
+
user=text.strip() or "I love this product but the onboarding is a bit confusing.",
|
|
72
|
+
agent_name="text_analyst",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _run_code_reviewer(diff: str) -> str:
|
|
77
|
+
return _llm(
|
|
78
|
+
system=(
|
|
79
|
+
"You are a senior code reviewer. Given a code snippet or diff, return a "
|
|
80
|
+
"terse review: 🟢 strengths, 🟡 nits, 🔴 bugs. Max 6 bullets total."
|
|
81
|
+
),
|
|
82
|
+
user=diff.strip() or "def add(a, b):\n return a + b # TODO: handle floats",
|
|
83
|
+
agent_name="code_reviewer",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _run_image_describer(prompt: str) -> str:
|
|
88
|
+
return _llm(
|
|
89
|
+
system=(
|
|
90
|
+
"You are an image-description assistant. The user describes an image in words. "
|
|
91
|
+
"Return a 3-bullet refinement covering subject, composition, and mood."
|
|
92
|
+
),
|
|
93
|
+
user=prompt.strip() or "A cat sitting on a windowsill at golden hour",
|
|
94
|
+
agent_name="image_describer",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class Agent:
|
|
100
|
+
slug: str
|
|
101
|
+
name: str
|
|
102
|
+
blurb: str
|
|
103
|
+
icon: str
|
|
104
|
+
price_usd: float # display only; real billing is per the SolvaPay product config
|
|
105
|
+
run: Callable[[str], str]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
AGENTS: list[Agent] = [
|
|
109
|
+
Agent("web_researcher", "Web Researcher", "3-bullet brief on any topic.", "🔎", 0.02, _run_web_researcher),
|
|
110
|
+
Agent("text_analyst", "Text Analyst", "Sentiment + themes for any text.", "📊", 0.05, _run_text_analyst),
|
|
111
|
+
Agent("code_reviewer", "Code Reviewer", "Senior-style review of a snippet. (Decorator-gated.)", "🧑💻", 0.10, _run_code_reviewer),
|
|
112
|
+
Agent("image_describer", "Image Describer", "Refines an image description.", "🖼️", 0.03, _run_image_describer),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
AGENTS_BY_SLUG: dict[str, Agent] = {a.slug: a for a in AGENTS}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def example_input_for(slug: str) -> str:
|
|
119
|
+
return {
|
|
120
|
+
"web_researcher": "Stockholm fintech startups working on AI agent payments",
|
|
121
|
+
"text_analyst": "I love this product but the onboarding is a bit confusing.",
|
|
122
|
+
"code_reviewer": "def divide(a, b):\n return a / b # TODO",
|
|
123
|
+
"image_describer": "A short-haired tabby cat on a windowsill at golden hour",
|
|
124
|
+
}.get(slug, "")
|