...
This commit is contained in:
345
specs/billingmanager_research/billingmanager.md
Normal file
345
specs/billingmanager_research/billingmanager.md
Normal file
@@ -0,0 +1,345 @@
|
||||
|
||||
### 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 Entity–Relationship 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**
|
Reference in New Issue
Block a user