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 spendPer-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→ unitimages,$0.08— exact, per image.nano-banana-lite/editand gpt-image → unitunits,$1.00— token-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).
- Config —
getGenerationCostConfig(lib/server/supabase-admin.ts) readsapp_configkeyscost_kie_credit_usd,cost_model_prices(JSON mapprovider:model→$), andcost_fallback_usd. Owner overrides merge over the code defaults (DEFAULT_MODEL_PRICES). An unknown model falls back tocost_fallback_usd— never guessed. refreshGenerationCosts()lazily costs newly-completed null-cost_usdjobs. Best-effort, called from the costsGET; never touches the generation path.getGenerationSpend()→ SQL functiongeneration_spend_summary→ spend byprovider: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:
- fal —
getFalUsageSpendThisMonth()calls the admin-scope fal usage APIGET api.fal.ai/v1/models/usage(paramsstart/endas ISO dates — notstart_date;expand=time_series;cursorpagination) and sums the real billedquantity×costper endpoint. Pure functionsumFalUsageinsrc/lib/billing/fal-usage.ts. Gated on envFAL_ADMIN_KEY. This is the only accurate cost source for token-billed models — it exposed that the per-job estimate was 12× low onnano-banana-lite($8.69 real vs $0.68 estimated), whilenano-banana-2matched 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_priceorgetPlan(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. - Pending —
overage_pendingextra try-ons accrued since the last settlement, × the rate (custom_overage_rateorplan.overage). Not yet billed; the cron resets it to0on 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
- Vercel —
getVercelInfraSpendThisMonth()callsGET api.vercel.com/v1/billing/charges(paramsfrom/to— notstart/end; FOCUS v1.3 JSONL format) and sumsBilledCostvia the pure functionsumFocusBilledCostinsrc/lib/billing/infra-cost.ts. Gated on envVERCEL_BILLING_TOKEN+VERCEL_TEAM_ID. Best-effort — returnsnullon 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
staticMonthlyCostsin 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_usdsummed byprovider: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.
Related
- Billing Pipeline — plan MRR, overage accrual/settlement, usage accounting.
- AI Providers — kie.ai and fal.ai client details.
- Data Model —
tryon_provider_jobs,stores.