Skip to content

About

Offline-first Python AI agent that runs a tiny research business: quotes each job against its own costs, collects via Stripe, fulfils with NVIDIA Nemotron, pays vendors within a spend policy, and declines unprofitable work.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

114 stars

Watchers

0 watching

Forks

Repository files navigation

🪙 SOLVENT

A Python agent that runs a tiny research business: it quotes each job against its own costs, gets paid through Stripe, pays its own vendor bills within a spend policy, and declines work that would lose money.

CI PyPI Hugging Face Space Python 3.10+ Zero dependencies License: MIT Hackathon Research: SpendAudit Stars

Quick Start · How It Works · Live Demo · Make It Real · Research

SOLVENT in a terminal: the margin gate declines an $8 job with a counter-offer, then the offline demo runs five jobs end to end

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.

▶ Try it in your browser: the real package running in Pyodide, with no install and no keys.

The SOLVENT Hugging Face Space: five jobs run, J3 declined with a $15 counter-offer, and a simulated P&L snapshot

All dollar figures, including the final cash balance, are simulated.

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.


🚀 Quick Start

30 seconds. No API keys. No dependencies beyond Python 3.10+.

pipx install solvent-agent      # or: pip install solvent-agent
solvent --no-onboard            # run the offline demo batch (skip the first-run wizard)
solvent finance                 # income statement, unit economics, runway

Then open the dashboard path the demo prints (~/.solvent/treasury_dashboard.html for a pip/pipx install, or under $SOLVENT_HOME if you set it).

Try the margin gate on its own. This dry run declines an $8 order and returns a counter-offer that clears the margin floor:

solvent quote "Edge-AI in industrial robotics" --budget 8
Command What it does
solvent batch demo (onboarding wizard on first run; --no-onboard skips it)
solvent init create data dirs, treasury DB, and workspace files
solvent status live treasury summary (--watch to auto-refresh)
solvent finance income statement, unit economics, runway, forecast
solvent quote dry-run the margin gate on a job and price a counter-offer
solvent guardrails show the spend policy: vendor caps, velocity limit, job budgets
solvent doctor diagnostics: API keys, extras, workspace files
solvent serve webhooks + job API + hosted dashboard ([serve] extra)
solvent worker resume incomplete jobs / process the queue
solvent jobs list / show / retry / cancel jobs (jobs --help)
solvent help every command

Or run from a source checkout:

git clone https://github.com/ianalloway/solvent-agent.git
cd solvent-agent
python3 run_demo.py              # batch demo (onboarding wizard on first run)
python3 run_demo.py --no-onboard # skip wizard when scripting
pip install -e .                 # editable install from a checkout

The demo runs a batch of 5 sample analyst jobs (4 accepted, 1 declined by the margin gate) through margin gating, simulated Stripe payment, Nemotron fulfilment (offline stub), guardrail screening, and live P&L, in about 30 seconds.

Third-party features are opt-in extras, so install only what you need:

pip install "solvent-agent[stripe]"    # real Stripe test-mode payment links
pip install "solvent-agent[serve]"     # FastAPI webhooks + hosted briefs
pip install "solvent-agent[telegram]"  # Telegram bot channel
pip install "solvent-agent[qr]"        # scannable QR codes for OpenClaw pairing
pip install "solvent-agent[dev]"       # pytest, for running the test suite
pip install "solvent-agent[all]"       # everything

When run from a source checkout, runtime data stays under <repo>/data. When installed elsewhere, SOLVENT writes to ~/.solvent instead of into site-packages; override either with SOLVENT_HOME=/path/to/dir.

First run: A short onboarding wizard asks you to choose a model, interaction mode, and whether to enable Stripe test mode. Preferences are saved to .solvent/config.json and never committed.


The Big Idea

Most agents can spend money. Almost none can run as a business.

SOLVENT closes the full loop:

  Client pays Stripe → Agent earns revenue → Agent fulfils the work
  → Agent pays its own vendor bills → P&L booked → balance sheet grows

Every job is profit-gated before it starts. Unprofitable work is declined without touching Stripe. Vendor payments are screened by a NemoClaw-style policy sandbox, so the agent spends only out of revenue it has already collected.


📊 The Demo

After a run, the CLI prints the dashboard path. Open it in a browser:

open treasury_dashboard.html          # macOS (source checkout)
xdg-open treasury_dashboard.html      # Linux
# pip/pipx install: ~/.solvent/treasury_dashboard.html  (or $SOLVENT_HOME)

SOLVENT Treasury Dashboard — live P&L, job cards, resource allocation, transaction log

A typical offline demo batch (illustrative numbers from the simulated run — not production revenue):

Metric Demo value
Revenue $348.00
Operating spend $2.20 (booked vendor spend)
Net profit $345.80 (99.4% margin)
Jobs completed / declined 4 / 1 (below minimum order size)

From solvent --no-onboard on a fresh SOLVENT_HOME with no API keys. The 99.4% margin is higher than the gate's 87–92% projections because offline fulfilment makes no market-data or web-search calls; see #79.


⚙️ How It Works

 inbound job
     │
     ▼
 ┌─────────────┐   margin < floor?  ┌───────────┐
 │  MARGIN GATE│ ─────────────────▶ │  DECLINE  │
 │  (pricing)  │                    └───────────┘
 └─────┬───────┘ accept
       ▼
 ┌─────────────┐   EARN
 │   STRIPE    │ ── Payment Link → poll/webhook until paid ──▶ + revenue
 └─────┬───────┘    (records cs_... + pi_... on ledger)
       ▼
 ┌─────────────┐   FULFIL
 │  NEMOTRON   │ ── Llama-3.1-Nemotron-Ultra produces the brief ──▶ resource usage
 └─────┬───────┘
       ▼
 ┌─────────────┐   SPEND (every payment screened first)
 │ GUARDRAILS  │ ── NemoClaw policy: allowlist · caps · reserve · ROI
 │   → STRIPE  │ ── Issuing virtual card (test) or simulated spend ──▶ − expense
 └─────┬───────┘
       ▼
   BOOK P&L  ──▶ treasury updated · dashboard refreshed

The job pipeline checks payment before fulfilment and screens vendor payments against the configured spend policy. Quotes use estimated costs, so an accepted quote is a projected margin, not a guarantee of realized profit. The ledger and cost-calibration reports show how actual spending compares with that estimate.


🏗️ Architecture

Layer Technology File
Analyst / reasoning NVIDIA Nemotron (Llama-3.1-Nemotron-Ultra) solvent/nemotron.py
Spend safety NVIDIA NemoClaw-style policy sandbox solvent/guardrails.py
Earn Stripe Payment Links + Checkout Session polling solvent/stripe_client.py
Spend Stripe Issuing virtual cards (test mode) solvent/stripe_client.py
Orchestration Hermes / Nous tool-calling agent loop solvent/agent.py
Memory SQLite treasury + pricing ledger solvent/treasury.py · solvent/pricing.py

Key design choices:

  • Structural profitability — pricing.py computes unit cost before quoting. If margin < floor, the job never reaches Stripe.
  • Spend policy — guardrails.py enforces vendor allowlist, per-transaction cap, rolling 24h budget, minimum cash reserve, and no-negative-ROI rule.
  • 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.
  • Audit trail — every cs_... checkout session ID and pi_... PaymentIntent ID is recorded on the ledger before fulfilment begins.

Resource prices can be overridden in .solvent/pricing_overrides.json. Values are costs in cents and must be finite, non-negative numbers; 0 is valid for a free resource. Invalid entries (including negative values, booleans, NaN, and infinity) keep the built-in price. Valid entries in the same file still apply.


🎮 Running Modes

Batch demo (default — best for judges)

python3 run_demo.py

5 pre-loaded jobs (4 accepted, 1 declined). ~30 seconds. Shows margin gating, Stripe earn/spend, Nemotron fulfillment, and guardrails in action.

Interactive — your own jobs

python3 run_demo.py --interactive

Type a research topic and client budget at the prompt. The agent quotes, pays, fulfils, and books P&L for each one in real time. Keep going until you quit.

Add funds mid-session

python3 run_demo.py --seed 500        # start with $500 instead of $100
python3 run_demo.py --keep-balance    # resume existing treasury balance

In interactive mode, type /fund 200 at the prompt to deposit $200 into the live treasury without restarting.

Programmatic

from solvent.agent import Solvent
from solvent.jobs import SAMPLE_JOBS

agent = Solvent(seed_cents=10_000)  # reset treasury, seed $100
agent.handle_job(SAMPLE_JOBS[0])  # process one job
snap = agent.run(SAMPLE_JOBS[1:])  # process a list; returns snapshot

print(snap["balance_cents"], snap["margin_pct"])

Production mode (webhooks + async worker)

pip install "solvent-agent[serve]"
export SOLVENT_DASHBOARD_TOKEN=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')

python3 -m solvent serve --port 8787   # webhooks + job API + hosted briefs
python3 -m solvent worker              # resume incomplete jobs, process queue

# Interactive voice dashboard (chat + live SSE updates):
open "http://127.0.0.1:8787/?token=$SOLVENT_DASHBOARD_TOKEN"

The hosted dashboard at / includes a chat panel (type or use the mic with Web Speech API) and live treasury updates via Server-Sent Events (/api/events). Dashboard/control routes require SOLVENT_DASHBOARD_TOKEN via ?token=... or the X-Solvent-Dashboard-Token header before they expose status data or route chat through the Nemotron agent loop.

See docs/PRODUCTION.md for Stripe webhook setup, SMTP delivery, and reconciliation.

Operations

python3 -m solvent quote "AI inference chips, 2026" --budget 49   # dry-run the margin gate
python3 -m solvent backlog                # rank open jobs by return on capital
python3 -m solvent guardrails             # spend policy in force + vendor exposure
python3 -m solvent customers              # lifetime value, repeat rate, margin by customer
python3 -m solvent costs                  # estimated vs realized COGS + calibration
python3 -m solvent simulate               # run the policy over synthetic demand
python3 -m solvent optimize               # search margin floor x min order for the best policy
python3 -m solvent checkouts              # unpaid links: age, reminders, expiry
python3 -m solvent intake                 # the screen inbound jobs pass before pricing
python3 -m solvent capacity               # jobs/day ceiling and which rule binds it
python3 -m solvent alerts                 # one health sweep; non-zero exit on critical
python3 -m solvent export --since 30d     # ledger/jobs/metrics/customers + period close
python3 -m solvent products               # the price list, checked against current cost
python3 -m solvent quality                # scores the quality gate gave shipped briefs
python3 -m solvent review                 # approve/reject the jobs intake held for a human
python3 -m solvent funnel --since 7d      # win rate: submitted → quoted → paid → delivered
python3 -m solvent turnaround             # measured paid-to-delivered time, and the promise
python3 -m solvent reconcile --since 7d   # Stripe ↔ ledger drift check
python3 -m solvent finance                # income statement, unit economics, runway
python3 -m solvent finance --json         # machine-readable report
python3 -m solvent finance --reserve 50   # runway to a $50 cash-reserve floor
python3 -m solvent finance --period week  # net P&L trend by day | week | month
python3 -m solvent finance --horizon 60   # forecast the balance 60 days out

finance (alias report) turns the treasury ledger into the numbers a business steers by: revenue/cost/net-margin, average profit per job, a cash runway — days of burn remaining, or cash-flow positive once the agent funds itself — a net-P&L trend bucketed by day/week/month, and a balance forecast (central projection with a best/worst band whose width grows with daily volatility). The income statement, runway, trend, and forecast also render as a Financial Statement panel in the HTML dashboard.


🧭 Commercial Judgement

Three things turn the money loop into something that behaves like a shop rather than a script.

Counter-offers — a decline is a negotiating position

The margin gate still refuses work it cannot do profitably, but it no longer just says no. Every decline carries the deal the agent would accept:

python3 -m solvent quote "Edge-AI in industrial robotics" --budget 8 --tokens 30000
  Projected margin     $-4.33 (-54.1%)   floor 35.0%
  Verdict              DECLINE — order $8 below minimum order size $15

  Counter-offer        $19.00 at 35.1% margin
    can deliver this brief as specified for $19.00

Two shapes, in order of preference: a narrower scope the customer's existing budget can buy (fewer market-data pulls first — they are the priciest resource), or, when no sellable scope fits, the lowest price that clears the margin floor. The offer is emitted as a counter_offer event next to the decline, so any channel — terminal, Telegram, the job API — can quote it back. solvent quote runs the whole gate as a dry run: nothing is written to the treasury, no Stripe call is made, and the exit code is 1 on a decline so scripts can gate on the verdict.

Spend policy — bounding how money moves, not just how much

The guardrails gained two rules that shape the distribution of spend:

Rule What it stops
Per-vendor 24h cap one vendor — compromised, mispriced, or just buggy — absorbing the whole day's budget
Spend velocity a fulfilment loop that starts paying in a tight cycle, long before it drains the treasury

Limits are operator-tunable without touching code, via .solvent/spend_policy.json (a malformed file is ignored rather than allowed to widen the policy):

{
  "daily_budget_cents": 50000,
  "per_vendor_daily_cents": 8000,
  "max_txns_per_hour": 40,
  "vendor_daily_overrides": { "market-data-api": 15000 }
}

The .solvent configuration directory is inside the resolved runtime home: the source checkout by default, or $SOLVENT_HOME when set. Run solvent guardrails or solvent doctor to see the active spend-policy path. If an older installation kept its .solvent files in a different launch directory, move them into this resolved directory before upgrading.

python3 -m solvent guardrails prints the policy in force, how much of each rolling window is used, per-vendor exposure against its cap, and every spend the policy blocked.

Spending controls — per-job envelopes and human approval

Two more rules sit on top of the daily, per-vendor and velocity limits. Both run inside the same deterministic guardrail check, before any Stripe call, and both fail closed.

Rule Decision rule id What happens
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.
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.

Configure them in .solvent/spend_policy.json:

{ "job_budget_cents": 8000, "approval_threshold_cents": 2500 }

or with the environment variables SOLVENT_JOB_BUDGET_CENTS and SOLVENT_APPROVAL_THRESHOLD_CENTS. Env vars can only tighten these settings: a lower budget or a lower threshold. An env value that would raise or disable a limit (for example SOLVENT_JOB_BUDGET_CENTS=0) is ignored. Loosening or disabling takes an explicit policy-file setting, and any override that loosens or disables a limit, or is ignored, is logged as a warning and shown in solvent guardrails. The approval hook is off unless a threshold is set.

A spend evaluated without a job id is not exempt from the budget. All unscoped spends share one unscoped envelope under the same cap.

Wire in your own approver (a Slack prompt, a ticket queue, a CLI confirm):

from solvent.guardrails import ApprovalRequest, Guardrails

def approver(req: ApprovalRequest) -> str:
    return "approved" if req.vendor == "market-data-api" else "held"

guard = Guardrails(treasury, approver=approver)

A held spend stops the job and emits a spend_held event, so nothing moves until a human decides.

How this relates to the card networks. The pattern is close to what the networks describe publicly. Mastercard's Agent Pay for Machines gives an agent "a job and a spending limit", and Mastercard's agentic token framework carries transaction limits and validity windows. In the Visa, BILL and Billtrust pilot, the agent only pays invoices the buyer has already approved. Stripe's Issuing for agents offers per-card and per-cardholder spending controls and real-time authorization webhooks. Those controls run at the card or network layer. Solvent's run inside the agent, before it calls any payment API, so they work in simulation and with any payment backend. They complement network-level controls and don't replace them.

Backlog — which job to work on next

A queue is not a plan. When several jobs are open and both cash and the 24h spend budget are finite, the order the agent works in decides what it earns. python3 -m solvent backlog ranks the open work the way a business would — and the async worker consumes the same ranking:

  1. Finish what is already paid for. Revenue is collected before cost is incurred, so a paid job left unfinished is a refund waiting to happen.
  2. Then best return on capital — margin per cent of fulfilment cost, so a $20 job costing $5 outranks a $90 job costing $60.
  3. Never start work the treasury cannot fund. A job whose fulfilment would breach the spend budget or the cash reserve is deferred until the treasury can pay for it — where the quote stage would otherwise decline it permanently — and a cheaper job behind it can still take the remaining capacity.
  #  JOB         STATUS                    PRICE     COST    ROI  TOPIC
  1  J4          awaiting_payment         $99.00    $8.07  11.27   Edge-AI adoption in industri
  2  J5          awaiting_payment        $125.00   $10.41  11.01   Unit economics of autonomous
    ⏸ J2: fulfilment needs 845c; only 200c of spend capacity left (24h budget / cash reserve)

Knowing the business

Three more views the agent keeps on itself.

solvent customers — who actually pays. Every job carries an email and every ledger entry carries a job id, so the two join into lifetime value per customer: revenue, COGS, net, repeat rate, and the share of revenue riding on the single best customer (the concentration risk). --email <addr> drills into one customer's job history.

  CUSTOMER                           JOBS    REVENUE        NET  MARGIN  LAST
   analyst@logistics.example            1    $125.00    $124.45   99.6%  just now
  ↻analyst@fund.example                 2     $98.00     $96.90   98.9%  just now

  Customers 5  ·  repeat 1 (20.0%)  ·  revenue per customer $79.40
  Top customer is 31.5% of revenue

solvent costs — a margin gate that learns. The stage machine already recorded what each job really cost; now that feeds back into the next quote. When realized COGS run hotter than the static model, quotes are marked up by the observed ratio (clamped, and only after five fulfilled jobs) so the margin floor keeps meaning what it says. When they run cooler, nothing happens automatically — an optimistic sample is not a reason to quote closer to the bone, and cutting prices stays an operator decision.

solvent simulate — try the policy before it touches money. Margin floor, transaction cap, daily budget, cash reserve: every one is a number somebody picks, and picking them on a live treasury means finding out the expensive way. This runs the same pricing and guardrails kernel over synthetic demand, many times, and reports the distribution — acceptance rate, net per day, ending balance percentiles, how often the business ends below its reserve, and which rule did the blocking. One trial is one day of trading, so the rolling 24h budget and the velocity rule bind the way they would in a real day.

python3 -m solvent simulate                            # the policy as it stands
python3 -m solvent simulate --cost-multiplier 6        # what if vendors got 6× pricier
python3 -m solvent simulate --margin-floor 55 --json   # tune the floor, machine-readable

Nothing in a simulation touches the treasury, Stripe, or Nemotron, and the same --seed always reproduces the same run.

Running the shop

solvent checkouts — abandoned carts are normal; leaving them open is not. A job that reached awaiting_payment used to sit there forever: polled on every worker pass, counted as pipeline, and never chased. Unpaid links now have a lifecycle — the customer is reminded after reminder_after_hours (at most max_reminders times, spaced), and the link expires after expire_after_hours, closing the Stripe session and dropping the job out of the queue. Expiry never moves money: an unpaid job has no revenue to refund. The worker sweeps on every pass; --sweep runs it by hand. Tune it in .solvent/checkout_policy.json.

solvent intake — the screen before the margin gate. security.py refuses hostile content; this refuses bad commerce, and it runs before pricing so a screened-out job costs nothing:

Rule What it catches
duplicate the same customer asking for the same brief inside an hour — a double-click, not two commissions
customer_burst one customer flooding the queue
oversized_order an order above the automatic ceiling, where a human should look first
unreachable_customer a missing or malformed email, or a blocked domain

Blocks are recorded on the job with an intake: reason, so they are greppable in solvent jobs and the event log. Thresholds live in .solvent/intake_policy.json.

solvent optimize — which policy should I actually run? simulate answers "what would this do"; this searches the space. It sweeps margin floor × minimum order over identical synthetic demand (common random numbers, so cells differ by policy and nothing else) and picks the best-paying cell inside a stated risk budget — by default, ending a day below the cash reserve at most 5% of the time. The constraint is the whole point: without it, "best" always picks the reckless cell.

    FLOOR  MIN ORDER   ACCEPT     NET/DAY    p10 BAL  BELOW RES
    35.0%     $10.00    17.7%      $-3.04    $-34.29      28.0%
    45.0%     $10.00    24.0%      $35.84     $32.26       0.0% ←

  → Run a 45.0% margin floor with a $10.00 minimum order: $35.84 net per day.
    vs the policy in force (35.0% / $15.00): $12.35 more per day.

(that run is --cost-multiplier 8: the same search under a vendor price shock)

Operating it day to day

solvent capacity — how much work can this policy get through? The simulator found the ceiling the expensive way; this derives it in closed form. Every spend rule implies a maximum number of jobs per day — daily budget ÷ cost per job, spendable cash ÷ cost per job, payments per hour ÷ payments per job, each vendor cap ÷ that vendor's share — and the lowest one is the real ceiling. Unit costs come from what jobs actually cost when there is history and from the pricing model when there is not, and the report names the one rule worth changing:

  Ceiling              250.0 jobs/day
  Bound by             vendor_daily_budget:pdf-render-saas
                       $100.00 cap ÷ $0.40 of pdf-render-saas per job
  → Raise the cap for pdf-render-saas via vendor_daily_overrides in .solvent/spend_policy.json.

solvent alerts — the checks you'd run every morning. Runway, cash against the reserve, paid-but-undelivered work, spend headroom, vendor exposure, guardrail blocks, unpaid pipeline, and cost-model drift, in one sweep with severities. It exits non-zero on anything critical, so it works as a cron job or a CI step (solvent alerts || page-someone), --notify pushes problems to a chat channel, and every alert says what to do about it — an alert nobody can act on is noise.

solvent export — the books, in a form a spreadsheet can read. Writes the ledger, jobs, per-job metrics and customer book as CSV (one file per table) or a single JSON document, over a period given as 7d, 24h, 2w, 3m or YYYY-MM-DD. The period is closed on the ledger rather than the job table — revenue and cost counted when the money moved — so the printed summary ties out against the rows it ships, with refunds separated from cost of sales.

How long it takes

"When will I get it?" is the second question every customer asks, and the agent had no answer. Fulfilment seconds were recorded, but that is only the model's part — what the customer experiences is the wait from paying to having the brief, across the queue, the worker's poll, retries and delivery.

That wait is already on the record (the ledger says when the money arrived, the event log says when the brief went out), so solvent turnaround measures it rather than estimating, and turns the distribution into something quotable: a promise at a chosen percentile, rounded up, never faster than a floor. The promise then appears on accepted quotes:

  Verdict              ACCEPT — accepted
  Turnaround           within 5 minutes

It also retires an arbitrary threshold. alerts.py called a paid job "stuck" after two hours because two hours sounded reasonable; a shop that normally delivers in forty seconds should not wait two hours to notice, and one that takes a day should not cry wolf. With enough history, late is defined by the shop's own p95 — and only until then by the fixed hours.

Running it from Telegram

Fifteen commands' worth of reporting lived in the CLI, which is where the operator is not. The gateway's slash commands now cover the business too — /funnel, /customers, /capacity, /alerts, /products, /quality, /review, /costs, /status — each a short read over local state, no model call, no spend.

The catch is who is asking. Any paired Telegram user reaches that handler, and a customer is not entitled to the agent's win rate or cash position, so these are operator-only:

  • the dashboard channel is the operator by construction — it sits behind SOLVENT_DASHBOARD_TOKEN;
  • a Telegram user id in SOLVENT_OPERATOR_IDS is the operator in a private chat;
  • anyone else gets "That command is operator-only", and /help does not list commands they cannot run.

The treasury_status model tool uses the same operator boundary. Pairing QR tokens also require an operator; pairing a customer does not grant permission to mint new tokens, and Telegram groups cannot mint them.

The deliverable

Every job has been billed pdf-render-saas since the first commit, and no PDF was ever produced — the agent was paying itself for a deliverable it did not deliver. It renders one now: markdown, HTML and PDF, written beside each other in data/reports/, linked from the delivery email and served from the same token-gated route as the brief itself (&format=pdf), so nothing new is exposed. With SMTP configured the PDF is attached to the email.

The renderer is solvent/pdf.py, about 250 lines of stdlib: a PDF is a text container with a byte-offset table, so a single-font document needs no library, only exact xref offsets. Base-14 Helvetica means nothing is embedded and a brief weighs a couple of kilobytes. A render failure is caught and logged rather than raised — losing a brief the customer has paid for would be far worse than shipping it without the PDF.

Knowing the win rate

solvent funnel. The treasury has always known the margin on the jobs it took; it never knew the win rate. Those are different businesses — a 90% margin on one job in ten is a worse shop than a 60% margin on eight, and nothing here could tell them apart.

Every stage already leaves a trace, so the funnel is a read over data the agent keeps anyway: submitted → quoted → accepted → checkout → paid → delivered. What falls out at each step is the point, and the three failures want three different fixes:

Exit What it means
screened held before pricing — cost nothing
declined a customer who wanted to pay and was turned away by the margin gate
abandoned a customer who said yes at checkout and then did not pay
refunded paid, then refunded when fulfilment failed — the expensive loss

Lost pipeline is reported at the price the customer named, because that is the money that was on the table. Win rate is broken down by product and price band, and counter-offers are tracked separately, so "does the counter-offer actually convert?" finally has an answer.

  Submitted                 9  ████████████████████████ 100.0%
  Accepted by the gate      6  ████████████████········ 66.7%
  Paid                      5  █████████████··········· 55.6%

  Gate acceptance      85.7%   ·  payment conversion 83.3%
  Won $373.00  ·  left on the table $9,130.00
  Counter-offers       1 made, 0 won (0.0%)

The same picture now has a Commercial panel in the HTML dashboard — win rate, customer book, brief quality and the throughput ceiling beside the P&L — so the business reads in one place instead of across six commands.

Selling, not just quoting

solvent products — a price list. A shop that cannot say what it charges cannot be ordered from. A product is a named scope at a list price (how much reasoning, how many data pulls, how many searches), and a job can name one instead of pricing itself:

python3 -m solvent quote "Edge-AI in robotics" --product standard
curl -X POST localhost:8787/jobs -d '{"topic": "...", "product": "deep-dive", ...}'

The catalogue is priced against current costs — calibration included — so a list price that has quietly stopped clearing the margin floor shows up as a stale price rather than as a run of unprofitable work.

Changing what the shop sells is a business decision, not a code change, so the built-in list is overridable in .solvent/products.json beside the spend and intake policies. Entries merge by slug — naming only a price keeps the built-in scope — retire drops a product, and replace: true makes the file the whole list:

{
  "products": [
    { "slug": "express", "list_price_cents": 3500 },
    { "slug": "retainer", "name": "Monthly retainer", "list_price_cents": 49900,
      "est_tokens": 60000, "market_data_calls": 12, "web_search_calls": 40 }
  ],
  "retire": ["deep-dive"]
}

A malformed entry is dropped rather than sold — a half-read product is a price nobody agreed to — and solvent products --validate reports it. The command exits non-zero when any product has gone stale or the file is broken, so cron catches either without reading the output.

solvent quality — a gate on the product, not just the margin. Every other gate here asks whether the money is sound; this one asks whether the brief is. Deterministic checks — required sections, length, figures cited, no prompt scaffolding, topic coverage — score each brief out of 100 with a named reason for every point lost. A model grading its own homework is the one judge you cannot audit, which is why none of this asks an LLM.

Two failures are gates rather than tariffs, because points cannot buy them back: scaffolding in the text is what a customer notices first, and a brief matching none of the topic's keywords is not about what they asked for. Either can score 85 on structure alone; neither is worth a pass. A failing brief gets one regeneration attempt and the better draft ships — it always ships, because the customer has paid and withholding the work is worse than delivering it with the shortfall recorded.

solvent review — the queue only a human can clear. The intake screen refuses an order above the automatic ceiling with "needs an operator", which was honest and incomplete: there was no way to approve one. Now held jobs sit in a queue with the rule that caught them, and the operator decides:

python3 -m solvent review                      # what is held, and why
python3 -m solvent review approve BIG          # back into the pipeline
python3 -m solvent review reject BIG --reason "could not verify funds"

Approval writes a one-off exemption onto that job — it does not loosen the policy, which stays a deliberate edit — and both decisions are recorded as events, because "who let this $5,000 order through" is a question that gets asked later.


🔑 Make It Real

To use live Nemotron inference and real Stripe test-mode payment links:

pip install "solvent-agent[stripe]"

export NVIDIA_API_KEY=nvapi-...        # from build.nvidia.com
export STRIPE_API_KEY=sk_test_...      # Stripe test mode only (live keys refused)

python3 run_demo.py

With both keys set:

  • Briefs are written by NVIDIA Nemotron (Llama-3.1-Nemotron-Ultra).
  • Each job creates a real Stripe Payment Link. Pay with test card 4242 4242 4242 4242.
  • SOLVENT polls the Checkout Session (cs_...) until payment_status == paid before fulfilling — no instant confirm.
  • Optional: set STRIPE_WEBHOOK_SECRET and forward checkout.session.completed events via StripeClient.process_webhook().
  • Optional: enable Stripe Issuing on your test account to provision capped single-use virtual debit cards for each vendor payment.

Local or OpenAI-compatible model (Ollama, LM Studio, vLLM)

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:

ollama pull llama3.2
export SOLVENT_LLM_MODEL=llama3.2                       # base URL defaults to Ollama
# export SOLVENT_LLM_BASE_URL=http://localhost:11434/v1  # or any OpenAI-compatible URL
# export SOLVENT_LLM_API_KEY=...                         # optional; sent only as a Bearer header
solvent --no-onboard

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.

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.

Environment variables

Variable Purpose
SOLVENT_HOME Where runtime data and .solvent configuration (treasury DB, policies, reports, dashboard, logs) are stored. Defaults to the repo when run from a checkout, else ~/.solvent
NVIDIA_API_KEY Live Nemotron inference (nvapi-...)
STRIPE_API_KEY Stripe test key (sk_test_...)
STRIPE_WEBHOOK_SECRET Required to accept /webhooks/stripe events (signature is verified before anything is stored)
STRIPE_PAYMENT_POLL_TIMEOUT Seconds to wait for payment (default 120)
STRIPE_PAYMENT_POLL_INTERVAL Poll interval in seconds (default 2)
SOLVENT_FORCE_STRIPE_SIMULATE Force offline simulate mode even with a key
SOLVENT_DASHBOARD_TOKEN Shared secret required for hosted dashboard/control routes
TELEGRAM_BOT_TOKEN Telegram bot token from BotFather
SOLVENT_TELEGRAM_DM_POLICY pairing · allowlist · open (default pairing)
SOLVENT_TELEGRAM_ALLOW_FROM Comma-separated Telegram user IDs for allowlist mode
SOLVENT_PORT Port for the serve API server (default 8787)
SOLVENT_BASE_URL Base URL for hosted brief links and Stripe webhook callbacks
SOLVENT_LLM_BASE_URL Opt-in OpenAI-compatible endpoint for briefs (default when enabled: http://localhost:11434/v1, Ollama)
SOLVENT_LLM_MODEL Model name for that endpoint; setting it alone enables the backend (default llama3.2)
SOLVENT_LLM_API_KEY Optional Bearer key for the endpoint; never logged
SOLVENT_LLM_TIMEOUT Request timeout in seconds for that endpoint (default 30)
NEMOTRON_MODEL Nemotron model override (default: nvidia/llama-3.1-nemotron-ultra-253b-v1)
SOLVENT_DELIVERY_SECRET HMAC token secret for /briefs/{job_id}; at least 32 characters, high entropy
SOLVENT_SKIP_ONBOARD Set to 1 to skip the first-run wizard
SOLVENT_ALLOW_POLL When set to 1/true/yes, actively poll Stripe Checkout Sessions for payment status instead of awaiting webhook confirmation (default: off)
SOLVENT_ASYNC Run job fulfillment asynchronously instead of blocking on payment polling (default: off / synchronous)
SOLVENT_LIVE_SEARCH Enable live web search integration in the agent chat loop (default: off)
SOLVENT_LOG_JSON Emit structured JSON log lines to stderr in addition to the log file (default: off)
SOLVENT_UPDATE_CHECK Opt-in: run a background version-update hint on CLI startup when set to 1/true/yes
SOLVENT_NO_UPDATE_CHECK Set to any value to suppress the background version-update hint
SOLVENT_WORKSPACE Override path for the agent workspace directory (SOUL/BRAIN/AGENTS files)
SOLVENT_WORKSPACE_MAX_CHARS Max characters loaded per workspace context file (default 8000)
SOLVENT_WORKSPACE_TOTAL_MAX_CHARS Max total characters across all workspace context files (default 40000)
SMTP_HOST SMTP server hostname. When empty (default), brief delivery is simulated — research briefs are written to the outbox directory instead of emailed. When set, briefs are emailed to the customer
SMTP_PORT SMTP server port (default 587)
SMTP_USER SMTP authentication username
SMTP_PASS SMTP authentication password
SMTP_FROM "From" address for outgoing brief emails (default: SMTP_USER, else agent@solvent.local)

Product/Price objects are cached in .solvent/stripe_catalog.json so repeated runs reuse a single SOLVENT Research Brief product instead of cluttering your Stripe dashboard.


💬 Telegram (conversational channel)

Full chat on Telegram with OpenClaw-style pairing and Hermes-style tool/memory patterns. See docs/TELEGRAM.md.

pip install "solvent-agent[telegram]"
export TELEGRAM_BOT_TOKEN=...

python -m solvent serve &    # Stripe webhooks + checkout
python -m solvent worker &   # fulfill jobs
python -m solvent telegram     # long-poll bot

python -m solvent doctor       # diagnostics
python -m solvent pairing list # pending DM codes

Users pair via /start, commission briefs in natural language, receive checkout links, and get push updates when jobs are paid and delivered.

Personality and operating rules come from the agent workspace (SOUL.md, BRAIN.md, AGENTS.md) — see docs/WORKSPACE.md.


🧪 Tests

pip install "solvent-agent[dev]"
python3 -m pytest tests/ -v
ruff check solvent tests run_demo.py

Unit tests cover: pricing & margin gate · guardrail policy · treasury ledger · Stripe client (simulate + test mode) · config/onboarding.


📁 Repository Layout

solvent/
  __main__.py      `python -m solvent` / `solvent` command dispatcher
  cli.py           demo / interactive CLI (`solvent` with no subcommand)
  agent.py         the orchestrator (earn → fulfil → spend → book)
  stages.py        idempotent stage machine (quote→paid→fulfill→deliver→spend)
  treasury.py      SQLite ledger / balance sheet
  pricing.py       the margin gate (+ counter-offers on a decline)
  quote_cmd.py     `solvent quote` — dry-run the margin gate
  guardrails.py    NemoClaw-style spend policy (caps · vendor budgets · velocity)
  guardrail_cmd.py `solvent guardrails` — policy in force + vendor exposure
  backlog.py       capital-aware job prioritisation (`solvent backlog`)
  calibration.py   realized COGS → cost-model calibration (`solvent costs`)
  customers.py     lifetime value and repeat rate (`solvent customers`)
  simulate.py      policy simulator over synthetic demand (`solvent simulate`)
  optimize.py      policy search under a risk budget (`solvent optimize`)
  checkout.py      payment reminders and link expiry (`solvent checkouts`)
  intake.py        commercial screen on inbound jobs (`solvent intake`)
  capacity.py      throughput ceiling and binding rule (`solvent capacity`)
  alerts.py        health sweep with exit codes (`solvent alerts`)
  export.py        books export + period close (`solvent export`)
  products.py      the price list (`solvent products`)
  quality.py       deliverable quality gate (`solvent quality`)
  review.py        operator approval queue (`solvent review`)
  funnel.py        order funnel and win rate (`solvent funnel`)
  turnaround.py    measured delivery time (`solvent turnaround`)
  ops.py           operator-only business commands on the chat surface
  stripe_client.py two-sided Stripe layer (earn + spend)
  nemotron.py      NVIDIA Nemotron client (+ offline stub)
  service.py       the product: an on-demand research brief
  pdf.py           dependency-free PDF rendering of the brief
  jobs.py          sample inbound work
  dashboard.py     renders the treasury to HTML + JSON
  finance.py       income statement · unit economics · runway · forecast
  config.py        onboarding wizard and config persistence
  server.py        FastAPI webhooks + job API + hosted briefs (serve)
  worker.py        async job processor + resume incomplete jobs
  gateway.py       channel router (Telegram → chat sessions)
  chat.py          conversational loop + business tools
  memory.py        Hermes-style session memory
  doctor.py        stack diagnostics
  workspace.py     SOUL/BRAIN/AGENTS prompt assembly
  channels/        Telegram long-poll adapter
run_demo.py        the full business loop (CLI entry point)
tests/             pytest suite
docs/              screenshots and supporting docs

🔬 Research Use

SOLVENT's spend policy is the label source for SpendAudit, a benchmark that asks whether an LLM auditor reaches the same verdict as the policy, and whether a prompt injection in a payment memo changes that verdict. The benchmark pins v0.2.2 (bdc896a), and its generator rebuilds the dataset byte for byte from that commit. The working paper, every model response, and a one-command recount of the results are in that repository.

Headline result from the working paper: requiring rule-by-rule evidence cut LLM auditors' false approvals sharply (for example 34 → 6 on clean traces for Gemini 3.1 Flash-Lite), but an injected payment memo still got violating payments approved for every model tested. Results, data and citation details are in the SpendAudit README.


🏆 Built For

Hermes Agent Accelerated Business Hackathon — NVIDIA × Stripe × Nous Research

The agent was designed to demonstrate:

  • An agent that is economically self-aware — it has a treasury, prices against its own costs, and gates every action on projected profit
  • A complete two-sided Stripe integration — earns via Payment Links, spends via Issuing virtual cards
  • Provable spend safety — a NemoClaw-style policy sandbox that makes "give an agent a payment credential" a reasonable thing to do
  • Live inference with NVIDIA Nemotron — the offline stub means the demo always works, even without API keys

🤝 Contributing

Issues, PRs, and ideas are very welcome. Some good starting points:

  • Add more sample research topics in solvent/jobs.py
  • Improve the Nemotron prompt template in solvent/service.py
  • Add a new guardrail policy to solvent/guardrails.py
  • Extend the dashboard with charts or new metrics in solvent/dashboard.py

If SOLVENT gave you ideas, give it a ⭐

About

Offline-first Python AI agent that runs a tiny research business: quotes each job against its own costs, collects via Stripe, fulfils with NVIDIA Nemotron, pays vendors within a spend policy, and declines unprofitable work.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

114 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages