solvapay-python 0.6.0__tar.gz → 0.7.1__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.
Files changed (69) hide show
  1. solvapay_python-0.7.1/CHANGELOG.md +53 -0
  2. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/PKG-INFO +15 -5
  3. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/README.md +10 -3
  4. solvapay_python-0.7.1/examples/marketplace/.env.example +15 -0
  5. solvapay_python-0.7.1/examples/marketplace/.streamlit/config.toml +16 -0
  6. solvapay_python-0.7.1/examples/marketplace/PLAN.md +108 -0
  7. solvapay_python-0.7.1/examples/marketplace/README.md +67 -0
  8. solvapay_python-0.7.1/examples/marketplace/agents.py +124 -0
  9. solvapay_python-0.7.1/examples/marketplace/app.py +259 -0
  10. solvapay_python-0.7.1/examples/marketplace/demo_customers.py +27 -0
  11. solvapay_python-0.7.1/examples/marketplace/requirements.txt +4 -0
  12. solvapay_python-0.7.1/examples/marketplace/sdk_gateway.py +85 -0
  13. solvapay_python-0.7.1/examples/marketplace/ui_components.py +155 -0
  14. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/pyproject.toml +5 -2
  15. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/src/solvapay/__init__.py +23 -2
  16. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/src/solvapay/_async_client.py +41 -9
  17. solvapay_python-0.7.1/src/solvapay/_http.py +220 -0
  18. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/src/solvapay/client.py +36 -8
  19. solvapay_python-0.7.1/src/solvapay/exceptions.py +86 -0
  20. solvapay_python-0.7.1/src/solvapay/idempotency.py +16 -0
  21. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/src/solvapay/models.py +31 -5
  22. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/src/solvapay/paywall.py +20 -2
  23. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/src/solvapay/paywall_state.py +67 -1
  24. solvapay_python-0.7.1/tests/__init__.py +0 -0
  25. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_customer.py +52 -0
  26. solvapay_python-0.7.1/tests/test_errors.py +113 -0
  27. solvapay_python-0.7.1/tests/test_idempotency.py +62 -0
  28. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_lifecycle.py +20 -4
  29. solvapay_python-0.7.1/tests/test_packaging.py +9 -0
  30. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_paywall_state.py +75 -0
  31. solvapay_python-0.7.1/tests/test_redaction.py +57 -0
  32. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/uv.lock +2 -2
  33. solvapay_python-0.6.0/src/solvapay/_http.py +0 -107
  34. solvapay_python-0.6.0/src/solvapay/exceptions.py +0 -16
  35. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/.github/workflows/ci.yml +0 -0
  36. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/.github/workflows/publish.yml +0 -0
  37. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/.gitignore +0 -0
  38. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/.python-version +0 -0
  39. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/LICENSE +0 -0
  40. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/fastmcp-paywall/.env.example +0 -0
  41. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/fastmcp-paywall/.gitignore +0 -0
  42. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/fastmcp-paywall/README.md +0 -0
  43. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/fastmcp-paywall/claim.py +0 -0
  44. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/fastmcp-paywall/pyproject.toml +0 -0
  45. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/fastmcp-paywall/server.py +0 -0
  46. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/fastmcp-paywall/uv.lock +0 -0
  47. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/langchain-paywall/.env.example +0 -0
  48. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/langchain-paywall/.gitignore +0 -0
  49. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/langchain-paywall/README.md +0 -0
  50. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/langchain-paywall/agent.py +0 -0
  51. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/examples/langchain-paywall/pyproject.toml +0 -0
  52. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/src/solvapay/_config.py +0 -0
  53. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/src/solvapay/events.py +0 -0
  54. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/src/solvapay/fastapi.py +0 -0
  55. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/src/solvapay/langchain.py +0 -0
  56. /solvapay_python-0.6.0/tests/__init__.py → /solvapay_python-0.7.1/src/solvapay/py.typed +0 -0
  57. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/src/solvapay/webhooks.py +0 -0
  58. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/conftest.py +0 -0
  59. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_admin.py +0 -0
  60. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_async_client.py +0 -0
  61. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_checkout.py +0 -0
  62. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_config.py +0 -0
  63. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_http.py +0 -0
  64. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_invariants.py +0 -0
  65. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_langchain.py +0 -0
  66. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_limits.py +0 -0
  67. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_paywall.py +0 -0
  68. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_webhook_events.py +0 -0
  69. {solvapay_python-0.6.0 → solvapay_python-0.7.1}/tests/test_webhooks.py +0 -0
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ ## 0.7.1 — 2026-05-17
4
+
5
+ Payments-grade hardening: structured errors, idempotency keys, py.typed, structured logging.
6
+
7
+ ### Added
8
+ - **Structured error hierarchy** (`exceptions.py`): `APIError` base with `status_code`, `request_id`, `error_code`, `error_message`. Subclasses: `AuthenticationError` (401), `PermissionError` (403), `NotFoundError` (404), `RateLimitError` (429, adds `.retry_after`), `InvalidRequestError` (4xx), `APIServerError` (5xx), `APIConnectionError`, `APITimeoutError`. `SolvaPayAPIError` aliased to `APIError` for back-compat.
9
+ - **Idempotency keys** on all mutating ops: `create_checkout_session`, `ensure_customer`, `track_usage`, `cancel_purchase`, `reactivate_purchase`, `create_product`, `clone_product`, `create_plan` all accept `idempotency_key: str | None = None`. Header casing: `Idempotency-Key` (matches TS SDK).
10
+ - **`solvapay.idempotency.from_payload(*parts)`** — SHA256 of stable-serialized payload parts → 32-hex-char deterministic key.
11
+ - **PEP 561 `py.typed` marker** — downstream mypy users no longer see `Any` on `solvapay.*` imports.
12
+ - **Structured logging** at `solvapay.http` logger: INFO on success (method, path, status, request_id, duration_ms), WARNING on 4xx/5xx (adds body_excerpt ≤200 chars). Never calls `logging.basicConfig`. Optional `logger=` injection on `SolvaPay`/`AsyncSolvaPay` constructors for `loguru`/`structlog`.
13
+ - **Secret redaction**: `Authorization` header never appears in logs. Verified by `tests/test_redaction.py`.
14
+ - **pyproject classifiers**: `Development Status :: 4 - Beta`, `Framework :: AsyncIO`, `Topic :: Office/Business :: Financial`, `Typing :: Typed`.
15
+
16
+ ### Internal
17
+ - User-Agent bumped to `solvapay-python/0.7.1`
18
+ - 142 tests (up from 125), `mypy --strict` clean, `ruff` clean
19
+
20
+ ## 0.7.0 — 2026-05-17
21
+
22
+ Real-API alignment after testing against the SolvaPay sandbox revealed wire-format mismatches.
23
+
24
+ ### Fixed
25
+ - **`Customer.customer_ref`** now accepts `reference` (real API) in addition to `customerRef` via `validation_alias=AliasChoices(...)`.
26
+ - **`ensure_customer()`** reads `reference` from API response; falls back to `customerRef`. Raises if neither present.
27
+ - **`BalanceResponse`** rewritten: `credits`, `display_currency`, `credits_per_minor_unit`, `display_exchange_rate`; `balance` and `currency` kept as computed properties.
28
+
29
+ ### Added
30
+ - **`paywall_state.gate()`** — one-call enrichment helper (limits + checkout URL + plan via `get_customer`).
31
+ - **`paywall.require` / `require_async`**: auto-mints checkout URL when `LimitResponse.checkout_url is None`.
32
+ - **`examples/marketplace/`** — Streamlit demo with 4 paywalled AI agents (Google Gemini) against real SolvaPay sandbox.
33
+
34
+ ### Internal
35
+ - User-Agent `solvapay-python/0.7.0`. 125 tests (up from 121). `mypy --strict` clean.
36
+
37
+ ## 0.6.0
38
+ - Admin endpoints (products, plans, merchant config). GitHub Actions trusted-publish to PyPI.
39
+
40
+ ## 0.5.0
41
+ - `paywall_state` classifier (ACTIVATION_REQUIRED / TOPUP_REQUIRED / UPGRADE_REQUIRED). LangChain `monetize_tool`.
42
+
43
+ ## 0.4.0
44
+ - Full async client. 5 lifecycle operations. 13 typed webhook event classes.
45
+
46
+ ## 0.3.0
47
+ - FastMCP example: AI agent with two paywalled tools.
48
+
49
+ ## 0.2.0
50
+ - `@paywall.require` decorator. FastAPI webhook router.
51
+
52
+ ## 0.1.0
53
+ - 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.6.0
3
+ Version: 0.7.1
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
@@ -9,14 +9,17 @@ Author: Dhruv Sanan
9
9
  License: MIT
10
10
  License-File: LICENSE
11
11
  Keywords: agents,fintech,mcp,payments,solvapay
12
- Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: AsyncIO
13
14
  Classifier: Intended Audience :: Developers
14
15
  Classifier: License :: OSI Approved :: MIT License
15
16
  Classifier: Programming Language :: Python :: 3
16
17
  Classifier: Programming Language :: Python :: 3.10
17
18
  Classifier: Programming Language :: Python :: 3.11
18
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Office/Business :: Financial
19
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
20
23
  Requires-Python: >=3.10
21
24
  Requires-Dist: httpx>=0.27
22
25
  Requires-Dist: pydantic>=2.5
@@ -28,20 +31,26 @@ Description-Content-Type: text/markdown
28
31
 
29
32
  # solvapay-python
30
33
 
34
+ [![PyPI](https://img.shields.io/pypi/v/solvapay-python)](https://pypi.org/project/solvapay-python/)
35
+
31
36
  Community Python SDK for [SolvaPay](https://solvapay.com) — payment rails for the agentic economy.
32
37
 
33
- > **Status:** v0.5, community-maintained. Pending official adoption.
38
+ > **Status:** v0.6, community-maintained. Available on PyPI. Pending official adoption.
34
39
  > Mirrors the most-used surface of [@solvapay/core](https://github.com/solvapay/solvapay-sdk).
35
40
 
36
41
  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
42
 
38
- > 🎬 **New in v0.5:** Paywall state classifier (`paywall_state` module) and LangChain `monetize_tool` decorator gate any LangChain tool behind a SolvaPay paywall with one line.
43
+ > **New in v0.6:** Admin endpoints (products, plans, merchant, platform config). Published to PyPI.
44
+ > **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
45
  > **v0.4:** Async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events.
40
46
 
41
47
  ## Install
42
48
 
43
49
  ```bash
44
- pip install git+https://github.com/dhruv-sanan/solvapay-python
50
+ pip install solvapay-python
51
+ # with optional extras:
52
+ pip install solvapay-python[langchain]
53
+ pip install solvapay-python[fastapi]
45
54
  ```
46
55
 
47
56
  ## Quickstart
@@ -251,6 +260,7 @@ async def handle_webhook(request: Request) -> dict:
251
260
  - v0.3 — FastMCP paywall demo (`examples/fastmcp-paywall/`) ✅
252
261
  - v0.4 — async client (`AsyncSolvaPay`), lifecycle ops, typed webhook events ✅
253
262
  - v0.5 — paywall state classifier, LangChain `monetize_tool` decorator ✅
263
+ - v0.6 — admin endpoints (products, plans, merchant, platform config), PyPI publish ✅
254
264
 
255
265
  ## Contributing
256
266
 
@@ -1,19 +1,25 @@
1
1
  # solvapay-python
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/solvapay-python)](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.5, community-maintained. Pending official adoption.
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
- > 🎬 **New in v0.5:** Paywall state classifier (`paywall_state` module) and LangChain `monetize_tool` decorator gate any LangChain tool behind a SolvaPay paywall with one line.
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 git+https://github.com/dhruv-sanan/solvapay-python
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, "")