Cost & P&L

Cost & P&L

The admin Costs & P&L tab (/admin) shows real-time generation cost and profit. Backend: GET /api/admin/costs (admin-only). On each request it first lazily costs any newly-completed jobs, then reads and returns range, config, measuredSpend, falUsage, authoritativeProviderSpend, storeProfitability, vercelInfraSpend, staticMonthlyTotal.

Date range — Costs, Overview, and Generations accept a custom range via ?from=&to= (YYYY-MM-DD, to exclusive; parseAdminRange defaults to month-to-date). It threads through the ranged RPCs (generation_spend_summary(p_from, p_to), store_generation_spend(p_from, p_to)) and every cost helper. Caveat: base-plan MRR is not historized — a past month reuses the store’s current plan price, so only provider spend, overage, and infra are truly historical.

The profit formula

Net profit = Revenue − Provider spend − Infra
Revenue        = Est. MRR (base plans, live from Shopify) + Usage overage (pending)
Provider spend = real fal billing (usage API) + kie actual credits   [authoritative]
Infra          = static monthly costs + live Vercel spend

Per-merchant: Margin = (plan MRR + overage) − that store's generation cost.

Per-generation cost — tryon_provider_jobs.cost_usd

Captured from data already on the job row — no change to the generation hot path. Pure function computeGenerationCost in src/lib/billing/generation-cost.ts.

  • kie.ai — actual metered credits, read from response_payload.taskRecord.creditsConsumed × $0.005/credit. Exact.
  • fal.ai — fal bills by different unit types (confirmed via GET api.fal.ai/v1/models/pricing?endpoint_id=X):
    • nano-banana-2/edit → unit images, $0.08 — exact, per image.
    • nano-banana-lite/edit and gpt-image → unit units, $1.00token-based, not per image. Per-request unit consumption is not derivable per job, so token-billed models use an invoice-derived estimate at the per-job level (see the authoritative fal usage API below for the real total).
  • ConfiggetGenerationCostConfig (lib/server/supabase-admin.ts) reads app_config keys cost_kie_credit_usd, cost_model_prices (JSON map provider:model$), and cost_fallback_usd. Owner overrides merge over the code defaults (DEFAULT_MODEL_PRICES). An unknown model falls back to cost_fallback_usd — never guessed.
  • refreshGenerationCosts() lazily costs newly-completed null-cost_usd jobs. Best-effort, called from the costs GET; never touches the generation path.
  • getGenerationSpend() → SQL function generation_spend_summary → spend by provider:model (month + all-time).

Migrations: 20260707_provider_job_cost_usd.sql (adds cost_usd numeric), 20260707_generation_spend_summary.sql.

⚠️

fal’s unit field matters: images is exact per-image pricing; units is token-based. Never multiply the $1.00 unit price by generation count for token-billed models — it will be wrong.

Authoritative provider spend

The per-job cost_usd estimate is good enough for the model-spend breakdown, but for token-billed fal models it is not the real invoice number. The authoritative total comes from each provider’s own billing API:

  • falgetFalUsageSpendThisMonth() calls the admin-scope fal usage API GET api.fal.ai/v1/models/usage (params start/end as ISO dates — not start_date; expand=time_series; cursor pagination) and sums the real billed quantity × cost per endpoint. Pure function sumFalUsage in src/lib/billing/fal-usage.ts. Gated on env FAL_ADMIN_KEY. This is the only accurate cost source for token-billed models — it exposed that the per-job estimate was 12× low on nano-banana-lite ($8.69 real vs $0.68 estimated), while nano-banana-2 matched the real invoice exactly ($189.44).
  • kie — its cost_usd (actual credits) is already exact, no separate call needed.

authoritativeProviderMonth = falUsage.total + kie month cost_usd, falling back to the per-job estimate (measuredSpend.monthSpend) when FAL_ADMIN_KEY is absent.

⚠️

The fal usage API requires an admin-scope key — an API-scope key gets a 403. Query params are start/end, not start_date/end_date.

Per-merchant P&L — getStoreProfitability()

SQL function store_generation_spend joins each costed job to its store via tryon_provider_jobs.session_id → tryon_sessions.id → store_id. Per store:

  • Revenue = plan MRR (custom_price or getPlan(planId).price, only counted while the subscription is active and not on trial) + usage overage (below).
  • Margin = revenue − that store’s summed cost_usd.

This is contribution margin only — shared/fixed infra is not allocated per store.

Migration: 20260708_store_generation_spend.sql.

Usage overage in revenue

Revenue counts usage overage, not just base plan MRR. It has two parts:

overageRevenue = settled_this_month + (overage_pending × getOverageRate(store))
  • Settled — overage already billed to Shopify this month, summed from billing_events (event_type = 'overage_charged', success = true, amount). settlePendingOverage (lib/server/billing-overage.ts, overage cron) writes one such event each time it charges.
  • Pendingoverage_pending extra try-ons accrued since the last settlement, × the rate (custom_overage_rate or plan.overage). Not yet billed; the cron resets it to 0 on settle.

Both are counted in per-merchant revenue and the app-wide P&L. Using settled + pending (rather than pending alone) is essential — a store settling daily can bill most of its month’s overage before you look, leaving overage_pending tiny. See Billing Pipeline for the full overage lifecycle.

Infra costs

  • VercelgetVercelInfraSpendThisMonth() calls GET api.vercel.com/v1/billing/charges (params from/to — not start/end; FOCUS v1.3 JSONL format) and sums BilledCost via the pure function sumFocusBilledCost in src/lib/billing/infra-cost.ts. Gated on env VERCEL_BILLING_TOKEN + VERCEL_TEAM_ID. Best-effort — returns null on failure/missing config. (The account currently runs on Vercel Hobby, so this is typically ~$0/month.)
  • Supabase — no clean spend API exists, so it’s a flat static cost, part of staticMonthlyCosts in the cost config.
⚠️

The Vercel billing-charges API takes from/to, not start/end — the opposite convention from the fal usage API above. Easy to mix up.

Admin UI (Costs & P&L tab)

KPI cards: Est. MRR (base plans), Usage overage (pending), Provider spend (real billing), Infra + static, Net profit.

Tables:

  • fal — real billing — real quantity/unit/cost per endpoint, straight from the fal usage API.
  • Generation spend by model (measured) — per-job cost_usd summed by provider:model.
  • Profit by merchant — store, plan, MRR, overage, generation count, generation cost, margin.

Cost config (cost_kie_credit_usd, cost_model_prices, cost_fallback_usd, static monthly costs) is editable from the same tab. A per-generation cost column also appears in the admin generations log and on the store-detail recent-generations list.

Env vars

See Env Vars (Cost & P&L section) for FAL_ADMIN_KEY, VERCEL_BILLING_TOKEN, and VERCEL_TEAM_ID.