Platform Economics

Account for participating paid calls

LLM calls, embeddings, web search, and your own metered services can use one control flow: verify, reserve, run, settle. The runtime checks funding and quota before accountable work, reserves against concurrent oversubscription, and settles actual usage. The same mechanism works inside chat, APIs, search, tools, and background jobs.

Verify

Resolve plan and funding, admit against quota — before anything runs.

Reserve

Hold the estimate so concurrent requests can't oversubscribe it.

Run

The accounted work runs; usage is emitted into the flow's scope.

Settle

Charge the actual cost across the funding sources; release unused holds.

Economics lifecycle: verify, reserve, run, settle, with denial before any work runs
Economics lifecycle Verify, reserve, run, and settle a paid call. Denial raises before any work runs and leaves no money hold behind. Verify plan · funding · quota admit Reserve hold the estimate Run accounted work · usage emitted Settle commit actual cost · release unused deny before any work runs — EconomicsLimitException, no hold left behind
Verify → reserve → run → settle. Denial raises before anything runs; a zero-cost flow releases its holds.
i
There is no fixed “cost per turn.” A turn's cost is the sum of the spending events inside it: model calls, embeddings, web search, tools, APIs, and other tracked work. Attribution follows tenant/project, user, app, conversation, turn, agent, flow, provider, and model. Helper-agent spend is recorded on the child conversation and rolled up under the delegating turn.
i
Quota windows are active buckets, not calendar labels. An hourly window is the rolling last 60 minutes; a daily window is the current 24-hour quota period since the last reset; a monthly window is the current 30-day period. A user dashboard should show usage since the current bucket began and what remains — not "today since midnight."

Live policy

The economics descriptor and Eco Admin

economics.yaml is the sixth environment descriptor. It defines reservation defaults, price tables, the reference LLM service, quota and budget policies, plans, and overdraft behavior for one tenant/project deployment.

Policy familyRuntime authorityHow it changes
Reservation, price tables, reference serviceRead live from economics.yamlEdit the descriptor; Eco Admin also exposes direct live editing for reservation floors.
Quota policies, budget policies, plansSeeded into PostgreSQL; the database is then operational authorityEco Admin updates runtime rows and writes supported seeded sections back to the descriptor.
Project overdraft limitSeeded from economics.yaml into the project-budget rowCurrently displayed read-only in Eco Admin; change it in the descriptor and re-seed.
Per-app reservationconfig.economics.reservation.<surface>, for example economics.reservation.chatApp descriptor or live App Configuration UI; the legacy scalar remains accepted.

Eco Admin manages plan overrides, trials and lifetime credits, quotas, budgets, the plan catalog, external subscription price mappings, and reservation floors without a platform rebuild. General service price tables and the reference service remain descriptor-edited policy. Descriptor policy and database-backed operational policy remain distinct rather than treating Redis or a dashboard draft as authority.

Who pays

One request, one funding split

Before it reserves anything, the engine resolves who pays. Every request is a single split. A primary funding source — an external subscription's period budget, or the project budget for everyone else — covers the part bounded by both the remaining plan quota and the primary funds. The user's wallet covers the remainder.

Funding split: plan part covered by the primary source under quota, wallet part covers the remainder, project budget absorbs residual
Funding split The estimated cost R divides into a plan part funded by the primary source under quota and funds, and a wallet part covering the remainder. The project budget absorbs any residual shortfall. R = estimated cost plan_part min(R, quota, primary funds) wallet_part R − plan_part if actual spend still overshoots → the project budget absorbs the residual (last resort) · subscriptions and wallets never go negative
The primary source covers the plan part under quota; the wallet covers the remainder; the project budget absorbs any residual.

When plan quota or funds run out, plan_part shrinks toward zero and the wallet covers the rest in the same pass. Settlement charges plan quota and primary funds first, then the wallet for the over-quota remainder; wallet-paid tokens do not consume plan quota.

Limits

Plans & quotas

A plan (plan_id) is the quota policy identity the rate limiter uses — requests, concurrency, and token windows — resolved per request. Four plan ids are baked into the runtime; a deployment descriptor overrides them per field and adds chargeable subscription plans.

PlanConcurrentReq/DayReq/30-dayTokens/Hour
anonymous1260150k
free210030k133k
wallet42006kfrom free
admin10unlimitedunlimitedunlimited

Built-in baseline (DEFAULT_QUOTA_POLICIES); a blank window means unlimited. A wallet-backed free user keeps plan_id = free but draws service limits (concurrency / requests) from wallet while token limits stay from free. Chargeable subscription plans are added in the economics descriptor.

i
Plan and funding come from the economics subject. Plan resolution and funding access read economics state and explicit authority. The EconomicsSubject passed into enforcement carries the identity that pays — id, roles, permissions, and any explicit budget bypass — projected from Connection Hub authority for delegated and channel-owned work.
subscription external Stripe budget project tenant/project budget wallet lifetime USD credits

The engine

Guard any paid surface

The economics model is exposed as a small, reusable engine so any accountable flow — one that runs paid work on a user's behalf without a chat turn — verifies, reserves, and settles through the same resolution. Two entry points, chosen by who settles the cost.

EconomicsGuard

Verify · reserve · settle. An async with around the flow — the accounted work runs inside, and the actual cost settles on exit.

economic_preflight

Verify only — no reserve, no settle. A feasibility gate for when the cost is metered elsewhere, or the caller degrades on denial.

from kdcube_ai_app.apps.chat.sdk.infra.economics.enforcement import (
    EconomicsGuard, EconomicsEstimate, FlowPolicy,
)
from kdcube_ai_app.apps.chat.sdk.infra.economics.policy import EconomicsLimitException

try:
    async with EconomicsGuard(
        self, subject=subject,
        scope_id="report_render_42", flow="reports.render",
        estimate=EconomicsEstimate(reservation_usd=0.05),
        policy=FlowPolicy(enforce_concurrency=False),
    ) as decision:
        result = await do_the_paid_work()   # accounted calls run here
except EconomicsLimitException as exc:
    return degraded_response(exc)      # nothing ran; no hold left behind
💡
A guard entered inside a chat turn degrades to verify-only — the turn settles, so the same work is never charged twice. A guard outside a turn settles its own operation scope. Trace any flow with GET /economics/request-lineage?request_id=<scope_id>.

Recipe: Guard a Paid Surface and Enforce Economics.

Search

Let search fall back when embedding is denied

Semantic search is the most common paid surface, and it has a purpose-built facade so components never touch the guard. An economics-enabled entrypoint hands a searchable component one dependency with two methods that fail differently on purpose.

embed_texts()

Document / index embeddings — exceptions propagate. A write must not silently skip content.

embed_search_query()

The query embed, wrapped in a guard — returns None on denial so the caller falls back to lexical / BM25.

When economics denies the query embedding, a search component can continue with lexical or BM25 ranking instead of failing solely because the paid semantic step was unavailable. Memory, canvas pin, task, and app-owned search surfaces can share this behavior while keeping index writes independent of query-time spend. Label each check with a stable flow name such as memory.search, canvas.pins.search, or an app-specific flow so usage attributes clearly.

Usage

Accounting & self-tracked services

A paid call routed through the accounting trackers emits an accounting event — a service type, provider/model, unit counts, cost, and runtime lineage — captured by a decorator such as @track_llm, @track_embedding, or @track_web_search. A turn report sums those spending events; it does not assign a synthetic turn price. Your own paid work can join the same rails through a tracker and usage extractors.

Cold-Turn Attribution

A model switch moves to a different model cache namespace. A capability toggle changes the cached system catalog, so the entire prompt is cold for one turn rather than only one catalog slice. When a selection delta is adopted on a warm conversation, the runtime emits a [CACHE] ANNOUNCE line, cache_cold_turn accounting metadata on the decision call, and a correlating log entry.

The marker attributes the cache-rebuild premium as one identifiable spending component; it is not a total or a promised amount. The user holds the cache policy (accept, confirm, defer_cold, or defer_conversation), while app configuration supplies only the allowed set and default.

from kdcube_ai_app.infra.accounting import AccountingTracker, ServiceType, ServiceUsage

def track_document_ocr(**extractors):
    return AccountingTracker(ServiceType.VISION, **extractors)

def _ocr_usage(result, *args, **kwargs) -> ServiceUsage:
    pages = int(result["pages_processed"])
    return ServiceUsage(
        document_pages=pages, requests=1,
        cost_usd=round(pages * 0.01, 6),   # the pricing lever
    )
track what did it use? price what did it cost? enforce may it run, who pays?
i
The per-turn calculator prices llm, embedding, and web_search today — each honoring a self-reported cost_usd. A brand-new service type is recorded for analytics but priced at $0 unless the calculator is explicitly extended. To charge through the current path, emit under a supported priced type with the real cost_usd.

The Claude Code runtime already runs on exactly this path: a Claude Code turn is accounted as ordinary LLM usage — same @track_llm tracker, provider = "anthropic", runtime = "claude_code" — with tokens and a cost_usd parsed from the CLI's stream output, priced by table lookup or the reported cost. Recipe: Implement a Self-Tracked Service.

Coverage is explicit, not universal: integrated model, embedding, web-search, and participating custom/self-tracked calls can be enforced and attributed. Arbitrary external spending by code that bypasses these service contracts is outside the economics boundary.

Enable it

App economics in one base

An application becomes economics-enabled by extending one base, which binds the runtime primitives the engine reuses. Where none of Redis / PostgreSQL are configured, the runtime has no economics and paid calls run unmetered — so enable it and set a reservation floor for chat turns:

from kdcube_ai_app.infra.plugin.bundle_loader import bundle_entrypoint, bundle_id
from kdcube_ai_app.apps.chat.sdk.solutions.chatbot.entrypoint_with_economic import (
    BaseEntrypointWithEconomics
)

@bundle_entrypoint(name="my-app", version="1.0.0")
@bundle_id(BUNDLE_ID)
class MyApp(BaseEntrypointWithEconomics):
    def configuration_defaults(self):
        return {"economics": {"reservation": {"chat": 2.0}}, ...}
⚠️
Always re-raise EconomicsLimitException unchanged — never catch it silently. The base report_turn_error() handles it correctly.

Payments

Payment Integration (Stripe)

Stripe handles all external payment flows: wallet top-ups (one-time credits), recurring subscriptions, refunds, and subscription cancellations. Project budgets are never topped up directly from Stripe events — only by admin actions or the subscription rollover job.

Webhook Flow

  1. 1

    Checkout

    User initiates a wallet top-up or subscription via POST /api/economics/stripe/checkout/topup or .../subscription. The API creates a Stripe Checkout Session and returns a redirect URL.

  2. 2

    Stripe fires webhook

    On payment completion Stripe sends an event to POST /api/economics/webhooks/stripe. The handler verifies the HMAC signature, checks idempotency via external_economics_events (keyed by Stripe event ID), and applies the change.

  3. 3

    Reconcile job

    A cron-driven reconcile job (STRIPE_RECONCILE_CRON) catches missed webhooks by fetching Stripe events ordered ascending by created timestamp from a Redis-stored watermark. All replay is idempotent.

Webhook Events Handled

EventAction
payment_intent.succeededTop up wallet (lifetime credits) for wallet_topup kind
invoice.paidTop up subscription period budget; update subscription record
refund.created / refund.updatedFinalize wallet refund pending event
checkout.session.completedWrite stripe_subscription_id to DB (prevents race with invoice.paid)
customer.subscription.updatedSync subscription status and next_charge_at
customer.subscription.deletedMark subscription canceled; rollover unused balance to project budget

Subscription Plans & Funding Sources

Each chargeable plan maps to a Stripe product via its price id. When invoice.paid fires, the handler tops up the user's subscription period budget (the funding source for that billing cycle). On period end the rollover job moves unused balance into the project budget. Wallet top-ups credit the lifetime credits source directly; refunds debit lifetime credits and issue a Stripe refund against the original payment_intent_id.

⚠️
The webhook endpoint must be excluded from gateway rate limiting via bypass_throttling_patterns (^.*/webhooks/stripe$). Without this, webhook bursts during reconcile catch-up may be throttled and Stripe will retry, causing duplicate processing attempts.

Reporting

Usage Reporting & Aggregations

The OPEX subsystem provides pre-computed accounting aggregates on top of raw usage events. These power the OPEX API endpoints and avoid rescanning event logs on every request. Raw events are written under accounting/<tenant>/<project>/<YYYY.MM.DD>/<service_type>/; aggregates land under analytics/<tenant>/<project>/accounting/.

OPEX API Endpoints

EndpointBacked ByDescription
GET /accounting/opex/totalDaily aggregates + raw gap-fillGlobal usage totals with cost estimate for a date range
GET /accounting/opex/usersDaily users.jsonPer-user usage and cost breakdown
GET /accounting/opex/agentsDaily agents.jsonPer-agent usage and cost breakdown
GET /accounting/opex/conversationRaw events (prefix-optimized)Per-conversation usage
GET /accounting/opex/turn/*Raw events (prefix-optimized)Per-turn and per-turn-per-agent usage

Cost Breakdown Example

// GET /accounting/opex/total?tenant=home&project=demo&date_from=2026-06-01&date_to=2026-06-21
{
  "event_count": 1234,
  "cost_estimate": {
    "total_cost_usd": 123.45,
    "breakdown": [
      { "service": "llm", "provider": "anthropic", "model": "claude-sonnet-...", "cost_usd": 78.90 },
      { "service": "embedding", "provider": "openai", "model": "text-embedding-3-small", "cost_usd": 44.55 }
    ]
  }
}
💡
A nightly scheduler (OPEX_AGG_CRON) computes daily and monthly aggregates; cross-instance dedup uses a Redis lock. The OPEX layer provides metering and pricing; budget caps and enforcement are applied at the service call site by the guard engine above.