solvent-agent 0.2.2__tar.gz → 0.3.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.
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/PKG-INFO +230 -4
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/README.md +219 -3
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/pyproject.toml +9 -4
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/__init__.py +1 -1
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/__main__.py +12 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/agent.py +3 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/alerts.py +14 -3
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/checkout.py +86 -17
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/cli.py +14 -8
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/dashboard.py +100 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/delivery.py +57 -31
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/doctor.py +38 -0
- solvent_agent-0.3.0/solvent/funnel.py +359 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/gateway.py +19 -9
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/guardrail_cmd.py +18 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/guardrails.py +197 -11
- solvent_agent-0.3.0/solvent/llm_backend.py +133 -0
- solvent_agent-0.3.0/solvent/nano_client.py +633 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/nemotron.py +11 -1
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/onboarding.py +26 -4
- solvent_agent-0.3.0/solvent/ops.py +246 -0
- solvent_agent-0.3.0/solvent/pdf.py +249 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/pricing.py +15 -5
- solvent_agent-0.3.0/solvent/products.py +398 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/quote_cmd.py +22 -5
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/server.py +94 -2
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/service.py +16 -1
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/simulate.py +16 -4
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/stages.py +182 -22
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/treasury.py +134 -8
- solvent_agent-0.3.0/solvent/turnaround.py +234 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/worker.py +4 -3
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent_agent.egg-info/PKG-INFO +230 -4
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent_agent.egg-info/SOURCES.txt +18 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent_agent.egg-info/requires.txt +14 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_checkout.py +57 -1
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_cli_routing.py +12 -0
- solvent_agent-0.3.0/tests/test_cli_summary.py +48 -0
- solvent_agent-0.3.0/tests/test_dashboard_commercial.py +101 -0
- solvent_agent-0.3.0/tests/test_funnel.py +200 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_guardrails.py +6 -4
- solvent_agent-0.3.0/tests/test_job_budget_approval.py +276 -0
- solvent_agent-0.3.0/tests/test_llm_backend.py +215 -0
- solvent_agent-0.3.0/tests/test_malformed_policy.py +59 -0
- solvent_agent-0.3.0/tests/test_nano_client.py +492 -0
- solvent_agent-0.3.0/tests/test_ops.py +214 -0
- solvent_agent-0.3.0/tests/test_pdf.py +265 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_pricing.py +33 -0
- solvent_agent-0.3.0/tests/test_products.py +295 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_route_security.py +1 -0
- solvent_agent-0.3.0/tests/test_supply_chain.py +40 -0
- solvent_agent-0.3.0/tests/test_turnaround.py +192 -0
- solvent_agent-0.3.0/tests/test_wizard_no_tty.py +46 -0
- solvent_agent-0.2.2/solvent/products.py +0 -209
- solvent_agent-0.2.2/tests/test_products.py +0 -129
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/LICENSE +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/setup.cfg +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/_filelock.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/access.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/backlog.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/calibration.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/capacity.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/channels/__init__.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/channels/telegram.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/chat.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/config.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/config_cmd.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/customers.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/dashboard_chat.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/event_hub.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/export.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/finance.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/init.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/intake.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/job_cmd.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/jobs.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/logs.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/memory.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/money.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/notifications.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/observability.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/optimize.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/pairing.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/paths.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/py.typed +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/qr.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/quality.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/queue.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/rate_limit.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/receipt.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/reconcile.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/review.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/security.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/status.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/stripe_client.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/templates/workspace/AGENTS.md +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/templates/workspace/BRAIN.md +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/templates/workspace/SOUL.md +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/templates/workspace/skills/commission-research/SKILL.md +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/tools.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/upgrade.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/webhook_log.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent/workspace.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent_agent.egg-info/dependency_links.txt +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent_agent.egg-info/entry_points.txt +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/solvent_agent.egg-info/top_level.txt +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_agent.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_alerts.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_backlog.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_calibration.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_capacity.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_chat_tools.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_cogs_reconciliation.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_config.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_config_cmd.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_counter_offer.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_customers.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_dashboard_chat.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_dashboard_financials.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_dashboard_ops_xss.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_dashboard_security.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_delivery.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_doctor.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_event_hub.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_export.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_filelock.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_finance.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_gateway.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_init.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_intake.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_job_cmd.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_jobs.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_logs.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_memory.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_metrics_block.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_money.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_nemotron_parse.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_notifications.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_observability.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_onboarding.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_optimize.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_packaging.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_paths.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_qr.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_quality.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_queue.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_rate_limit.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_receipt.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_reconcile.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_research_brief.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_retry.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_review.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_runtime_config_paths.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_security.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_server.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_server_job_submission.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_simulate.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_spend_policy.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_stages.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_status.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_stripe_client.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_telegram_pairing.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_tools.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_treasury.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_upgrade.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_upgrade_hint.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_webhook_log.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_webhook_security.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_worker.py +0 -0
- {solvent_agent-0.2.2 → solvent_agent-0.3.0}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solvent-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A self-funding analyst agent: earns on Stripe, fulfils with NVIDIA Nemotron, spends within guardrails.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/ianalloway/solvent-agent
|
|
@@ -21,13 +21,19 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Provides-Extra: stripe
|
|
23
23
|
Requires-Dist: stripe>=9.0.0; extra == "stripe"
|
|
24
|
+
Requires-Dist: urllib3>=2.8.0; extra == "stripe"
|
|
25
|
+
Requires-Dist: certifi>=2026.10.19; extra == "stripe"
|
|
24
26
|
Provides-Extra: serve
|
|
25
27
|
Requires-Dist: fastapi>=0.110.0; extra == "serve"
|
|
26
28
|
Requires-Dist: uvicorn[standard]>=0.27.0; extra == "serve"
|
|
29
|
+
Requires-Dist: anyio>=4.14.2; extra == "serve"
|
|
27
30
|
Provides-Extra: qr
|
|
28
31
|
Requires-Dist: qrcode[pil]>=7.0; extra == "qr"
|
|
32
|
+
Requires-Dist: pillow>=13.0.0; python_version >= "3.11" and extra == "qr"
|
|
29
33
|
Provides-Extra: telegram
|
|
30
34
|
Requires-Dist: python-telegram-bot>=21.0; extra == "telegram"
|
|
35
|
+
Requires-Dist: anyio>=4.14.2; extra == "telegram"
|
|
36
|
+
Requires-Dist: certifi>=2026.10.19; extra == "telegram"
|
|
31
37
|
Provides-Extra: dev
|
|
32
38
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
33
39
|
Requires-Dist: ruff>=0.3; extra == "dev"
|
|
@@ -37,6 +43,10 @@ Requires-Dist: fastapi>=0.110.0; extra == "all"
|
|
|
37
43
|
Requires-Dist: uvicorn[standard]>=0.27.0; extra == "all"
|
|
38
44
|
Requires-Dist: qrcode[pil]>=7.0; extra == "all"
|
|
39
45
|
Requires-Dist: python-telegram-bot>=21.0; extra == "all"
|
|
46
|
+
Requires-Dist: urllib3>=2.8.0; extra == "all"
|
|
47
|
+
Requires-Dist: certifi>=2026.10.19; extra == "all"
|
|
48
|
+
Requires-Dist: anyio>=4.14.2; extra == "all"
|
|
49
|
+
Requires-Dist: pillow>=13.0.0; python_version >= "3.11" and extra == "all"
|
|
40
50
|
Dynamic: license-file
|
|
41
51
|
|
|
42
52
|
<div align="center">
|
|
@@ -47,6 +57,7 @@ Dynamic: license-file
|
|
|
47
57
|
|
|
48
58
|
[](https://github.com/ianalloway/solvent-agent/actions/workflows/ci.yml)
|
|
49
59
|
[](https://pypi.org/project/solvent-agent/)
|
|
60
|
+
[](https://huggingface.co/spaces/ianalloway/solvent-agent-demo)
|
|
50
61
|
[](https://www.python.org/downloads/)
|
|
51
62
|
[](pyproject.toml)
|
|
52
63
|
[](LICENSE)
|
|
@@ -59,6 +70,11 @@ Dynamic: license-file
|
|
|
59
70
|
|
|
60
71
|
<sub>Recorded from the real CLI with no API keys and no network access: the Stripe checkout links and Nemotron output are the built-in offline stubs, and the dollar figures are simulated.</sub>
|
|
61
72
|
|
|
73
|
+
**[▶ Try it in your browser](https://huggingface.co/spaces/ianalloway/solvent-agent-demo)**: the real package running in Pyodide, with no install and no keys.
|
|
74
|
+
|
|
75
|
+
[](https://huggingface.co/spaces/ianalloway/solvent-agent-demo)
|
|
76
|
+
|
|
77
|
+
<sub>All dollar figures, including the final cash balance, are simulated.</sub>
|
|
62
78
|
</div>
|
|
63
79
|
|
|
64
80
|
> **Demo by default.** With no keys set, `solvent` runs an **offline, zero-key simulation** of the whole loop. Dollar figures in the demo are illustrative, **not production revenue**. Stripe test-mode Payment Links and live NVIDIA Nemotron are opt-in; see [Make It Real](#-make-it-real).
|
|
@@ -196,7 +212,10 @@ A typical **offline demo** batch (illustrative numbers from the simulated run
|
|
|
196
212
|
BOOK P&L ──▶ treasury updated · dashboard refreshed
|
|
197
213
|
```
|
|
198
214
|
|
|
199
|
-
|
|
215
|
+
The job pipeline checks payment before fulfilment and screens vendor payments
|
|
216
|
+
against the configured spend policy. Quotes use estimated costs, so an accepted
|
|
217
|
+
quote is a projected margin, not a guarantee of realized profit. The ledger and
|
|
218
|
+
cost-calibration reports show how actual spending compares with that estimate.
|
|
200
219
|
|
|
201
220
|
---
|
|
202
221
|
|
|
@@ -218,6 +237,11 @@ Revenue is **always collected before cost is incurred**, and no payment can viol
|
|
|
218
237
|
- **Offline-first** — without API keys the demo runs on deterministic stubs. Add `NVIDIA_API_KEY` + `STRIPE_API_KEY=sk_test_...` to unlock live inference and real Payment Links.
|
|
219
238
|
- **Audit trail** — every `cs_...` checkout session ID and `pi_...` PaymentIntent ID is recorded on the ledger before fulfilment begins.
|
|
220
239
|
|
|
240
|
+
Resource prices can be overridden in `.solvent/pricing_overrides.json`. Values
|
|
241
|
+
are costs in cents and must be finite, non-negative numbers; `0` is valid for a
|
|
242
|
+
free resource. Invalid entries (including negative values, booleans, `NaN`, and
|
|
243
|
+
infinity) keep the built-in price. Valid entries in the same file still apply.
|
|
244
|
+
|
|
221
245
|
---
|
|
222
246
|
|
|
223
247
|
## 🎮 Running Modes
|
|
@@ -295,6 +319,8 @@ python3 -m solvent export --since 30d # ledger/jobs/metrics/customers + peri
|
|
|
295
319
|
python3 -m solvent products # the price list, checked against current cost
|
|
296
320
|
python3 -m solvent quality # scores the quality gate gave shipped briefs
|
|
297
321
|
python3 -m solvent review # approve/reject the jobs intake held for a human
|
|
322
|
+
python3 -m solvent funnel --since 7d # win rate: submitted → quoted → paid → delivered
|
|
323
|
+
python3 -m solvent turnaround # measured paid-to-delivered time, and the promise
|
|
298
324
|
python3 -m solvent reconcile --since 7d # Stripe ↔ ledger drift check
|
|
299
325
|
python3 -m solvent finance # income statement, unit economics, runway
|
|
300
326
|
python3 -m solvent finance --json # machine-readable report
|
|
@@ -376,6 +402,65 @@ directory, move them into this resolved directory before upgrading.
|
|
|
376
402
|
rolling window is used, per-vendor exposure against its cap, and every spend
|
|
377
403
|
the policy blocked.
|
|
378
404
|
|
|
405
|
+
### Spending controls — per-job envelopes and human approval
|
|
406
|
+
|
|
407
|
+
Two more rules sit on top of the daily, per-vendor and velocity limits. Both
|
|
408
|
+
run inside the same deterministic guardrail check, before any Stripe call, and
|
|
409
|
+
both fail closed.
|
|
410
|
+
|
|
411
|
+
| Rule | Decision rule id | What happens |
|
|
412
|
+
|---|---|---|
|
|
413
|
+
| **Per-job budget envelope** | `job_budget` | Every job has a hard spend cap in integer cents (`job_budget_cents`, default 15000 = $150; `0` turns it off). Spend up to *exactly* the cap is allowed; anything that would go past it is refused with "job '…' budget exhausted". Refunds don't count against it. |
|
|
414
|
+
| **Human-approval hook** | `approval_pending` / `approval_denied` | A spend *strictly above* `approval_threshold_cents` is not paid. It goes to a pluggable approver, which answers `"approved"`, `"denied"` or `"held"`. The default approver always holds. An approver that raises an error or returns anything else also holds. Approval cannot override a hard rule such as the job envelope. |
|
|
415
|
+
|
|
416
|
+
Configure them in `.solvent/spend_policy.json`:
|
|
417
|
+
|
|
418
|
+
```json
|
|
419
|
+
{ "job_budget_cents": 8000, "approval_threshold_cents": 2500 }
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
or with the environment variables `SOLVENT_JOB_BUDGET_CENTS` and
|
|
423
|
+
`SOLVENT_APPROVAL_THRESHOLD_CENTS`. Env vars can only **tighten** these
|
|
424
|
+
settings: a lower budget or a lower threshold. An env value that would raise
|
|
425
|
+
or disable a limit (for example `SOLVENT_JOB_BUDGET_CENTS=0`) is ignored.
|
|
426
|
+
Loosening or disabling takes an explicit policy-file setting, and any override
|
|
427
|
+
that loosens or disables a limit, or is ignored, is logged as a warning and
|
|
428
|
+
shown in `solvent guardrails`. The approval hook is off unless a threshold is
|
|
429
|
+
set.
|
|
430
|
+
|
|
431
|
+
A spend evaluated without a job id is not exempt from the budget. All unscoped
|
|
432
|
+
spends share one `unscoped` envelope under the same cap.
|
|
433
|
+
|
|
434
|
+
Wire in your own approver (a Slack prompt, a ticket queue, a CLI confirm):
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
from solvent.guardrails import ApprovalRequest, Guardrails
|
|
438
|
+
|
|
439
|
+
def approver(req: ApprovalRequest) -> str:
|
|
440
|
+
return "approved" if req.vendor == "market-data-api" else "held"
|
|
441
|
+
|
|
442
|
+
guard = Guardrails(treasury, approver=approver)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
A held spend stops the job and emits a `spend_held` event, so nothing moves
|
|
446
|
+
until a human decides.
|
|
447
|
+
|
|
448
|
+
**How this relates to the card networks.** The pattern is close to what the
|
|
449
|
+
networks describe publicly. Mastercard's
|
|
450
|
+
[Agent Pay for Machines](https://www.mastercard.com/global/en/news-and-trends/press/2026/october/Mastercard-Agent-Pay-for-Machines-in-action.html)
|
|
451
|
+
gives an agent "a job and a spending limit", and Mastercard's
|
|
452
|
+
[agentic token framework](https://www.mastercard.com/us/en/news-and-trends/stories/2025/agentic-commerce-framework.html)
|
|
453
|
+
carries transaction limits and validity windows. In the
|
|
454
|
+
[Visa, BILL and Billtrust pilot](https://corporate.visa.com/en/sites/visa-perspectives/newsroom/visa-bill-billtrust-launch-pilot.html),
|
|
455
|
+
the agent only pays invoices the buyer has already approved. Stripe's
|
|
456
|
+
[Issuing for agents](https://docs.stripe.com/issuing/agents) offers per-card and
|
|
457
|
+
per-cardholder
|
|
458
|
+
[spending controls](https://docs.stripe.com/issuing/controls/spending-controls)
|
|
459
|
+
and real-time authorization webhooks. Those controls run at the card or network
|
|
460
|
+
layer. Solvent's run inside the agent, before it calls any payment API, so they
|
|
461
|
+
work in simulation and with any payment backend. They complement network-level
|
|
462
|
+
controls and don't replace them.
|
|
463
|
+
|
|
379
464
|
### Backlog — which job to work on next
|
|
380
465
|
|
|
381
466
|
A queue is not a plan. When several jobs are open and both cash and the 24h
|
|
@@ -524,6 +609,102 @@ a single JSON document, over a period given as `7d`, `24h`, `2w`, `3m` or
|
|
|
524
609
|
revenue and cost counted when the money moved — so the printed summary ties out
|
|
525
610
|
against the rows it ships, with refunds separated from cost of sales.
|
|
526
611
|
|
|
612
|
+
### How long it takes
|
|
613
|
+
|
|
614
|
+
"When will I get it?" is the second question every customer asks, and the agent
|
|
615
|
+
had no answer. Fulfilment seconds were recorded, but that is only the model's
|
|
616
|
+
part — what the customer experiences is the wait from *paying* to *having the
|
|
617
|
+
brief*, across the queue, the worker's poll, retries and delivery.
|
|
618
|
+
|
|
619
|
+
That wait is already on the record (the ledger says when the money arrived, the
|
|
620
|
+
event log says when the brief went out), so `solvent turnaround` measures it
|
|
621
|
+
rather than estimating, and turns the distribution into something quotable: a
|
|
622
|
+
promise at a chosen percentile, rounded up, never faster than a floor. The
|
|
623
|
+
promise then appears on accepted quotes:
|
|
624
|
+
|
|
625
|
+
```
|
|
626
|
+
Verdict ACCEPT — accepted
|
|
627
|
+
Turnaround within 5 minutes
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
It also retires an arbitrary threshold. `alerts.py` called a paid job "stuck"
|
|
631
|
+
after two hours because two hours sounded reasonable; a shop that normally
|
|
632
|
+
delivers in forty seconds should not wait two hours to notice, and one that
|
|
633
|
+
takes a day should not cry wolf. With enough history, late is defined by the
|
|
634
|
+
shop's own p95 — and only until then by the fixed hours.
|
|
635
|
+
|
|
636
|
+
### Running it from Telegram
|
|
637
|
+
|
|
638
|
+
Fifteen commands' worth of reporting lived in the CLI, which is where the
|
|
639
|
+
operator is not. The gateway's slash commands now cover the business too —
|
|
640
|
+
`/funnel`, `/customers`, `/capacity`, `/alerts`, `/products`, `/quality`,
|
|
641
|
+
`/review`, `/costs` — each a short read over local state, no model call, no
|
|
642
|
+
spend.
|
|
643
|
+
|
|
644
|
+
The catch is who is asking. Any *paired* Telegram user reaches that handler,
|
|
645
|
+
and a customer is not entitled to the agent's win rate or cash position, so
|
|
646
|
+
these are operator-only:
|
|
647
|
+
|
|
648
|
+
- the **dashboard** channel is the operator by construction — it sits behind
|
|
649
|
+
`SOLVENT_DASHBOARD_TOKEN`;
|
|
650
|
+
- a Telegram id in `SOLVENT_OPERATOR_IDS` is the operator;
|
|
651
|
+
- anyone else gets "That command is operator-only", and `/help` does not list
|
|
652
|
+
commands they cannot run.
|
|
653
|
+
|
|
654
|
+
### The deliverable
|
|
655
|
+
|
|
656
|
+
Every job has been billed `pdf-render-saas` since the first commit, and no PDF
|
|
657
|
+
was ever produced — the agent was paying itself for a deliverable it did not
|
|
658
|
+
deliver. It renders one now: markdown, HTML **and** PDF, written beside each
|
|
659
|
+
other in `data/reports/`, linked from the delivery email and served from the
|
|
660
|
+
same token-gated route as the brief itself (`&format=pdf`), so nothing new is
|
|
661
|
+
exposed. With SMTP configured the PDF is attached to the email.
|
|
662
|
+
|
|
663
|
+
The renderer is `solvent/pdf.py`, about 250 lines of stdlib: a PDF is a text
|
|
664
|
+
container with a byte-offset table, so a single-font document needs no library,
|
|
665
|
+
only exact xref offsets. Base-14 Helvetica means nothing is embedded and a
|
|
666
|
+
brief weighs a couple of kilobytes. A render failure is caught and logged
|
|
667
|
+
rather than raised — losing a brief the customer has paid for would be far
|
|
668
|
+
worse than shipping it without the PDF.
|
|
669
|
+
|
|
670
|
+
### Knowing the win rate
|
|
671
|
+
|
|
672
|
+
**`solvent funnel`.** The treasury has always known the margin on the jobs it
|
|
673
|
+
took; it never knew the win rate. Those are different businesses — a 90% margin
|
|
674
|
+
on one job in ten is a worse shop than a 60% margin on eight, and nothing here
|
|
675
|
+
could tell them apart.
|
|
676
|
+
|
|
677
|
+
Every stage already leaves a trace, so the funnel is a read over data the agent
|
|
678
|
+
keeps anyway: submitted → quoted → accepted → checkout → paid → delivered. What
|
|
679
|
+
falls out at each step is the point, and the three failures want three
|
|
680
|
+
different fixes:
|
|
681
|
+
|
|
682
|
+
| Exit | What it means |
|
|
683
|
+
|---|---|
|
|
684
|
+
| `screened` | held before pricing — cost nothing |
|
|
685
|
+
| `declined` | a customer who wanted to pay and was turned away by the margin gate |
|
|
686
|
+
| `abandoned` | a customer who said yes at checkout and then did not pay |
|
|
687
|
+
| `refunded` | paid, then refunded when fulfilment failed — the expensive loss |
|
|
688
|
+
|
|
689
|
+
Lost pipeline is reported at the price the customer named, because that is the
|
|
690
|
+
money that was on the table. Win rate is broken down by product and price band,
|
|
691
|
+
and counter-offers are tracked separately, so "does the counter-offer actually
|
|
692
|
+
convert?" finally has an answer.
|
|
693
|
+
|
|
694
|
+
```
|
|
695
|
+
Submitted 9 ████████████████████████ 100.0%
|
|
696
|
+
Accepted by the gate 6 ████████████████········ 66.7%
|
|
697
|
+
Paid 5 █████████████··········· 55.6%
|
|
698
|
+
|
|
699
|
+
Gate acceptance 85.7% · payment conversion 83.3%
|
|
700
|
+
Won $373.00 · left on the table $9,130.00
|
|
701
|
+
Counter-offers 1 made, 0 won (0.0%)
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
The same picture now has a **Commercial** panel in the HTML dashboard — win
|
|
705
|
+
rate, customer book, brief quality and the throughput ceiling beside the P&L —
|
|
706
|
+
so the business reads in one place instead of across six commands.
|
|
707
|
+
|
|
527
708
|
### Selling, not just quoting
|
|
528
709
|
|
|
529
710
|
**`solvent products` — a price list.** A shop that cannot say what it charges
|
|
@@ -538,8 +719,29 @@ curl -X POST localhost:8787/jobs -d '{"topic": "...", "product": "deep-dive", ..
|
|
|
538
719
|
|
|
539
720
|
The catalogue is priced against *current* costs — calibration included — so a
|
|
540
721
|
list price that has quietly stopped clearing the margin floor shows up as a
|
|
541
|
-
stale price rather than as a run of unprofitable work.
|
|
542
|
-
|
|
722
|
+
stale price rather than as a run of unprofitable work.
|
|
723
|
+
|
|
724
|
+
Changing what the shop sells is a business decision, not a code change, so the
|
|
725
|
+
built-in list is overridable in `.solvent/products.json` beside the spend and
|
|
726
|
+
intake policies. Entries merge by slug — naming only a price keeps the built-in
|
|
727
|
+
scope — `retire` drops a product, and `replace: true` makes the file the whole
|
|
728
|
+
list:
|
|
729
|
+
|
|
730
|
+
```json
|
|
731
|
+
{
|
|
732
|
+
"products": [
|
|
733
|
+
{ "slug": "express", "list_price_cents": 3500 },
|
|
734
|
+
{ "slug": "retainer", "name": "Monthly retainer", "list_price_cents": 49900,
|
|
735
|
+
"est_tokens": 60000, "market_data_calls": 12, "web_search_calls": 40 }
|
|
736
|
+
],
|
|
737
|
+
"retire": ["deep-dive"]
|
|
738
|
+
}
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
A malformed entry is dropped rather than sold — a half-read product is a price
|
|
742
|
+
nobody agreed to — and `solvent products --validate` reports it. The command
|
|
743
|
+
exits non-zero when any product has gone stale *or* the file is broken, so cron
|
|
744
|
+
catches either without reading the output.
|
|
543
745
|
|
|
544
746
|
**`solvent quality` — a gate on the product, not just the margin.** Every other
|
|
545
747
|
gate here asks whether the money is sound; this one asks whether the brief is.
|
|
@@ -595,6 +797,22 @@ With both keys set:
|
|
|
595
797
|
- Optional: set `STRIPE_WEBHOOK_SECRET` and forward `checkout.session.completed` events via `StripeClient.process_webhook()`.
|
|
596
798
|
- Optional: enable **Stripe Issuing** on your test account to provision capped single-use virtual debit cards for each vendor payment.
|
|
597
799
|
|
|
800
|
+
### Local or OpenAI-compatible model (Ollama, LM Studio, vLLM)
|
|
801
|
+
|
|
802
|
+
Briefs can be written by any OpenAI-compatible `/chat/completions` server instead of Nemotron. This is opt-in, uses only the standard library, and is ignored unless you set one of the variables below:
|
|
803
|
+
|
|
804
|
+
```bash
|
|
805
|
+
ollama pull llama3.2
|
|
806
|
+
export SOLVENT_LLM_MODEL=llama3.2 # base URL defaults to Ollama
|
|
807
|
+
# export SOLVENT_LLM_BASE_URL=http://localhost:11434/v1 # or any OpenAI-compatible URL
|
|
808
|
+
# export SOLVENT_LLM_API_KEY=... # optional; sent only as a Bearer header
|
|
809
|
+
solvent --no-onboard
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
If the server is down, slow (`SOLVENT_LLM_TIMEOUT`, default 30s), or returns an error or malformed reply, SOLVENT logs a warning and falls back to the deterministic offline brief, so the job still completes. The API key is never logged or written to a brief.
|
|
813
|
+
|
|
814
|
+
**API keys.** `SOLVENT_LLM_API_KEY` stays on your machine: it is read from the environment and sent only as a Bearer header to the configured `SOLVENT_LLM_BASE_URL`, never anywhere else. Redirects are refused rather than followed, so a 3xx can never forward the key to another host or downgrade it to `http://`; point the base URL at the final endpoint. Give each tool its own key with a spend cap, so one leaked or misbehaving key can only spend so much.
|
|
815
|
+
|
|
598
816
|
### Environment variables
|
|
599
817
|
|
|
600
818
|
| Variable | Purpose |
|
|
@@ -612,6 +830,10 @@ With both keys set:
|
|
|
612
830
|
| `SOLVENT_TELEGRAM_ALLOW_FROM` | Comma-separated Telegram user IDs for allowlist mode |
|
|
613
831
|
| `SOLVENT_PORT` | Port for the `serve` API server (default `8787`) |
|
|
614
832
|
| `SOLVENT_BASE_URL` | Base URL for hosted brief links and Stripe webhook callbacks |
|
|
833
|
+
| `SOLVENT_LLM_BASE_URL` | Opt-in OpenAI-compatible endpoint for briefs (default when enabled: `http://localhost:11434/v1`, Ollama) |
|
|
834
|
+
| `SOLVENT_LLM_MODEL` | Model name for that endpoint; setting it alone enables the backend (default `llama3.2`) |
|
|
835
|
+
| `SOLVENT_LLM_API_KEY` | Optional Bearer key for the endpoint; never logged |
|
|
836
|
+
| `SOLVENT_LLM_TIMEOUT` | Request timeout in seconds for that endpoint (default `30`) |
|
|
615
837
|
| `NEMOTRON_MODEL` | Nemotron model override (default: `nvidia/llama-3.1-nemotron-ultra-253b-v1`) |
|
|
616
838
|
| `SOLVENT_DELIVERY_SECRET` | HMAC token secret for `/briefs/{job_id}`; at least 32 characters, high entropy |
|
|
617
839
|
| `SOLVENT_SKIP_ONBOARD` | Set to `1` to skip the first-run wizard |
|
|
@@ -694,9 +916,13 @@ solvent/
|
|
|
694
916
|
products.py the price list (`solvent products`)
|
|
695
917
|
quality.py deliverable quality gate (`solvent quality`)
|
|
696
918
|
review.py operator approval queue (`solvent review`)
|
|
919
|
+
funnel.py order funnel and win rate (`solvent funnel`)
|
|
920
|
+
turnaround.py measured delivery time (`solvent turnaround`)
|
|
921
|
+
ops.py operator-only business commands on the chat surface
|
|
697
922
|
stripe_client.py two-sided Stripe layer (earn + spend)
|
|
698
923
|
nemotron.py NVIDIA Nemotron client (+ offline stub)
|
|
699
924
|
service.py the product: an on-demand research brief
|
|
925
|
+
pdf.py dependency-free PDF rendering of the brief
|
|
700
926
|
jobs.py sample inbound work
|
|
701
927
|
dashboard.py renders the treasury to HTML + JSON
|
|
702
928
|
finance.py income statement · unit economics · runway · forecast
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://github.com/ianalloway/solvent-agent/actions/workflows/ci.yml)
|
|
8
8
|
[](https://pypi.org/project/solvent-agent/)
|
|
9
|
+
[](https://huggingface.co/spaces/ianalloway/solvent-agent-demo)
|
|
9
10
|
[](https://www.python.org/downloads/)
|
|
10
11
|
[](pyproject.toml)
|
|
11
12
|
[](LICENSE)
|
|
@@ -18,6 +19,11 @@
|
|
|
18
19
|
|
|
19
20
|
<sub>Recorded from the real CLI with no API keys and no network access: the Stripe checkout links and Nemotron output are the built-in offline stubs, and the dollar figures are simulated.</sub>
|
|
20
21
|
|
|
22
|
+
**[▶ Try it in your browser](https://huggingface.co/spaces/ianalloway/solvent-agent-demo)**: the real package running in Pyodide, with no install and no keys.
|
|
23
|
+
|
|
24
|
+
[](https://huggingface.co/spaces/ianalloway/solvent-agent-demo)
|
|
25
|
+
|
|
26
|
+
<sub>All dollar figures, including the final cash balance, are simulated.</sub>
|
|
21
27
|
</div>
|
|
22
28
|
|
|
23
29
|
> **Demo by default.** With no keys set, `solvent` runs an **offline, zero-key simulation** of the whole loop. Dollar figures in the demo are illustrative, **not production revenue**. Stripe test-mode Payment Links and live NVIDIA Nemotron are opt-in; see [Make It Real](#-make-it-real).
|
|
@@ -155,7 +161,10 @@ A typical **offline demo** batch (illustrative numbers from the simulated run
|
|
|
155
161
|
BOOK P&L ──▶ treasury updated · dashboard refreshed
|
|
156
162
|
```
|
|
157
163
|
|
|
158
|
-
|
|
164
|
+
The job pipeline checks payment before fulfilment and screens vendor payments
|
|
165
|
+
against the configured spend policy. Quotes use estimated costs, so an accepted
|
|
166
|
+
quote is a projected margin, not a guarantee of realized profit. The ledger and
|
|
167
|
+
cost-calibration reports show how actual spending compares with that estimate.
|
|
159
168
|
|
|
160
169
|
---
|
|
161
170
|
|
|
@@ -177,6 +186,11 @@ Revenue is **always collected before cost is incurred**, and no payment can viol
|
|
|
177
186
|
- **Offline-first** — without API keys the demo runs on deterministic stubs. Add `NVIDIA_API_KEY` + `STRIPE_API_KEY=sk_test_...` to unlock live inference and real Payment Links.
|
|
178
187
|
- **Audit trail** — every `cs_...` checkout session ID and `pi_...` PaymentIntent ID is recorded on the ledger before fulfilment begins.
|
|
179
188
|
|
|
189
|
+
Resource prices can be overridden in `.solvent/pricing_overrides.json`. Values
|
|
190
|
+
are costs in cents and must be finite, non-negative numbers; `0` is valid for a
|
|
191
|
+
free resource. Invalid entries (including negative values, booleans, `NaN`, and
|
|
192
|
+
infinity) keep the built-in price. Valid entries in the same file still apply.
|
|
193
|
+
|
|
180
194
|
---
|
|
181
195
|
|
|
182
196
|
## 🎮 Running Modes
|
|
@@ -254,6 +268,8 @@ python3 -m solvent export --since 30d # ledger/jobs/metrics/customers + peri
|
|
|
254
268
|
python3 -m solvent products # the price list, checked against current cost
|
|
255
269
|
python3 -m solvent quality # scores the quality gate gave shipped briefs
|
|
256
270
|
python3 -m solvent review # approve/reject the jobs intake held for a human
|
|
271
|
+
python3 -m solvent funnel --since 7d # win rate: submitted → quoted → paid → delivered
|
|
272
|
+
python3 -m solvent turnaround # measured paid-to-delivered time, and the promise
|
|
257
273
|
python3 -m solvent reconcile --since 7d # Stripe ↔ ledger drift check
|
|
258
274
|
python3 -m solvent finance # income statement, unit economics, runway
|
|
259
275
|
python3 -m solvent finance --json # machine-readable report
|
|
@@ -335,6 +351,65 @@ directory, move them into this resolved directory before upgrading.
|
|
|
335
351
|
rolling window is used, per-vendor exposure against its cap, and every spend
|
|
336
352
|
the policy blocked.
|
|
337
353
|
|
|
354
|
+
### Spending controls — per-job envelopes and human approval
|
|
355
|
+
|
|
356
|
+
Two more rules sit on top of the daily, per-vendor and velocity limits. Both
|
|
357
|
+
run inside the same deterministic guardrail check, before any Stripe call, and
|
|
358
|
+
both fail closed.
|
|
359
|
+
|
|
360
|
+
| Rule | Decision rule id | What happens |
|
|
361
|
+
|---|---|---|
|
|
362
|
+
| **Per-job budget envelope** | `job_budget` | Every job has a hard spend cap in integer cents (`job_budget_cents`, default 15000 = $150; `0` turns it off). Spend up to *exactly* the cap is allowed; anything that would go past it is refused with "job '…' budget exhausted". Refunds don't count against it. |
|
|
363
|
+
| **Human-approval hook** | `approval_pending` / `approval_denied` | A spend *strictly above* `approval_threshold_cents` is not paid. It goes to a pluggable approver, which answers `"approved"`, `"denied"` or `"held"`. The default approver always holds. An approver that raises an error or returns anything else also holds. Approval cannot override a hard rule such as the job envelope. |
|
|
364
|
+
|
|
365
|
+
Configure them in `.solvent/spend_policy.json`:
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
{ "job_budget_cents": 8000, "approval_threshold_cents": 2500 }
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
or with the environment variables `SOLVENT_JOB_BUDGET_CENTS` and
|
|
372
|
+
`SOLVENT_APPROVAL_THRESHOLD_CENTS`. Env vars can only **tighten** these
|
|
373
|
+
settings: a lower budget or a lower threshold. An env value that would raise
|
|
374
|
+
or disable a limit (for example `SOLVENT_JOB_BUDGET_CENTS=0`) is ignored.
|
|
375
|
+
Loosening or disabling takes an explicit policy-file setting, and any override
|
|
376
|
+
that loosens or disables a limit, or is ignored, is logged as a warning and
|
|
377
|
+
shown in `solvent guardrails`. The approval hook is off unless a threshold is
|
|
378
|
+
set.
|
|
379
|
+
|
|
380
|
+
A spend evaluated without a job id is not exempt from the budget. All unscoped
|
|
381
|
+
spends share one `unscoped` envelope under the same cap.
|
|
382
|
+
|
|
383
|
+
Wire in your own approver (a Slack prompt, a ticket queue, a CLI confirm):
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from solvent.guardrails import ApprovalRequest, Guardrails
|
|
387
|
+
|
|
388
|
+
def approver(req: ApprovalRequest) -> str:
|
|
389
|
+
return "approved" if req.vendor == "market-data-api" else "held"
|
|
390
|
+
|
|
391
|
+
guard = Guardrails(treasury, approver=approver)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
A held spend stops the job and emits a `spend_held` event, so nothing moves
|
|
395
|
+
until a human decides.
|
|
396
|
+
|
|
397
|
+
**How this relates to the card networks.** The pattern is close to what the
|
|
398
|
+
networks describe publicly. Mastercard's
|
|
399
|
+
[Agent Pay for Machines](https://www.mastercard.com/global/en/news-and-trends/press/2026/october/Mastercard-Agent-Pay-for-Machines-in-action.html)
|
|
400
|
+
gives an agent "a job and a spending limit", and Mastercard's
|
|
401
|
+
[agentic token framework](https://www.mastercard.com/us/en/news-and-trends/stories/2025/agentic-commerce-framework.html)
|
|
402
|
+
carries transaction limits and validity windows. In the
|
|
403
|
+
[Visa, BILL and Billtrust pilot](https://corporate.visa.com/en/sites/visa-perspectives/newsroom/visa-bill-billtrust-launch-pilot.html),
|
|
404
|
+
the agent only pays invoices the buyer has already approved. Stripe's
|
|
405
|
+
[Issuing for agents](https://docs.stripe.com/issuing/agents) offers per-card and
|
|
406
|
+
per-cardholder
|
|
407
|
+
[spending controls](https://docs.stripe.com/issuing/controls/spending-controls)
|
|
408
|
+
and real-time authorization webhooks. Those controls run at the card or network
|
|
409
|
+
layer. Solvent's run inside the agent, before it calls any payment API, so they
|
|
410
|
+
work in simulation and with any payment backend. They complement network-level
|
|
411
|
+
controls and don't replace them.
|
|
412
|
+
|
|
338
413
|
### Backlog — which job to work on next
|
|
339
414
|
|
|
340
415
|
A queue is not a plan. When several jobs are open and both cash and the 24h
|
|
@@ -483,6 +558,102 @@ a single JSON document, over a period given as `7d`, `24h`, `2w`, `3m` or
|
|
|
483
558
|
revenue and cost counted when the money moved — so the printed summary ties out
|
|
484
559
|
against the rows it ships, with refunds separated from cost of sales.
|
|
485
560
|
|
|
561
|
+
### How long it takes
|
|
562
|
+
|
|
563
|
+
"When will I get it?" is the second question every customer asks, and the agent
|
|
564
|
+
had no answer. Fulfilment seconds were recorded, but that is only the model's
|
|
565
|
+
part — what the customer experiences is the wait from *paying* to *having the
|
|
566
|
+
brief*, across the queue, the worker's poll, retries and delivery.
|
|
567
|
+
|
|
568
|
+
That wait is already on the record (the ledger says when the money arrived, the
|
|
569
|
+
event log says when the brief went out), so `solvent turnaround` measures it
|
|
570
|
+
rather than estimating, and turns the distribution into something quotable: a
|
|
571
|
+
promise at a chosen percentile, rounded up, never faster than a floor. The
|
|
572
|
+
promise then appears on accepted quotes:
|
|
573
|
+
|
|
574
|
+
```
|
|
575
|
+
Verdict ACCEPT — accepted
|
|
576
|
+
Turnaround within 5 minutes
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
It also retires an arbitrary threshold. `alerts.py` called a paid job "stuck"
|
|
580
|
+
after two hours because two hours sounded reasonable; a shop that normally
|
|
581
|
+
delivers in forty seconds should not wait two hours to notice, and one that
|
|
582
|
+
takes a day should not cry wolf. With enough history, late is defined by the
|
|
583
|
+
shop's own p95 — and only until then by the fixed hours.
|
|
584
|
+
|
|
585
|
+
### Running it from Telegram
|
|
586
|
+
|
|
587
|
+
Fifteen commands' worth of reporting lived in the CLI, which is where the
|
|
588
|
+
operator is not. The gateway's slash commands now cover the business too —
|
|
589
|
+
`/funnel`, `/customers`, `/capacity`, `/alerts`, `/products`, `/quality`,
|
|
590
|
+
`/review`, `/costs` — each a short read over local state, no model call, no
|
|
591
|
+
spend.
|
|
592
|
+
|
|
593
|
+
The catch is who is asking. Any *paired* Telegram user reaches that handler,
|
|
594
|
+
and a customer is not entitled to the agent's win rate or cash position, so
|
|
595
|
+
these are operator-only:
|
|
596
|
+
|
|
597
|
+
- the **dashboard** channel is the operator by construction — it sits behind
|
|
598
|
+
`SOLVENT_DASHBOARD_TOKEN`;
|
|
599
|
+
- a Telegram id in `SOLVENT_OPERATOR_IDS` is the operator;
|
|
600
|
+
- anyone else gets "That command is operator-only", and `/help` does not list
|
|
601
|
+
commands they cannot run.
|
|
602
|
+
|
|
603
|
+
### The deliverable
|
|
604
|
+
|
|
605
|
+
Every job has been billed `pdf-render-saas` since the first commit, and no PDF
|
|
606
|
+
was ever produced — the agent was paying itself for a deliverable it did not
|
|
607
|
+
deliver. It renders one now: markdown, HTML **and** PDF, written beside each
|
|
608
|
+
other in `data/reports/`, linked from the delivery email and served from the
|
|
609
|
+
same token-gated route as the brief itself (`&format=pdf`), so nothing new is
|
|
610
|
+
exposed. With SMTP configured the PDF is attached to the email.
|
|
611
|
+
|
|
612
|
+
The renderer is `solvent/pdf.py`, about 250 lines of stdlib: a PDF is a text
|
|
613
|
+
container with a byte-offset table, so a single-font document needs no library,
|
|
614
|
+
only exact xref offsets. Base-14 Helvetica means nothing is embedded and a
|
|
615
|
+
brief weighs a couple of kilobytes. A render failure is caught and logged
|
|
616
|
+
rather than raised — losing a brief the customer has paid for would be far
|
|
617
|
+
worse than shipping it without the PDF.
|
|
618
|
+
|
|
619
|
+
### Knowing the win rate
|
|
620
|
+
|
|
621
|
+
**`solvent funnel`.** The treasury has always known the margin on the jobs it
|
|
622
|
+
took; it never knew the win rate. Those are different businesses — a 90% margin
|
|
623
|
+
on one job in ten is a worse shop than a 60% margin on eight, and nothing here
|
|
624
|
+
could tell them apart.
|
|
625
|
+
|
|
626
|
+
Every stage already leaves a trace, so the funnel is a read over data the agent
|
|
627
|
+
keeps anyway: submitted → quoted → accepted → checkout → paid → delivered. What
|
|
628
|
+
falls out at each step is the point, and the three failures want three
|
|
629
|
+
different fixes:
|
|
630
|
+
|
|
631
|
+
| Exit | What it means |
|
|
632
|
+
|---|---|
|
|
633
|
+
| `screened` | held before pricing — cost nothing |
|
|
634
|
+
| `declined` | a customer who wanted to pay and was turned away by the margin gate |
|
|
635
|
+
| `abandoned` | a customer who said yes at checkout and then did not pay |
|
|
636
|
+
| `refunded` | paid, then refunded when fulfilment failed — the expensive loss |
|
|
637
|
+
|
|
638
|
+
Lost pipeline is reported at the price the customer named, because that is the
|
|
639
|
+
money that was on the table. Win rate is broken down by product and price band,
|
|
640
|
+
and counter-offers are tracked separately, so "does the counter-offer actually
|
|
641
|
+
convert?" finally has an answer.
|
|
642
|
+
|
|
643
|
+
```
|
|
644
|
+
Submitted 9 ████████████████████████ 100.0%
|
|
645
|
+
Accepted by the gate 6 ████████████████········ 66.7%
|
|
646
|
+
Paid 5 █████████████··········· 55.6%
|
|
647
|
+
|
|
648
|
+
Gate acceptance 85.7% · payment conversion 83.3%
|
|
649
|
+
Won $373.00 · left on the table $9,130.00
|
|
650
|
+
Counter-offers 1 made, 0 won (0.0%)
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
The same picture now has a **Commercial** panel in the HTML dashboard — win
|
|
654
|
+
rate, customer book, brief quality and the throughput ceiling beside the P&L —
|
|
655
|
+
so the business reads in one place instead of across six commands.
|
|
656
|
+
|
|
486
657
|
### Selling, not just quoting
|
|
487
658
|
|
|
488
659
|
**`solvent products` — a price list.** A shop that cannot say what it charges
|
|
@@ -497,8 +668,29 @@ curl -X POST localhost:8787/jobs -d '{"topic": "...", "product": "deep-dive", ..
|
|
|
497
668
|
|
|
498
669
|
The catalogue is priced against *current* costs — calibration included — so a
|
|
499
670
|
list price that has quietly stopped clearing the margin floor shows up as a
|
|
500
|
-
stale price rather than as a run of unprofitable work.
|
|
501
|
-
|
|
671
|
+
stale price rather than as a run of unprofitable work.
|
|
672
|
+
|
|
673
|
+
Changing what the shop sells is a business decision, not a code change, so the
|
|
674
|
+
built-in list is overridable in `.solvent/products.json` beside the spend and
|
|
675
|
+
intake policies. Entries merge by slug — naming only a price keeps the built-in
|
|
676
|
+
scope — `retire` drops a product, and `replace: true` makes the file the whole
|
|
677
|
+
list:
|
|
678
|
+
|
|
679
|
+
```json
|
|
680
|
+
{
|
|
681
|
+
"products": [
|
|
682
|
+
{ "slug": "express", "list_price_cents": 3500 },
|
|
683
|
+
{ "slug": "retainer", "name": "Monthly retainer", "list_price_cents": 49900,
|
|
684
|
+
"est_tokens": 60000, "market_data_calls": 12, "web_search_calls": 40 }
|
|
685
|
+
],
|
|
686
|
+
"retire": ["deep-dive"]
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
A malformed entry is dropped rather than sold — a half-read product is a price
|
|
691
|
+
nobody agreed to — and `solvent products --validate` reports it. The command
|
|
692
|
+
exits non-zero when any product has gone stale *or* the file is broken, so cron
|
|
693
|
+
catches either without reading the output.
|
|
502
694
|
|
|
503
695
|
**`solvent quality` — a gate on the product, not just the margin.** Every other
|
|
504
696
|
gate here asks whether the money is sound; this one asks whether the brief is.
|
|
@@ -554,6 +746,22 @@ With both keys set:
|
|
|
554
746
|
- Optional: set `STRIPE_WEBHOOK_SECRET` and forward `checkout.session.completed` events via `StripeClient.process_webhook()`.
|
|
555
747
|
- Optional: enable **Stripe Issuing** on your test account to provision capped single-use virtual debit cards for each vendor payment.
|
|
556
748
|
|
|
749
|
+
### Local or OpenAI-compatible model (Ollama, LM Studio, vLLM)
|
|
750
|
+
|
|
751
|
+
Briefs can be written by any OpenAI-compatible `/chat/completions` server instead of Nemotron. This is opt-in, uses only the standard library, and is ignored unless you set one of the variables below:
|
|
752
|
+
|
|
753
|
+
```bash
|
|
754
|
+
ollama pull llama3.2
|
|
755
|
+
export SOLVENT_LLM_MODEL=llama3.2 # base URL defaults to Ollama
|
|
756
|
+
# export SOLVENT_LLM_BASE_URL=http://localhost:11434/v1 # or any OpenAI-compatible URL
|
|
757
|
+
# export SOLVENT_LLM_API_KEY=... # optional; sent only as a Bearer header
|
|
758
|
+
solvent --no-onboard
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
If the server is down, slow (`SOLVENT_LLM_TIMEOUT`, default 30s), or returns an error or malformed reply, SOLVENT logs a warning and falls back to the deterministic offline brief, so the job still completes. The API key is never logged or written to a brief.
|
|
762
|
+
|
|
763
|
+
**API keys.** `SOLVENT_LLM_API_KEY` stays on your machine: it is read from the environment and sent only as a Bearer header to the configured `SOLVENT_LLM_BASE_URL`, never anywhere else. Redirects are refused rather than followed, so a 3xx can never forward the key to another host or downgrade it to `http://`; point the base URL at the final endpoint. Give each tool its own key with a spend cap, so one leaked or misbehaving key can only spend so much.
|
|
764
|
+
|
|
557
765
|
### Environment variables
|
|
558
766
|
|
|
559
767
|
| Variable | Purpose |
|
|
@@ -571,6 +779,10 @@ With both keys set:
|
|
|
571
779
|
| `SOLVENT_TELEGRAM_ALLOW_FROM` | Comma-separated Telegram user IDs for allowlist mode |
|
|
572
780
|
| `SOLVENT_PORT` | Port for the `serve` API server (default `8787`) |
|
|
573
781
|
| `SOLVENT_BASE_URL` | Base URL for hosted brief links and Stripe webhook callbacks |
|
|
782
|
+
| `SOLVENT_LLM_BASE_URL` | Opt-in OpenAI-compatible endpoint for briefs (default when enabled: `http://localhost:11434/v1`, Ollama) |
|
|
783
|
+
| `SOLVENT_LLM_MODEL` | Model name for that endpoint; setting it alone enables the backend (default `llama3.2`) |
|
|
784
|
+
| `SOLVENT_LLM_API_KEY` | Optional Bearer key for the endpoint; never logged |
|
|
785
|
+
| `SOLVENT_LLM_TIMEOUT` | Request timeout in seconds for that endpoint (default `30`) |
|
|
574
786
|
| `NEMOTRON_MODEL` | Nemotron model override (default: `nvidia/llama-3.1-nemotron-ultra-253b-v1`) |
|
|
575
787
|
| `SOLVENT_DELIVERY_SECRET` | HMAC token secret for `/briefs/{job_id}`; at least 32 characters, high entropy |
|
|
576
788
|
| `SOLVENT_SKIP_ONBOARD` | Set to `1` to skip the first-run wizard |
|
|
@@ -653,9 +865,13 @@ solvent/
|
|
|
653
865
|
products.py the price list (`solvent products`)
|
|
654
866
|
quality.py deliverable quality gate (`solvent quality`)
|
|
655
867
|
review.py operator approval queue (`solvent review`)
|
|
868
|
+
funnel.py order funnel and win rate (`solvent funnel`)
|
|
869
|
+
turnaround.py measured delivery time (`solvent turnaround`)
|
|
870
|
+
ops.py operator-only business commands on the chat surface
|
|
656
871
|
stripe_client.py two-sided Stripe layer (earn + spend)
|
|
657
872
|
nemotron.py NVIDIA Nemotron client (+ offline stub)
|
|
658
873
|
service.py the product: an on-demand research brief
|
|
874
|
+
pdf.py dependency-free PDF rendering of the brief
|
|
659
875
|
jobs.py sample inbound work
|
|
660
876
|
dashboard.py renders the treasury to HTML + JSON
|
|
661
877
|
finance.py income statement · unit economics · runway · forecast
|
|
@@ -24,10 +24,10 @@ classifiers = [
|
|
|
24
24
|
dependencies = []
|
|
25
25
|
|
|
26
26
|
[project.optional-dependencies]
|
|
27
|
-
stripe = ["stripe>=9.0.0"]
|
|
28
|
-
serve = ["fastapi>=0.110.0", "uvicorn[standard]>=0.27.0"]
|
|
29
|
-
qr = ["qrcode[pil]>=7.0"]
|
|
30
|
-
telegram = ["python-telegram-bot>=21.0"]
|
|
27
|
+
stripe = ["stripe>=9.0.0", "urllib3>=2.8.0", "certifi>=2026.10.19"]
|
|
28
|
+
serve = ["fastapi>=0.110.0", "uvicorn[standard]>=0.27.0", "anyio>=4.14.2"]
|
|
29
|
+
qr = ["qrcode[pil]>=7.0", "pillow>=13.0.0; python_version >= '3.11'"]
|
|
30
|
+
telegram = ["python-telegram-bot>=21.0", "anyio>=4.14.2", "certifi>=2026.10.19"]
|
|
31
31
|
dev = ["pytest>=7.0.0", "ruff>=0.3"]
|
|
32
32
|
all = [
|
|
33
33
|
"stripe>=9.0.0",
|
|
@@ -35,6 +35,11 @@ all = [
|
|
|
35
35
|
"uvicorn[standard]>=0.27.0",
|
|
36
36
|
"qrcode[pil]>=7.0",
|
|
37
37
|
"python-telegram-bot>=21.0",
|
|
38
|
+
# Security floors for transitive deps (verified GHSAs, Oct 2026). See constraints.txt.
|
|
39
|
+
"urllib3>=2.8.0",
|
|
40
|
+
"certifi>=2026.10.19",
|
|
41
|
+
"anyio>=4.14.2",
|
|
42
|
+
"pillow>=13.0.0; python_version >= '3.11'",
|
|
38
43
|
]
|
|
39
44
|
|
|
40
45
|
[project.scripts]
|