Billing & Credit Architecture

A PSP-agnostic credit ledger
built for global SaaS scale.

Every credit is sourced from the database, never hardcoded. Every payment processor (Stripe today; Razorpay, Chargebee, Paddle on the roadmap) plugs in without touching the ledger. Internal cost numbers stay internal. The customer sees a stable, predictable bill grounded in 20% of equivalent manual labour cost. 99%+ gross margin at scale, idempotent reservations, zero charge for failed jobs.

0 hardcoded credit constants 20% value-capture rate (default) 99%+ gross margin at scale 5 customer tiers, 10 complexity factors 40+ tax jurisdictions on day one

Section 1 · Why a credit architecture matters

The four SaaS billing landmines, sidestepped. CXO Finance

Most usage-based SaaS billing collapses under one of four pressures: (a) tight coupling to the payment processor forcing a rebuild when expanding to a new region; (b) leaking internal cost into customer-facing surfaces; (c) bills swinging wildly on legitimately-large jobs; (d) double-charging on payment-processor retries. Arcitopsia's design tackles each one explicitly.

Decoupled PSP

Ledger has zero Stripe imports. Razorpay, Chargebee, Paddle add new handler files — never touch the core.

Two layers

Internal cost & gross margin live in WorkflowExecutionTelemetry. Customer ledger lives in CreditTransaction. The two never cross.

log₂ dampening

10 runtime factors, weighted, normalised, then logarithmic dampening. Captures real cost variation without runaway bills.

Idempotent

@@unique(executionId, type) on every ledger row. Stripe webhook re-delivery is a no-op. Settle called twice → second call returns {alreadySettled:true}.

Section 2 · PSP-Agnostic Invariant (PAYG §9.6)

The single foundational rule. Zero Stripe imports in the ledger. Architect

Per PAYG §9.6: the credit ledger (credit-engine.ts, credit-service.ts, complexity-calculator.ts, and the data-model tables themselves) contains zero PSP-specific types, IDs, or imports. Stripe is isolated to stripe-credits.ts + the webhook handler file. This is checked mechanically in the Phase 3 hand-test by grepping every file under lib/billing/ for import.*stripe; exactly one file matches.

flowchart LR
    subgraph PSP["PSP Layer (isolated)"]
        SC[stripe-credits.ts]:::psp
        WH[webhooks/stripe/route.ts]:::psp
    end

    subgraph CORE["Credit Ledger Core — PSP-AGNOSTIC"]
        CE[credit-engine.ts
reserve / settle / release]:::core CS[credit-service.ts
DB-driven config lookup]:::core CC[complexity-calculator.ts
10-factor + log₂]:::core DB[(Tables
CreditTransaction
TenantCreditBalance
CreditReservation)]:::db end subgraph FUTURE["Future PSPs (drop-in, v1.5+)"] RZ[razorpay-credits.ts
v1.5 India]:::future CB[chargebee-bridge.ts
v2 Enterprise AR]:::future PD[paddle-credits.ts
v3 MOR]:::future end WH --> SC SC -- "topUpCreditsFromPurchase(
purchaseId, pspPaymentId)" --> CE CE --> CS CE --> CC CE --> DB RZ -. "same entry point" .-> CE CB -. "same entry point" .-> CE PD -. "same entry point" .-> CE classDef psp fill:#3f1d1d,stroke:#fb7185,color:#fff; classDef core fill:#0d1a0d,stroke:#a3e635,color:#fff; classDef db fill:#0d1a0d,stroke:#a3e635,color:#fff,stroke-dasharray:4 2; classDef future fill:#1e2a3a,stroke:#38bdf8,color:#fff,stroke-dasharray:4 2;

The PSP-agnostic boundary. Every PSP funnels into the same topUpCreditsFromPurchase entry point — and that function knows nothing about Stripe.

Why this matters operationally. Adding Razorpay for India in v1.5 is approximately one new file + one new webhook route + one IAM secret reference. No schema change, no ledger change, no migration. Same for Chargebee and Paddle. Engineering cost: ~2 weeks per PSP. Revenue unlock: a whole new region.

Section 3 · Two-Layer Measurement (PAYG §7.0)

Internal cost never reaches the customer's screen. CXO Architect

Every probe execution writes to two parallel layers. The internal layer (WorkflowExecutionTelemetry) captures iArchitron's actual cost — compute units, tokens, LLM cost, gross margin. The external layer (CreditTransaction) records the customer-facing deduction. Code that reads one never reads the other.

Layer Audience Table Records API surface
Internal telemetry iArchitron ops + finance WorkflowExecutionTelemetry compute units, tokens in/out/cached, storage bytes, LLM cost, internal cost, gross margin % /api/admin/billing/margin-analytics (admin-gated)
External credits Customer-facing CreditTransaction, TenantCreditBalance, CreditReservation fixed activity-type baseCredits × complexityMultiplier × tierMultiplier × globalMultiplier /api/billing/balance, /api/billing/transactions (tenant-scoped)
Enforcement: server-side, exhaustive. The /api/billing/config GET endpoint defines a selectClientSafe() that explicitly lists which columns leave the server — Stripe customer IDs and payment-method IDs are deliberately absent. The Phase 4 hand-test (PR #31) asserts they are absent from the response shape.

Section 4 · 20% Value-Capture Pricing (PAYG §5.0)

Credits priced at 20% of equivalent manual labour cost. CXO Sales

Every credit-bearing activity is priced as a fraction of the documented manual cost to produce the same outcome. This is the foundation of the pricing model and the 95–99% gross margin. Internal/LLM cost is irrelevant to what the customer is charged — what they're charged is anchored to their alternative.

baseCredits = round(manualCostBasisUSD × captureRatePct)
            where captureRatePct defaults to 0.20
            (negotiable 0.150.25 per TenantCreditContract)

Sampled activity prices (matches seed)

ActivityManual basisCaptureBase credits
Architecture Document (full)$4,000 · 40h × $100/hr EA Consultant0.20800
SOC2 / HIPAA / PCI Compliance Report$7,000 · 47h × $150/hr0.201,400
Full Compliance Assessment$2,000 · 13h × $150/hr0.20400
Architecture Simulation Run$1,000 · 10h × $100/hr0.20200
Code Generation (per component)$400 · 4h × $100/hr0.2080
IaC Generation (per module)$600 · 6h × $100/hr Sr. DevOps0.20120
Diagram Generation (per set)$300 · 3h × $100/hr EA Architect0.2060
Probe Discovery Run$500 · 5h × $100/hr0.20100
EA Artefact AI Draft$250 · 2.5h × $100/hr0.2050
Bulk Import (per 100 records)$50 · value-based not 20% capture1.00100
Customer-facing language. Show: "This run will use up to N credits ($N at base rate). Equivalent manual work: $M (Y hours of <role>)." Hide: token counts, LLM cost, compute milliseconds, gross margin. The dialog the customer sees at "Approve & Run Now" follows this contract exactly.

Section 5 · Customer Tier Multiplier (PAYG §5.8.3)

Five tiers. One multiplier. Applied at every deduction. CXO Sales Finance

Set once at contract time (TenantCreditContract.customerTierKey); applied multiplicatively to every credit deduction. Captures structural overhead (compliance automation, multi-region data residency, SLA cover, dedicated infrastructure) — independent of any runtime metric. Distinct from the marketing plan tier (Startup / Professional / Enterprise / Strategic / Global) which controls subscription price.

Tier Multiplier Structural overhead represented
INDIVIDUAL0.75Single-user, small graph, no compliance overlay, no SLA
SMB0.90Small team, moderate graph, occasional compliance
ENTERPRISE default1.00Multi-team, large graph (50K+ records), approval chains, compliance automation
MULTINATIONAL1.30Global ops, multi-region data residency, GDPR/SOX/HIPAA overlay, dedicated infra
MISSION_CRITICAL1.60Financial / Government / Healthcare, real-time audit, air-gapped, 99.9% SLA
Phase E pre-flight uses the actual tier. The Probe Analyzer's worst-case credit estimate at "Approve & Run Now" multiplies by the tenant's actual customerTierMultiplier, not 1.0. The displayed "max credits" matches what will be reserved, so MULTINATIONAL customers never see a number that's smaller than reality.

Section 6 · 10-Factor Complexity Multiplier (PAYG §5.8.4–5)

10 runtime dimensions. Weighted sum. log₂ dampening. Clamped. Architect Finance

Per-execution complexity is derived from 10 runtime dimensions captured by the workflow runner. Each is normalised against the activity's baseline profile, capped to prevent any one dimension from dominating, weight-summed, then logarithmically dampened. The result is multiplied onto the base credits.

The formula

// For each dimension i:
normalized_i  = actual_i / baseline_i              // baseline per activity
factorScore_i = min(normalized_i, capMultiplier_i) // per-factor cap
weightedSum  += factorScore_i × weight_i           // platform defaults sum to 1.00

complexityScore       = weightedSum / Σ(weight_i)
rawMultiplier         = log₂(complexityScore + 1) × 1.44   // scalingConstant
complexityMultiplier  = clamp(rawMultiplier,
                              contract.minComplexityMultiplier,  // default 0.5
                              contract.maxComplexityMultiplier)  // default 3.0

Default factor weights

factorKeyWeightCapNormalisation unit
child_count0.255.01 (count)
token_intensity0.224.01,000 (per-1K tokens)
context_size_kb0.153.0100 (per-100 KB)
wall_clock_ms0.102.51 (ms)
hierarchy_depth0.083.01 (level)
peak_concurrency0.062.01 (jobs)
model_tier0.055.01 (Haiku=1, Sonnet=2, Opus=5)
cache_miss_rate0.042.01.0 (=100% miss)
retry_count0.031.51 (retries)
external_api_calls0.021.51 (calls)
Σ weights1.00

Final credit formula

finalCredits = round(
  baseCredits
  × complexityMultiplier        // from runtime metrics (this section)
  × customerTierMultiplier      // from TenantCreditContract.customerTierKey (§5)
  × globalCreditMultiplier      // volume discount
  × (isByollm ? byollmMultiplier : 1.0)
)
Why log₂ dampening matters. A probe scanning 5,000 tables would naively trigger a 50× child-count multiplier vs the 100-target baseline. The log₂ curve plateaus this to ~5.7× before clamping — preventing runaway bills on legitimately-large scans. child_count + token_intensity together account for 47% of the weighting; both are heavily driven by probe behaviour, by design.

Section 7 · BYOLLM Economics (PAYG §5.6)

Bring your own LLM keys → reduced credit rate + 15% license discount. CXO Sales

When a tenant supplies their own LLM API keys, iArchitron does not bear LLM token cost. Credit prices are reduced by a fixed multiplier; a separate license-fee discount applies at the subscription layer. Gross margin on BYOLLM-discounted credits still exceeds 95% because internal cost drops to ~$3–8 (compute + storage only, no LLM).

SettingValueSource
Tenant.byollmEnabledBooleanColumn on Tenant
Tenant.byollmProviderANTHROPIC | OPENAI | GOOGLE_GEMINI | AWS_BEDROCK | AZURE_OPENAI | VERTEX_AIColumn on Tenant
TenantCreditContract.byollmCreditMultiplier0.62 default (negotiable 0.50–0.70)Reflects LLM cost being ~38% of internal cost
WorkflowCreditConfig.byollmCreditMultiplier0.62 default per activityPer-activity override possible
License-fee BYOLLM discountflat 15%Subscription billing (separate from credit ledger)
When BYOLLM makes economic sense. For most customers, no — the operational overhead of managing their own API quotas + billing relationships outweighs the credit savings. For very-high-volume customers (≥ $50K/month in credit consumption) and BYOLLM-mandated regulated industries (defence, classified government), the math flips. Sales engages on a per-customer basis to set the precise multiplier.

Section 8 · Reserve → Settle → Release Lifecycle (PAYG §8.3)

Worst-case hold, actual deduction, automatic release. Zero charge for failed jobs. Architect

Every credit-bearing execution follows this lifecycle. Reserve happens before work starts and holds the worst-case against the balance. Settle happens at successful completion and computes the actual via complexity. Release returns the full reservation if execution fails — customers never pay for our failures.

sequenceDiagram
    autonumber
    participant U as User
    participant API as /api/…
    participant CE as credit-engine
    participant DB as Ledger DB

    U->>API: Approve & Run Now (executionId)
    API->>CE: reserveCredits(executionId, baseCredits)
    CE->>DB: read TenantCreditContract + Config (5-min cache)
    CE->>DB: compute maxReserve, write CreditReservation(HELD)
    CE-->>API: ok, balance debited by maxReserve

    Note over U,DB: Workflow executes…

    alt success
        API->>CE: settleCredits(executionId, runtimeVector)
        CE->>DB: complexity calc, write CreditTransaction(DEDUCTION)
        CE->>DB: flip CreditReservation to SETTLED + release unused
        CE-->>API: settled = actualCredits
    else failure
        API->>CE: releaseReservation(executionId)
        CE->>DB: flip CreditReservation to RELEASED_ON_FAILURE
        CE->>DB: full reservedCredits returned to balance
        CE-->>API: zero charged
    end

    Note over CE,DB: Idempotency: @@unique(executionId, type)
second settle / release call → no-op

The lifecycle. Idempotent on every step — Stripe webhook retries and BullMQ job retries are safe.

Reserve at worst case

maxReserve = baseCredits
           × maxComplexityMultiplier   // from contract, default 3.0
           × customerTierMultiplier    // e.g. 1.30 for MULTINATIONAL
           × globalCreditMultiplier    // volume discount, e.g. 0.80 for Scale-pack

Idempotency guard

Every CreditTransaction row has a unique index on (executionId, type). The first settleCredits call inserts a DEDUCTION row. A second call for the same executionId raises a unique-constraint violation; the engine catches it and returns {alreadySettled: true}. No double-charge possible — verified in the Phase 2 hand-test (PR #29).

Section 9 · Credit Ledger Data Model

Six tables. Append-only ledger. PSP IDs isolated to four columns. Architect

Per PAYG §8.2. The credit ledger lives in six tables. Two are platform-level catalogues (CreditPack, WorkflowCreditConfig with tenantId=null rows); four are tenant-scoped with cascade delete.

TableScopePurposePSP fields (isolated)
CreditPackPlatformCatalogue of purchasable packs (5 SKUs in v1)stripePriceId
TenantCreditBalanceTenant (1 row)Source of truth — includedCredits + purchasedCredits = totalBalancenone
CreditTransactionTenant (N)Immutable ledger — every credit movement. @@unique(executionId, type)none
CreditPackPurchaseTenant (N)One row per pack purchase. Links PSP payment to credit grant.stripePaymentId, stripeInvoiceId
CreditReservationTenant (N)Pre-execution hold; status flips HELD → SETTLED or RELEASED_ON_FAILUREnone
TenantBillingConfigTenant (1)Auto-top-up + alert preferencesstripeCustomerId, stripePaymentMethodId

DB-driven config tables

TableHoldsOverride pattern
WorkflowCreditConfigPer-workflow-type credit config (manual cost basis, capture rate, base credits)tenantId=null = platform; non-null = tenant override (resolved via Postgres NULLS LAST)
WorkflowComplexityProfilePer-activity baseline (baselineChildCount, baselineTokenCount, etc.) for normalisationSame tenant-override pattern
ComplexityFactorConfigPer-dimension weight + cap. Platform defaults sum to 1.00.Same tenant-override pattern
TenantCreditContract1 per tenant: customer tier + multiplier + capture rate + complexity bounds + flat-pricing flagn/a — already per-tenant

Internal telemetry tables (iArchitron-only)

WorkflowExecutionTelemetry (per execution; compute/storage/LLM cost + gross margin %), CreditDeficitAlert (rare — post-execution balance went negative). Both gated behind hasAdminAccess; never returned to tenant-scoped routes.

Section 10 · DB-Driven Configuration Principle (PAYG §8.1 #2)

Zero hardcoded credit constants in code. Everything from the database. Architect

Every credit value — base credits per activity, factor weights, complexity baselines, tier multipliers, included-credits-per-plan, BYOLLM multipliers — lives in DB tables. Tenant-specific contract overrides (negotiated capture rates, flat pricing for regulated industries) take effect immediately, without code deploys.

What this means in practice

Operational consequence. A pricing committee decision to bump compliance-report from $7,000 to $7,500 manual basis becomes a single seed-script update + re-run. No application redeploy. Tenants negotiating a 15% capture rate get a new TenantCreditContract row inserted; the 5-min cache expires, every subsequent settle uses the new rate. Auditability via CreditTransaction.manualCostBasis column (manual basis snapshot at txn time) and the effectiveFrom / effectiveTo on contract rows.

Section 11 · Stripe Integration Surface

Stripe + Stripe Tax + Stripe Meters. One file in lib/billing/. Architect Sales

Per PAYG §9.1: Stripe is the v1 launch PSP. The integration is intentionally narrow — lib/billing/stripe-credits.ts is the only file in lib/billing/ that imports the Stripe SDK (mechanically verified in the Phase 3 hand-test). Public-API surface: Checkout sessions for pack purchases + webhook ingestion.

Stripe primitives used

PrimitiveUsed forNotes
Stripe CustomerOne per tenantLazy-created on first purchase; persisted on TenantBillingConfig.stripeCustomerId
Stripe Product + PriceOne per CreditPack SKUstripePriceId stored on the pack row; populated by deploy-time sync script
Stripe Checkout SessionOne per purchase attemptCreated with metadata.purchaseId for round-trip identification
Stripe PaymentIntentStored on completed purchasesOne-time-payment mode
Stripe SubscriptionStrategic Annual pack (recurring)Subscription mode; uses Stripe Meters for usage billing (v1.5)
Stripe Tax (Automatic)Compute tax/VAT/GST at checkoutautomatic_tax: { enabled: true } on every Checkout session
Stripe Webhook signaturesHMAC-SHA256 verificationSTRIPE_WEBHOOK_SECRET; SDK constructEvent handles timestamp tolerance

Checkout session creation flow

  1. User picks a pack at /admin/credits "Buy Credits" tab → POST /api/billing/credit-packs/[id]/purchase
  2. Server creates a PENDING CreditPackPurchase row first (stable purchaseId for round-trip)
  3. Stripe Customer is lazy-created if needed; ID persisted on TenantBillingConfig
  4. checkout.sessions.create({mode, customer, line_items, automatic_tax, metadata: {purchaseId, tenantId, packId}, ...})
  5. Server returns {purchaseId, checkoutUrl}
  6. Browser redirects to Stripe-hosted checkout. PCI-DSS scope is Stripe's, not ours.

Pinned API version

STRIPE_API_VERSION=2024-12-18.acacia set in both env and the client init. Bumped in lockstep with Stripe SDK upgrades; never silently floated. Stripe deprecation warnings surface on every SDK upgrade — the team reviews behavioural changes per Stripe's upgrade guide before bumping.

Section 12 · Webhook Event Handling

Three handled events. At-least-once delivery. Idempotent by construction. Architect

POST /api/billing/webhooks/stripe is the single webhook endpoint. Forces Node runtime (raw body required for HMAC), dynamic = "force-dynamic" (no caching), 400 on signature failure (Stripe retries), 200 on every successfully-ingested event (handled or ignored — non-2xx would trigger infinite retries for events we never plan to handle).

EventActionIdempotency mechanism
checkout.session.completed Calls topUpCreditsFromPurchase({purchaseId, pspPaymentId, pspInvoiceId}) executionId = "purchase:" + purchaseId + type=TOPUP on @@unique index
payment_intent.payment_failed Marks CreditPackPurchase.status = FAILED, stores PaymentIntent ID Re-delivery overwrites with same data → no-op
invoice.paid v1.5 — Strategic Annual renewal flow. v1 stub: returns 200 with ignored: "annual renewal handling deferred to v1.5" n/a in v1
All others Ignored, return 200 with handled: false + ignored reason Idempotent by design (no-op)
Why we 200 unhandled events. Stripe sends ~30 event types per account by default (customer.created, customer.subscription.updated, charge.dispute.*, etc.). Returning non-2xx for an event we don't care about forces Stripe to retry it forever. We log the event type we ignored (audit trail) and 200 immediately. Listed event types are explicitly enabled in the Stripe Dashboard webhook config.

Section 13 · International Strategy (PAYG §9)

Day-one coverage of 40+ tax jurisdictions. No re-architecture for region expansion. CXO Sales

Stripe Tax computes sales tax, VAT, and GST automatically at checkout for customers in the US (all 50 states), EU (all 27 countries), UK, Australia, Japan, Singapore, Canada, Switzerland, Norway, India (state-specific GST), South Korea, and 30+ additional jurisdictions. Tax compliance is Stripe's problem, not ours. iArchitron registers in jurisdictions per Stripe Tax's nexus dashboard recommendations.

Sales tax / VAT / GST

Automatic computation at Stripe Checkout via automatic_tax: {enabled:true}. Stripe handles exemption certificates (B2B), reverse charge (intra-EU), digital-services categorisation, and remittance reporting.

Currency conversion

Stripe Multi-Currency settles in 30+ presentment currencies. Customers see prices in their local currency at checkout. iArchitron settles to USD at Stripe's FX rate (transparent FX margin).

PCI compliance

Stripe is PCI Level 1 certified. Card data never reaches our servers. The hosted Checkout page reduces our PCI scope to SAQ-A — the lightest tier.

3DS / SCA

Strong Customer Authentication (EU SCA / 3D Secure) handled automatically by Stripe Checkout. Required for EU + UK + India + select other jurisdictions.

Local payment methods

SEPA Direct Debit, BACS, ACH, Bancontact, iDEAL, Sofort, Klarna, Apple/Google Pay, Link — enabled per region in the Stripe Dashboard.

Reporting

Stripe's reporting API exposes settlement reports, tax reports, and a Sigma SQL surface for revenue recognition. iArchitron's internal /admin/billing-internal dashboards consume the local CreditTransaction ledger; Stripe Sigma is reserved for AR reconciliation.

Excluded from v1. China mainland (requires WeChat Pay / Alipay; deferred until a specific customer request). Russia (currently blocked from major card networks). Cuba, North Korea, Iran, Syria (Stripe-blocked under OFAC). Customers in these jurisdictions cannot complete checkout in v1.

Section 14 · Phased PSP Roadmap (PAYG §9.4)

v1 Stripe. v1.5 Razorpay. v2 Chargebee. v3 Paddle MOR. Each adds without breaking. Sales Finance

Each PSP addition is approximately one new file + one new webhook route + one IAM secret reference. The credit ledger doesn't move. The customer-facing UI doesn't change. Triggers are revenue-mix driven, not engineering-driven — we add the PSP when the customer base or revenue mix justifies the operational cost.

PhasePSPWhyTrigger to addEngineering cost
v1 (live)Stripe + Stripe Tax + Stripe Meters~85% of likely Arcitopsia ICP coverage; PCI Level 1; automatic taxLaunchShipped — PR #28-#33
v1.5RazorpayIndia PSP — INR-native, India-domiciled GST, UPI railsIndia revenue > ~10% OR > 20 active India tenants~2 weeks
v2Chargebee (on top of Stripe)Enterprise AR orchestration: PO-driven invoicing, NET-60/90 terms, multi-entity AR consolidationENTERPRISE + MULTINATIONAL + MISSION_CRITICAL revenue mix > ~30% of total~4–6 weeks
v3Paddle (merchant-of-record evaluation)Paddle takes seller-of-record responsibility; skips US 50-state + EU 27-country + UK + AU + 40+ jurisdiction registrations. ~5% fee vs Stripe's ~3.5%.Global expansion mandate + ops headcount unavailable for tax compliance work~3 weeks (or replace Stripe entirely)
v3+Region-specific: Adyen, dLocal, Flutterwave, Mercado PagoOn-demand per strategic customer askPer-customer~1–2 weeks each
Architectural guarantee. Every PSP addition is confined to a new lib/billing/<psp>-credits.ts + a new webhook handler route + new env vars + a Stripe-tax-like tax provider plugin. The credit ledger (credit-engine.ts, credit-service.ts, complexity-calculator.ts) doesn't change. The customer-facing /admin/credits doesn't change. The internal /admin/billing-internal gains an additional reconciliation surface per PSP. No schema migration.

Section 15 · Customer Surface — /admin/credits

Four tabs. Customer-facing only. No internal-cost numbers. CXO Sales

The in-app dashboard for tenant users to manage their Intelligence Credits. PAYG §7.0 two-layer rule strictly enforced — only the customer-facing ledger view, never the internal telemetry. Stripe customer/payment-method IDs are deliberately excluded from the API response shape (verified in PR #31 hand-test).

Balance

Total available, included monthly (with next-reset date), purchased, last-30-days consumption, lifetime stats.

Buy Credits

5-tile pack catalogue: Starter / Growth / Scale / Enterprise / Strategic Annual. Click "Buy" → Stripe Checkout redirect.

History

Newest-first CreditTransaction ledger with type-filter chips and "Load more" cursor pagination.

Settings

Auto-top-up: enable + threshold + pack + max-per-month. Low-balance alerts. Notification emails.

Section 16 · Internal Surface — /admin/billing-internal

Four tabs. iArchitron-only. Behind hasAdminAccess. Architect Finance

The internal dashboard for iArchitron ops + finance. The four APIs (margin-analytics, anomalies, stripe-reconciliation, finance-export) are all gated at the route level — non-admin authenticated callers get 403. The Phase 6 hand-test (PR #33) asserts this for every endpoint.

Margin Analytics

1–365 day timeframes. Totals + per-activity rollup + top-25 tenants by spend. Margin coloured (≥95% green, ≥80% amber, <80% red).

Anomalies

4 classes in one round trip: low-margin tenants, failed purchases, unresolved deficits, large refunds.

Stripe Reconciliation

Completed / completed-without-Stripe-ID / abandoned-PENDING buckets. v1 ledger inspection only; v1.5 adds stripe.payouts.list round-trip.

Finance Export

Date range → CSV download. 10K row cap; columns: id, createdAt, tenantId, tenantSlug, tenantName, type, credits, balanceAfter, activityType, executionId, purchaseId, packRateUSD, usdEquivalent, manualCostBasis, isByollm, memo.

SOC 2 evidence path. The CSV export is the primary AR reconciliation artefact. Date-windowed exports + Stripe Sigma queries are the SOC 2 evidence trail for revenue recognition. The CredentialAccessLog (probe credentials), CreditTransaction (immutable ledger), and WorkflowExecutionTelemetry (compute audit) together cover SOC 2 Common Criteria 8 (audit logging).

Section 17 · Anomaly Detection

Four anomaly classes. One round trip. Surfaced daily to ops. Architect Finance

The Anomalies tab queries four parallel signals in a single round trip and renders them on one page so the ops team triages them daily. Thresholds are constants in v1 (margin floor 80%, large-refund 1,000 credits); v2 makes them tenant-configurable.

ClassSourceThresholdWhat it signals
Low-margin tenants WorkflowExecutionTelemetry aggregated per tenant < 80% gross margin over window Likely undercharged customer (negotiated bad capture rate) OR runaway LLM cost — sales investigates
Failed purchases CreditPackPurchase.status = FAILED in window Any failed purchase in the last N days Stripe error reason needs investigation (card declined, 3DS failure, fraud rule, etc.)
Unresolved deficits CreditDeficitAlert.resolvedAt IS NULL Any unresolved Balance went negative after settle — should be very rare; reserve-worst-case prevents in normal operation
Large refunds CreditTransaction.type = REFUND |credits| ≥ 1,000 Customer-service ticket likely behind it; sales-finance acknowledges

Section 18 · Worked Example — Full Lifecycle

PostgreSQL DataProbe + Hierarchy Synthesis end-to-end. $0.24 internal cost. CXO Finance

Scenario: 200 tables, 50 stored procedures (10 require AI summarisation), 4 EA artefacts drafted per tech across 7 techs (subset). Tenant on MULTINATIONAL tier (×1.30) with 20% Scale-pack discount (globalCreditMultiplier = 0.80), non-BYOLLM, complexity enabled.

Step 1 — Base credits (from WorkflowCreditConfig)

Line itemActivityManual basisBase credits
Probe Discovery Runprobe-discovery-run$500100
Bulk Import 200 tablesbulk-import-per-100-records$50 / 100200
AI Classification × 10 routinesai-enrichment-per-record$100 / record200
Hierarchy Synthesis × 4 artefacts × subset of techsprobe-ea-artifact-draft$250 / artefact200
Σ baseCredits700

Step 2 — Worst-case reservation (at "Approve & Run Now")

maxReserve = 700 × 3.0 × 1.30 × 0.80 = 2,184 credits  // ≈ $1,747 at Scale-pack rate

Step 3 — Runtime metrics + complexity score

DimensionActualBaselineNormalisedCapped× Weight
child_count301305.01.250
token_intensity (per 1K)1853.63.60.792
context_size_kb (per 100KB)1.80.53.63.00.450
wall_clock_ms95K30K3.162.50.250
hierarchy_depth313.03.00.240
peak_concurrency414.02.00.120
model_tier (Sonnet)221.01.00.050
cache_miss_rate0.400.301.331.330.053
retry_count00000.000
external_api_calls10(treated as 1)1.00.020
Σ / Σweight = complexityScore3.225
rawMultiplier        = log₂(3.225 + 1) × 1.44 = log₂(4.225) × 1.44 = 2.99
complexityMultiplier = clamp(2.99, min=0.5, max=3.0) = 2.99

Step 4 — Final settle

finalCredits = round(700 × 2.99 × 1.30 × 0.80) = round(2,177.4) = 2,177
2,184
Reserved
worst-case at Approve & Run
2,177
Settled
actual after complexity
7
Released
back to balance on settle
$0.24
Internal cost
LLM + compute + storage (iArchitron-only)

Step 5 — Margin (iArchitron-only, never customer-facing)

creditsBilled        = 2,177
internalCostUSD      = $0.18 LLM + $0.04 compute + $0.02 storage = $0.24
usdEquivalent        = 2,177 × $0.80/credit = $1,741.60
grossMarginPct       = ($1,741.60 − $0.24) / $1,741.60 = 99.99%
Failure scenario. If Stage 2 fails after importing 50 tables and 3 routine summaries: releaseReservation(executionId) flips reservation to RELEASED_ON_FAILURE. Full 2,184 credits returned to balance. Zero credits consumed per PAYG §5.4. Internal telemetry still recorded (compute consumed by the failed run is iArchitron's cost — captured in WorkflowExecutionTelemetry for capacity planning).

Section 19 · v1 scope vs v1.5+ deferrals

What ships in v1, and what's deliberately deferred. Sales Architect

v1 ships (PRs #28–#33)

  • PSP-agnostic credit ledger (6 tables, 4 DB-driven config tables)
  • Reserve / settle / release lifecycle with @@unique(executionId, type) idempotency
  • 10-factor complexity multiplier with log₂ dampening
  • 5-tier customer-tier multiplier (INDIVIDUAL → MISSION_CRITICAL)
  • BYOLLM economics (0.62× credit multiplier + 15% license discount)
  • Stripe + Stripe Tax + Stripe Meters integration
  • /admin/credits customer dashboard (4 tabs)
  • /admin/billing-internal internal dashboard (4 tabs)
  • Margin analytics, anomaly detection, AR ledger CSV export
  • Website pricing + credits pages synced with seed

Deferred to v1.5 / v2 / v3

  • Razorpay (India PSP) — v1.5 trigger at > 10% India revenue
  • Chargebee (enterprise AR) — v2 trigger at > 30% ENT+MULTI+MC revenue mix
  • Paddle MOR evaluation — v3 trigger on global expansion mandate
  • Stripe payouts API roundtrip for reconciliation — v1.5
  • Annual committed credit packages with 10% carryover — v2
  • Flat-rate billing for regulated industries (schema exists; sales-enabled v2)
  • Currency conversion for non-USD billing — billing infra v2
  • Real-time credit consumption websocket dashboard — v2
  • Inactivity-based credit expiry sweeper cron — v2
  • Advanced complexity factors (regulatory overlay, time-of-day demand pricing) — v2/v3
  • Annual renewals via invoice.paid webhook — v1.5
  • Region-specific PSPs (Adyen, dLocal, Flutterwave, Mercado Pago) — v3+ on-demand

Section 20 · Glossary

Billing vocabulary for LLM grounding + onboarding. LLM Finance

Credit
The platform unit of customer-facing billing. One credit = one USD at base rate ($1.00), discounted by volume pack tier (down to $0.60/credit on Strategic Annual).
Activity
A credit-bearing workflow type (e.g. architecture-document, compliance-report, probe-discovery-run). Each has a row in WorkflowCreditConfig with manual cost basis + capture rate + base credits.
Manual Cost Basis
The documented USD cost of producing the equivalent outcome manually (e.g. $4,000 for an architecture document = 40 hours × $100/hr EA consultant). Anchors the credit price.
Capture Rate
The fraction of manual cost basis charged as credits. Default 0.20 (20%). Negotiable 0.15–0.25 per TenantCreditContract. baseCredits = round(manualCostBasisUSD × captureRatePct).
Base Credits
The fixed pre-multiplier credit cost of an activity. Stored in WorkflowCreditConfig.baseCredits.
Customer Tier
Five-tier structural-overhead classification: INDIVIDUAL (0.75) → SMB (0.90) → ENTERPRISE (1.00) → MULTINATIONAL (1.30) → MISSION_CRITICAL (1.60). Set per tenant at contract time.
Complexity Multiplier
Runtime-derived multiplier from 10 factors (child count, token intensity, context size, etc.), per-factor capped, weight-summed, then log₂(score+1) × 1.44 dampened, clamped to [contract.min, contract.max].
Global Multiplier
Volume discount applied per TenantCreditContract.globalCreditMultiplier. Reflects pack tier (e.g. 0.80 for Scale-pack = 20% off; 0.65 for Enterprise-pack = 35% off).
BYOLLM
Bring-Your-Own-LLM. Tenant supplies their own LLM API keys; pays reduced credit rate (0.62× default) + 15% license discount. Tenant.byollmEnabled = true.
Reservation
Pre-execution credit hold (worst-case). Row in CreditReservation with status HELD → SETTLED or RELEASED_ON_FAILURE.
Settle
Post-execution computation of actual credits via complexity multiplier; writes CreditTransaction(DEDUCTION) + flips reservation to SETTLED + returns unused to balance.
Release
Post-execution full reservation return on failure. Writes nothing to CreditTransaction (no DEDUCTION). Zero credits consumed.
Top-up
Credit grant from a pack purchase. Writes CreditTransaction(TOPUP) with executionId = "purchase:" + purchaseId + type=TOPUP for idempotency.
Two-Layer Measurement
The PAYG §7.0 invariant separating internal telemetry (WorkflowExecutionTelemetry with compute units, tokens, LLM cost, gross margin %) from customer-facing credits (CreditTransaction). The two never cross.
PSP
Payment Service Provider. Stripe in v1; Razorpay (v1.5), Chargebee (v2), Paddle (v3) on the roadmap.
PSP-Agnostic Invariant
Per PAYG §9.6: the credit ledger (credit-engine.ts, credit-service.ts, complexity-calculator.ts) contains zero PSP-specific types/IDs/imports. Mechanically verified in the hand-test.
MOR (Merchant of Record)
A payment processor (e.g. Paddle) that takes seller-of-record responsibility — handling sales tax, VAT, GST registrations across all jurisdictions in exchange for a higher per-transaction fee. v3 evaluation.
Webhook Idempotency
The property that processing the same event twice has no additional effect. Achieved via @@unique(executionId, type) on CreditTransaction. Stripe delivers at-least-once; we accept that without double-crediting.
DB-Driven Configuration
Per PAYG §8.1 #2: zero hardcoded credit constants in code. All values come from WorkflowCreditConfig, WorkflowComplexityProfile, ComplexityFactorConfig, TenantCreditContract rows. Pricing committee can change values via seed-script re-run; no application redeploy.
Flat Pricing
For regulated industries (financial services, government, healthcare): TenantCreditContract.flatPricingEnabled = true → complexity multiplier always returns 1.0. Predictable bill, no scope-based variability.
Anomaly
One of 4 flagged conditions surfaced on /admin/billing-internal Anomalies tab: low-margin tenant, failed purchase, unresolved deficit, large refund.
Reconciliation
The process of matching internal ledger entries (CreditPackPurchase) against PSP payment records (Stripe payouts). v1: ledger inspection only. v1.5: stripe.payouts.list roundtrip + line-item match.

Want to see the architecture in action?

Book a demo and we'll walk through the credit ledger, run a worked example, and show the customer + internal dashboards side by side.

by iArchitron Software Inc. · iarchitron.ai