# Billing, accounts, and LLM modes

For **users** (account cabinet, Cursor setup) and **vendors** (AccountServer config, pricing, payments). ReasonService admin — see [ADMIN.md](../ADMIN.md#веб-админка-и-accountbilling-сервер) (RU).

---

## Why you need a key or LLM mode

Every VeriThink request (`reason_plan`, `reason_check`, etc.) calls the LLM multiple times. Without a working provider, reasoning degrades to a stub or fails with a configuration error.

**Important for Cursor:** the IDE **does not support MCP sampling**. `VERITHINK_LLM_PROVIDER=mcp-sampling` in Cursor **does not work** — the client falls back to a stub and you get no real reasoning. Do not rely on “no key via sampling” in deeplinks and sample `mcp.json` files.

For **real** reasoning in Cursor, pick one of:

| Option | Summary |
|--------|---------|
| **BYOK** | Your own API key (`openai`, `anthropic`, `gemini`) |
| **local** | Local model (Ollama, LM Studio) — free, no cloud key |
| **service** | Vendor keys via AccountServer + billing (account and token required) |
| **fake** | Dev/test only — deterministic responses, no network |

The three user-facing cabinet modes are below. Operator setup — [ADMIN.md](../ADMIN.md) (RU).

---

## Three LLM modes (cabinet and ReasonService)

In the **account cabinet** (`tools/AccountServer`, `/v1/account/settings`) and when **ReasonService** runs in `service` mode:

| Mode | Who pays | Own API key | How to configure |
|------|----------|-------------|------------------|
| **BYOK** (`byok`) | User (to their provider) | Yes — key stored encrypted on AccountServer | Cabinet → “BYOK” → provider, key, base URL. ReasonService: `VERITHINK_LLM_PROVIDER=service` + account token **or** directly `openai`/`anthropic`/`gemini` + `VERITHINK_LLM_API_KEY` in [admin.html](http://127.0.0.1:8787/admin.html) |
| **local** | Nobody (local inference) | No | Cabinet → “Local model” → base URL (default `http://127.0.0.1:11434/v1`). ReasonService: `VERITHINK_LLM_PROVIDER=local` + `VERITHINK_LLM_BASE_URL` |
| **service** | Vendor (`VERITHINK_VENDOR_*` keys) → user (balance after free quota) | No for the user | Register in cabinet → token → `VERITHINK_LLM_PROVIDER=service`, `VERITHINK_ACCOUNT_URL`, `VERITHINK_ACCOUNT_TOKEN` |

With **service**, ReasonService calls AccountServer gateway `POST /v1/llm/complete` (`ServiceGatewayLlmClient`). The gateway uses the account **mode** from DB (`llm_mode`): for `service` — vendor keys from env; for `byok`/`local` — user settings from the cabinet.

---

## How service billing works

### Chain: free quota → balance → markup

1. **Free quota** — `VERITHINK_FREE_QUOTA` (default **10**). On registration the account gets that many free **requests** in `service` mode (not tokens). Each successful gateway call with `freeQuotaRemaining > 0` consumes **1 request**; no USD charge (`source: free` in usage).
2. **Balance** — after quota is used, charges come from `balance_usd` at actual cost (`source: balance`).
3. **Markup** — applied to **all** providers and models via `VERITHINK_BILLING_MARKUP` (default **1.30**, i.e. **+30%** over base prices in `pricing.json`). Markup is set at registration and stored in `markup`.

**byok** and **local** through the gateway are **not billed** (balance and quota unchanged).

### Cost formula

Base price comes from `tools/AccountServer/pricing.json` (USD per **1000** input/output tokens). Unknown models use the `default` block.

```
cost_usd = (input_tokens / 1000 × price_in + output_tokens / 1000 × price_out) × markup
```

**Example:** model `gpt-4o`, `markup = 1.30`, `1000` input + `500` output:

- Base: `1000/1000 × 0.0025 + 500/1000 × 0.01 = 0.0025 + 0.005 = 0.0075` USD  
- With markup: `0.0075 × 1.30 = **0.00975**` USD  

With `VERITHINK_BILLING_TEST_MODE=1` the gateway returns a stub response with fixed `10` / `20` tokens — same formula, useful for local tests without vendor keys.

### HTTP 402 `insufficient_balance`

The gateway returns **402** when:

- mode is `service`, quota = 0, balance = 0 **before** the LLM call; or  
- after the LLM call, calculated `cost_usd` exceeds current balance.

Response body (JSON):

```json
{
  "error": "insufficient_balance",
  "requiredUsd": 0.00975,
  "balanceUsd": 0.0
}
```

ReasonService surfaces a clear error: top up balance in the cabinet.

---

## Pricing table

Base prices (before markup), USD per **1000** tokens. User-facing price = value × `markup` (e.g. ×1.30).

### OpenAI

| Model | Input ($/1K) | Output ($/1K) |
|-------|--------------|---------------|
| `gpt-4o` | 0.0025 | 0.01 |
| `gpt-4o-mini` | 0.00015 | 0.0006 |
| `gpt-4.1` | 0.002 | 0.008 |
| `gpt-4.1-mini` | 0.0004 | 0.0016 |
| `o3-mini` | 0.0011 | 0.0044 |

### Anthropic

| Model | Input ($/1K) | Output ($/1K) |
|-------|--------------|---------------|
| `claude-3-5-sonnet` | 0.003 | 0.015 |
| `claude-3-5-haiku` | 0.0008 | 0.004 |
| `claude-3-opus` | 0.015 | 0.075 |

### Gemini

| Model | Input ($/1K) | Output ($/1K) |
|-------|--------------|---------------|
| `gemini-1.5-pro` | 0.00125 | 0.005 |
| `gemini-1.5-flash` | 0.000075 | 0.0003 |
| `gemini-2.0-flash` | 0.0001 | 0.0004 |

### Fallback (unknown model)

| | Input ($/1K) | Output ($/1K) |
|--|--------------|---------------|
| **default** | 0.001 | 0.003 |

### Where to change prices and markup

| What | Where |
|------|-------|
| Model prices | `tools/AccountServer/pricing.json` — edit file and **restart** AccountServer |
| Default markup for **new** accounts | env `VERITHINK_BILLING_MARKUP` (default `1.30`) |
| Existing account markup | `markup` field in SQLite (copied from env at registration) |
| Live table with markup for UI | `GET /v1/pricing` or “Pricing” block in cabinet |

---

## AccountServer: API and cabinet

Service: `tools/AccountServer`, default `http://127.0.0.1:8091/` (port `VERITHINK_ACCOUNT_PORT`).

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/v1/auth/register` | Register → `{ accountId, token }` |
| POST | `/v1/auth/login` | Login → `{ token }` |
| GET | `/v1/account` | Profile, LLM mode, balance, quota |
| POST | `/v1/account/settings` | `llmMode`, BYOK key (encrypted), base URL |
| GET | `/v1/billing/balance` | Balance, quota, markup |
| POST | `/v1/billing/topup` | `{ amountUsd }` — top-up via payment provider |
| GET | `/v1/usage` | Last 50 usage entries |
| GET | `/v1/pricing` | Pricing table |
| POST | `/v1/llm/complete` | LLM gateway (Bearer or `X-Account-Token`) |
| GET | `/healthz` | Health check |

**Web cabinet:** static `wwwroot/index.html` — balance, LLM mode, top-up, usage, pricing, ready-made env / `mcp.json` snippets.

---

## Payment providers

`IPaymentProvider` interface; selection via `VERITHINK_PAYMENT_PROVIDER`.

| Value | Behavior |
|-------|----------|
| **`manual`** (default) | Test provider: instantly returns `status: completed`, balance credited without real payment |
| **`stripe`** | **Stub** — `ChargeAsync` throws `NotSupportedException`: “Stripe not configured: set VERITHINK_STRIPE_KEY and implement checkout” |

### Subscriptions (coming)

<!-- TODO: document subscription tiers, renewal, and Stripe Customer Portal when implemented -->

Recurring plans and subscription management are **not shipped in v0.3**. Use balance top-up (`POST /v1/billing/topup`) for now. Track roadmap in repo docs (RU).

### Only unfinished integration: real payments

To accept money, the vendor must:

1. Set `VERITHINK_PAYMENT_PROVIDER=stripe` and secret **`VERITHINK_STRIPE_KEY`** (extension point in code; variable not yet in `.env.example` but required by implementation).
2. **Implement** `StripePaymentProvider.ChargeAsync` — Checkout Session or Payment Intent, confirmation webhook, idempotency.
3. Record successful payments in `payments` table (as `manual` does after `completed`).

Until then, use **`manual` only for dev/staging**, **lava.top subscriptions**, **USDT TRC-20**, or external billing.

---

## Pro subscriptions (lava.top)

AccountServer accepts Pro subscriptions and top-up packages via [lava.top](https://lava.top). LicenseServer (internal Docker service, port 8090) issues Pro keys; AccountServer talks to LicenseServer over HTTP only.

See the Russian billing doc for full env tables, webhook URLs (`https://{host}/account/v1/billing/webhook/lava`), and API paths — [BILLING.md](../BILLING.md#подписки-pro-lavatop).

---

## USDT crypto (TRC-20)

Alternative to lava.top: users send **USDT** on **TRON (TRC-20)** to the vendor wallet. AccountServer assigns a **unique amount** (base price + 0.001–0.999 USDT suffix) to match incoming transfers.

| Variable | Purpose |
|----------|---------|
| `VERITHINK_CRYPTO_USDT_TRC20_ADDRESS` | TRON wallet (enables crypto when set) |
| `VERITHINK_CRYPTO_PRICES` | `pro-month:20;pro-year:200;topup-10:10;…` (USDT) |
| `VERITHINK_CRYPTO_API_URL` | TronGrid API (default `https://api.trongrid.io`) |
| `VERITHINK_ACCOUNT_ADMIN_TOKEN` | `X-Admin-Token` for `POST /v1/admin/crypto/confirm` |

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/v1/billing/crypto/invoice` | Create subscription or top-up invoice |
| GET | `/v1/billing/crypto/invoice/{id}` | Invoice status |
| POST | `/v1/billing/crypto/claim` | Link txid after transfer |
| POST | `/v1/admin/crypto/confirm` | Admin manual confirm |

Background `CryptoPaymentWatcher` polls TronGrid every 60s. Cabinet block: **Pay with USDT (TRC-20)**.

Full details: [BILLING.md](../BILLING.md#крипто-usdt-trc-20) (RU).

---

## Security

| Object | Mechanism |
|--------|-----------|
| **BYOK API keys** | AES-GCM (`SecretBox`), encryption key from `VERITHINK_SECRET_KEY` (SHA-256 of string). Without the same secret, old ciphertext cannot be decrypted on another instance |
| **Passwords** | PBKDF2-SHA256, 100,000 iterations, per-account salt |
| **Access tokens** | 32 random bytes; DB stores SHA-256 hash only; header `Authorization: Bearer …` or `X-Account-Token` |
| **Recommendations** | Set unique `VERITHINK_SECRET_KEY` in production; **do not commit** `VERITHINK_ACCOUNT_DB` (SQLite `./.verithink/accounts.db` by default); do not store vendor keys in git |

---

## See also

- [ADMIN.md — Web admin and Account/Billing server](../ADMIN.md#веб-админка-и-accountbilling-сервер) (RU) — run, env, local billing test
- [USER.md — Account cabinet and billing modes](USER.md#account-cabinet-and-billing-modes) — register, connect Cursor
- [README.en.md](../../README.en.md) — quickstart and service ports
