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

12 KiB
Raw Blame History

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

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)

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

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)

-- 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