Files
db/specs/billingmanager_research/billingmanager.md
2025-08-21 17:26:40 +02:00

346 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

### 2.1 Accounts
* **id**: `BIGINT` identity (non-negative), unique account id
* **pubkey**: `BYTEA` unique public key for signing/encryption
* **display\_name**: `TEXT` (optional)
* **created\_at**: `TIMESTAMPTZ`
### 2.2 Currencies
* **asset\_code**: `TEXT` PK (e.g., `USDC-ETH`, `EUR`, `LND`)
* **name**: `TEXT`
* **symbol**: `TEXT`
* **decimals**: `INT` (default 2)
---
## 3) Services & Groups
### 3.1 Services
* **id**: `BIGINT` identity
* **name**: `TEXT` unique
* **description**: `TEXT`
* **default\_billing\_mode**: `ENUM('per_second','per_request')`
* **default\_price**: `NUMERIC(38,18)` (≥0)
* **default\_currency**: FK → `currencies(asset_code)`
* **max\_request\_seconds**: `INT` (>0 or `NULL`)
* **schema\_heroscript**: `TEXT`
* **schema\_json**: `JSONB`
* **created\_at**: `TIMESTAMPTZ`
#### Accepted Currencies (per service)
* **service\_id**: FK → `services(id)`
* **asset\_code**: FK → `currencies(asset_code)`
* **price\_override**: `NUMERIC(38,18)` (optional)
* **billing\_mode\_override**: `ENUM` (optional)
Primary key: `(service_id, asset_code)`
### 3.2 Service Groups
* **id**: `BIGINT` identity
* **name**: `TEXT` unique
* **description**: `TEXT`
* **created\_at**: `TIMESTAMPTZ`
#### Group Memberships
* **group\_id**: FK → `service_groups(id)`
* **service\_id**: FK → `services(id)`
Primary key: `(group_id, service_id)`
---
## 4) Providers & Runners
### 4.1 Service Providers
* **id**: `BIGINT` identity
* **account\_id**: FK → `accounts(id)` (the owning account)
* **name**: `TEXT` unique
* **description**: `TEXT`
* **created\_at**: `TIMESTAMPTZ`
#### Providers Offer Groups
* **provider\_id**: FK → `service_providers(id)`
* **group\_id**: FK → `service_groups(id)`
Primary key: `(provider_id, group_id)`
#### Provider Pricing Overrides (optional)
* **provider\_id**: FK → `service_providers(id)`
* **service\_id**: FK → `services(id)`
* **asset\_code**: FK → `currencies(asset_code)` (nullable for currency-agnostic override)
* **price\_override**: `NUMERIC(38,18)` (optional)
* **billing\_mode\_override**: `ENUM` (optional)
* **max\_request\_seconds\_override**: `INT` (optional)
Primary key: `(provider_id, service_id, asset_code)`
### 4.2 Runners
* **id**: `BIGINT` identity
* **address**: `INET` (must be IPv6)
* **name**: `TEXT`
* **description**: `TEXT`
* **pubkey**: `BYTEA` (optional)
* **created\_at**: `TIMESTAMPTZ`
#### Runner Ownership (many-to-many)
* **runner\_id**: FK → `runners(id)`
* **provider\_id**: FK → `service_providers(id)`
Primary key: `(runner_id, provider_id)`
#### Routing (provider → service/service\_group → runners)
* **provider\_service\_runners**: `(provider_id, service_id, runner_id)` PK
* **provider\_service\_group\_runners**: `(provider_id, group_id, runner_id)` PK
---
## 5) Subscriptions & Spend Control
A subscription authorizes an **account** to use either a **service** **or** a **service group**, with optional spend limits and allowed providers.
* **id**: `BIGINT` identity
* **account\_id**: FK → `accounts(id)`
* **service\_id** *xor* **group\_id**: FK (exactly one must be set)
* **secret**: `BYTEA` (random, provided by subscriber; recommend storing a hash)
* **subscription\_data**: `JSONB` (free-form)
* **limit\_amount**: `NUMERIC(38,18)` (optional)
* **limit\_currency**: FK → `currencies(asset_code)` (optional)
* **limit\_period**: `ENUM('hour','day','month')` (optional)
* **active**: `BOOLEAN` default `TRUE`
* **created\_at**: `TIMESTAMPTZ`
#### Allowed Providers per Subscription
* **subscription\_id**: FK → `subscriptions(id)`
* **provider\_id**: FK → `service_providers(id)`
Primary key: `(subscription_id, provider_id)`
**Intended Use:**
* Subscribers bound spending by amount/currency/period.
* Merchant (provider) can claim charges for requests fulfilled under an active subscription, within limits, and only if listed in `subscription_providers`.
---
## 6) Requests & Billing
### 6.1 Request Lifecycle
* **id**: `BIGINT` identity
* **account\_id**: FK → `accounts(id)`
* **subscription\_id**: FK → `subscriptions(id)`
* **provider\_id**: FK → `service_providers(id)`
* **service\_id**: FK → `services(id)`
* **runner\_id**: FK → `runners(id)` (nullable)
* **request\_schema**: `JSONB` (payload matching `schema_json`/`schema_heroscript`)
* **started\_at**, **ended\_at**: `TIMESTAMPTZ`
* **status**: `ENUM('pending','running','succeeded','failed','canceled')`
* **created\_at**: `TIMESTAMPTZ`
### 6.2 Billing Ledger (append-only)
* **id**: `BIGINT` identity
* **account\_id**: FK → `accounts(id)`
* **provider\_id**: FK → `service_providers(id)` (nullable)
* **service\_id**: FK → `services(id)` (nullable)
* **request\_id**: FK → `requests(id)` (nullable)
* **amount**: `NUMERIC(38,18)` (debit = positive, credit/refund = negative)
* **asset\_code**: FK → `currencies(asset_code)`
* **entry\_type**: `ENUM('debit','credit','adjustment')`
* **description**: `TEXT`
* **created\_at**: `TIMESTAMPTZ`
**Balances View (example):**
* `account_balances(account_id, asset_code, balance)` as a view over `billing_ledger`.
---
## 7) Pricing Precedence
When computing the **effective** pricing, billing mode, and max duration for a `(provider, service, currency)`:
1. **Provider override for (service, asset\_code)** — if present, use it.
2. **Service accepted currency override** — if present, use it.
3. **Service defaults** — fallback.
If `billing_mode` or `max_request_seconds` are not overridden at steps (1) or (2), inherit from the next step down.
---
## 8) Key Constraints & Validations
* All identity ids are non-negative (`CHECK (id >= 0)`).
* Runner IPv6 enforcement: `CHECK (family(address) = 6)`.
* Subscriptions must point to **exactly one** of `service_id` or `group_id`.
* Prices and limits must be non-negative if set.
* Unique natural keys where appropriate: service names, provider names, currency asset codes, account pubkeys.
---
## 9) Mermaid Diagrams
### 9.1 EntityRelationship Overview
```mermaid
erDiagram
ACCOUNTS ||--o{ SERVICE_PROVIDERS : "owns via account_id"
ACCOUNTS ||--o{ SUBSCRIPTIONS : has
CURRENCIES ||--o{ SERVICES : "default_currency"
CURRENCIES ||--o{ SERVICE_ACCEPTED_CURRENCIES : "asset_code"
CURRENCIES ||--o{ PROVIDER_SERVICE_OVERRIDES : "asset_code"
CURRENCIES ||--o{ BILLING_LEDGER : "asset_code"
SERVICES ||--o{ SERVICE_ACCEPTED_CURRENCIES : has
SERVICES ||--o{ SERVICE_GROUP_MEMBERS : member_of
SERVICE_GROUPS ||--o{ SERVICE_GROUP_MEMBERS : contains
SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_GROUPS : offers
SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_OVERRIDES : sets
SERVICE_PROVIDERS ||--o{ RUNNER_OWNERS : owns
SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_RUNNERS : routes
SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_GROUP_RUNNERS : routes
RUNNERS ||--o{ RUNNER_OWNERS : owned_by
RUNNERS ||--o{ PROVIDER_SERVICE_RUNNERS : executes
RUNNERS ||--o{ PROVIDER_SERVICE_GROUP_RUNNERS : executes
SUBSCRIPTIONS ||--o{ SUBSCRIPTION_PROVIDERS : allow
SERVICE_PROVIDERS ||--o{ SUBSCRIPTION_PROVIDERS : allowed
REQUESTS }o--|| ACCOUNTS : by
REQUESTS }o--|| SUBSCRIPTIONS : under
REQUESTS }o--|| SERVICE_PROVIDERS : via
REQUESTS }o--|| SERVICES : for
REQUESTS }o--o{ RUNNERS : executed_by
BILLING_LEDGER }o--|| ACCOUNTS : charges
BILLING_LEDGER }o--o{ SERVICES : reference
BILLING_LEDGER }o--o{ SERVICE_PROVIDERS : reference
BILLING_LEDGER }o--o{ REQUESTS : reference
```
### 9.2 Request Flow (Happy Path)
```mermaid
sequenceDiagram
autonumber
participant AC as Account
participant API as Broker/API
participant PR as Provider
participant RU as Runner
participant DB as PostgreSQL
AC->>API: Submit request (subscription_id, service_id, payload, secret)
API->>DB: Validate subscription (active, provider allowed, spend limits)
DB-->>API: OK + effective pricing (resolve precedence)
API->>PR: Dispatch request (service, payload)
PR->>DB: Select runner (provider_service_runners / group runners)
PR->>RU: Start job (payload)
RU-->>PR: Job started (started_at)
PR->>DB: Update REQUESTS (status=running, started_at)
RU-->>PR: Job finished (duration, result)
PR->>DB: Update REQUESTS (status=succeeded, ended_at)
API->>DB: Insert BILLING_LEDGER (debit per effective price)
DB-->>API: Ledger entry id
API-->>AC: Return result + charge info
```
### 9.3 Pricing Resolution
```mermaid
flowchart TD
A[Input: provider_id, service_id, asset_code] --> B{Provider override exists for (service, asset_code)?}
B -- Yes --> P1[Use provider price/mode/max]
B -- No --> C{Service accepted currency override exists?}
C -- Yes --> P2[Use service currency price/mode]
C -- No --> P3[Use service defaults]
P1 --> OUT[Effective pricing]
P2 --> OUT
P3 --> OUT
```
---
## 10) Operational Notes
* **Secrets:** store a hash (e.g., `digest(secret,'sha256')`) rather than raw `secret`. Keep the original only client-side.
* **Limits enforcement:** before insert of a debit ledger entry, compute period window (hour/day/month UTC or tenant TZ) and enforce `SUM(amount) + new_amount ≤ limit_amount`.
* **Durations:** enforce `max_request_seconds` (effective) at orchestration and/or via DB trigger on `REQUESTS` when transitioning to `running/succeeded`.
* **Routing:** prefer `provider_service_runners` when a request targets a service directly; otherwise use the union of runners from `provider_service_group_runners` for the group.
* **Balances:** serve balance queries via the `account_balances` view or a materialized cache updated by triggers/jobs.
---
## 11) Example Effective Pricing Query (sketch)
```sql
-- Inputs: :provider_id, :service_id, :asset_code
WITH p AS (
SELECT price_override, billing_mode_override, max_request_seconds_override
FROM provider_service_overrides
WHERE provider_id = :provider_id
AND service_id = :service_id
AND (asset_code = :asset_code)
),
sac AS (
SELECT price_override, billing_mode_override
FROM service_accepted_currencies
WHERE service_id = :service_id AND asset_code = :asset_code
),
svc AS (
SELECT default_price AS price, default_billing_mode AS mode, max_request_seconds
FROM services WHERE id = :service_id
)
SELECT
COALESCE(p.price_override, sac.price_override, svc.price) AS effective_price,
COALESCE(p.billing_mode_override, sac.billing_mode_override, svc.mode) AS effective_mode,
COALESCE(p.max_request_seconds_override, svc.max_request_seconds) AS effective_max_seconds;
```
---
## 12) Indices (non-exhaustive)
* `services(default_currency)`
* `service_accepted_currencies(service_id)`
* `provider_service_overrides(service_id, provider_id)`
* `requests(account_id)`, `requests(provider_id)`, `requests(service_id)`
* `billing_ledger(account_id, asset_code)`
* `subscriptions(account_id) WHERE active`
---
## 13) Migration & Compatibility
* Prefer additive migrations (new columns/tables) to avoid downtime.
* Use `ENUM` via `CREATE TYPE`; when extending, plan for `ALTER TYPE ... ADD VALUE`.
* For high-write ledgers, consider partitioning `billing_ledger` by `created_at` (monthly) and indexing partitions.
---
## 14) Non-Goals
* Wallet custody and on-chain settlement are out of scope.
* SLA tracking and detailed observability (metrics/log schema) are not part of this spec.
---
## 15) Acceptance Criteria
* Can represent services, groups, and providers with currency-specific pricing.
* Can route requests to runners by service or group.
* Can authorize usage via subscriptions, enforce spend limits, and record charges.
* Can reconstruct balances and audit via append-only ledger.
---
**End of Spec**