Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Plans and pricing

A plan is the smallest billable unit: a price + a billing period + a set of quotas + a tier. Every active service has one or more plans; subscriptions reference plans by ID.

Schema#

tsql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
plans
  service_id (FK service_registry),
  slug,                       -- unique within service (trial, starter, pro, enterprise)
  name,                       -- human label
  description,
  tier,                       -- trial | starter | pro | enterprise (or your own)
  billing_period,             -- monthly | yearly | quarterly | one_time
  base_price_cents,           -- in cents, currency below
  currency,                   -- ISO 4217
  features,                   -- JSON {items: [string], unit: string}
  quotas,                     -- JSON service-specific limits
  is_active, is_public,
  trial_days,                 -- 0 means no auto-trial on signup
  cost_multiplier,            -- partner-cost markup (rarely used)
  sort_order, metadata

plan_usage_pricing
  plan_id, metric_slug,
  tier_start, tier_end,       -- usage tier brackets
  unit_price_cents,
  unit_label, unit_divisor

Tier conventions#

The four canonical tier values map to standard product expectations:

Tier Use
trial 14-day free trial. base_price_cents = 0, trial_days = 14, low quotas.
starter Entry paid tier. Small quotas, basic features.
pro Mainstream paid tier. The "sweet spot" most customers land on.
enterprise Top tier. Often custom-quoted; the plan in the DB is a reference price.

Tier names are free-form strings — these are just the conventions ScaiLabs' own services use.

Quotas#

plans.quotas is service-specific JSON. Examples:

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ScaiKey
{ "monthly_active_users": 10000 }

// ScaiVault
{ "secrets": 25000 }

// ScaiSend
{ "monthly_emails": 100000 }

// ScaiFlow
{ "monthly_runs": 250000, "active_workflows": 50 }

A quota value of 0 (or absent) is the conventional "unlimited" sentinel for Enterprise tiers.

Quotas are enforced by each service itself, not by ScaiControl. ScaiControl publishes them; the service polls (or webhooks back) for the current plan_id and reads its own quota.

Pricing normalisation#

For events and reports, prices are normalised to monthly (MRR):

billing_period MRR formula
monthly base_price_cents
yearly base_price_cents / 12
quarterly base_price_cents / 3
weekly base_price_cents * 4
daily base_price_cents * 30
one_time base_price_cents (pass-through; consumer interprets)

services/events/builders.py:_normalize_mrr() is the canonical implementation; subscription event payloads (subscription.activated.v1 etc.) carry the normalised value as mrr_amount_cents.

Plan keys for stable referencing#

Plan IDs are UUIDs, which churn between re-seeds. For event payloads and cross-system references, ScaiControl emits a composite plan_key of the form:

scdoc
1
<service_slug>.<plan_slug>

Examples: scaikey.starter, scaivault.pro, scaiwave.enterprise. The plan_key is stable across re-seeds (assuming you keep slug conventions consistent), while plan_id is still emitted alongside for the database-level link.

Usage-based pricing#

For metered services (API calls, storage GB, model invocations), plan_usage_pricing adds tiered overage:

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
PlanUsagePricing(
    plan_id="<plan-id>",
    metric_slug="api_calls",
    tier_start=0,           # First N free under the plan's base price
    tier_end=100_000,       # ...up to 100k
    unit_price_cents=1,
    unit_label="call",
    unit_divisor=1,
)
PlanUsagePricing(
    plan_id="<plan-id>",
    metric_slug="api_calls",
    tier_start=100_000,
    tier_end=None,          # Open-ended above 100k
    unit_price_cents=2,     # 2¢ per call beyond
)

The monthly billing job (services/billing/invoice.py:generate_monthly_invoices) joins this with the metering data (usage_events, partitioned by month) to compute overage lines.

Editing plans without orphaning subscriptions#

Best practice:

  • Never delete a plan that has live subscriptions. Make it is_active = false and is_public = false so it stops appearing in the catalog, but existing subscriptions keep working.
  • Price changes only take effect on the next billing cycle. Already-finalised invoices snapshot their VAT but not their plan price (the line item carries the exact unit_price_cents). So an active subscription whose plan price changed mid-month bills the new price on the next cycle.
  • Trial-day changes apply only to newly-created subscriptions. An in-flight trialing subscription keeps its original trial_ends_at.

Admin surface#

  • List plans for a service: GET /catalog/services/{slug}/plans (also returned in GET /admin/registry/services with ?expand=plans).
  • Create/edit plans: POST/PUT /admin/registry/services/{slug}/plans (see Admin — service registry).
  • The admin UI exposes plan management on each service's detail page.

See also#

Updated 2026-05-18 01:48:39 View source (.md) rev 2