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.
Resolve plan and funding, admit against quota — before anything runs.
Hold the estimate so concurrent requests can't oversubscribe it.
The accounted work runs; usage is emitted into the flow's scope.
Charge the actual cost across the funding sources; release unused holds.
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 family | Runtime authority | How it changes |
|---|---|---|
| Reservation, price tables, reference service | Read live from economics.yaml | Edit the descriptor; Eco Admin also exposes direct live editing for reservation floors. |
| Quota policies, budget policies, plans | Seeded into PostgreSQL; the database is then operational authority | Eco Admin updates runtime rows and writes supported seeded sections back to the descriptor. |
| Project overdraft limit | Seeded from economics.yaml into the project-budget row | Currently displayed read-only in Eco Admin; change it in the descriptor and re-seed. |
| Per-app reservation | config.economics.reservation.<surface>, for example economics.reservation.chat | App 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.
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.
| Plan | Concurrent | Req/Day | Req/30-day | Tokens/Hour |
|---|---|---|---|---|
anonymous | 1 | 2 | 60 | 150k |
free | 2 | 100 | 30k | 133k |
wallet | 4 | 200 | 6k | from free |
admin | 10 | unlimited | unlimited | unlimited |
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.
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.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.
Verify · reserve · settle. An async with around the flow — the accounted work runs inside, and the actual cost settles on exit.
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
GET /economics/request-lineage?request_id=<scope_id>.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.
Document / index embeddings — exceptions propagate. A write must not silently skip content.
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
)
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}}, ...}
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
Checkout
User initiates a wallet top-up or subscription via
POST /api/economics/stripe/checkout/topupor.../subscription. The API creates a Stripe Checkout Session and returns a redirect URL. -
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 viaexternal_economics_events(keyed by Stripe event ID), and applies the change. -
3
Reconcile job
A cron-driven reconcile job (
STRIPE_RECONCILE_CRON) catches missed webhooks by fetching Stripe events ordered ascending bycreatedtimestamp from a Redis-stored watermark. All replay is idempotent.
Webhook Events Handled
| Event | Action |
|---|---|
payment_intent.succeeded | Top up wallet (lifetime credits) for wallet_topup kind |
invoice.paid | Top up subscription period budget; update subscription record |
refund.created / refund.updated | Finalize wallet refund pending event |
checkout.session.completed | Write stripe_subscription_id to DB (prevents race with invoice.paid) |
customer.subscription.updated | Sync subscription status and next_charge_at |
customer.subscription.deleted | Mark 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.
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
| Endpoint | Backed By | Description |
|---|---|---|
GET /accounting/opex/total | Daily aggregates + raw gap-fill | Global usage totals with cost estimate for a date range |
GET /accounting/opex/users | Daily users.json | Per-user usage and cost breakdown |
GET /accounting/opex/agents | Daily agents.json | Per-agent usage and cost breakdown |
GET /accounting/opex/conversation | Raw 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 }
]
}
}
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.