GenerateSaaS

Pricing and margins

costToCredits economics - creditsPerUsd, markups per category, the quantization step, the minimum charge, and the plan-solvency check.

Price any debit that has a real upstream cost through costToCredits(costUsd, category?) rather than hand-picking a number. It applies the shared economics on pricingConfig.credits:

KeyShippedMeaning
creditsPerUsd1Credits per 1 USD of underlying cost. Must be present, finite, and > 0 - costToCredits throws otherwise rather than silently under-billing every metered run.
markupPercent100The default margin on every cost-priced debit - AI, external APIs, your features. 100 charges double cost.
markupByCategory-Per-category overrides of markupPercent. Omit it and one margin covers everything.
step0.000001The amount every debit ceils UP to, in credits - the ledger's own scale. Max 1; a coarser step over-bills small calls: at 0.0001 a 0.000044-credit model call bills 0.0001.
minCharge-A floor applied AFTER the step, for a fixed per-call overhead the provider's price does not cover. Ceiled to the step itself; max 10 credits.

Margining services apart

A per-call search vendor and token-billed AI rarely deserve the same margin, so a debit may name the category it belongs to:

credits: {
	creditsPerUsd: 1,
	markupPercent: 100,
	markupByCategory: { ai: 100, webSearch: 50, webExtract: 25 }
}
CategoryCovers
aiModel usage on a built-in key: chat, chat titles, automations.
webSearchThe web_search tool's per-call search-vendor charge.
webExtractThe web_extract tool's per-page extraction charge.
  • Your own feature picks its own key: costToCredits(cost, "serp"), then add a serp entry when you want it margined apart.
  • A category with no entry takes markupPercent, so adding the map re-prices only what it names. 0 bills that category at cost - legitimate, but it becomes your lowestMarkup, so re-run the solvency check below before shipping it.
  • A broken entry throws at debit time (non-finite or negative), exactly like a broken markupPercent. An unrecognized key is ignored silently, since it may be a category you have not shipped yet - check spelling against the table.

The category is not the ledger's feature tag. Web-tool calls settle under the run's own tag (ai.chat) alongside the model usage they run beside, so pricing needs a dimension of its own.

The solvency check

Credit packs and custom purchases sell 1 credit for 1 base-currency unit, so at the shipped values a credit carries about 0.50 USD of cost - a ~50% gross margin. Whenever you change plan credits, prices, or any markup, verify each plan still holds at your lowest markup:

plan.credits / (creditsPerUsd * (1 + lowestMarkup / 100))  <  plan revenue per credit interval
  • lowestMarkup is the smallest of markupPercent and every markupByCategory entry. A plan's whole allotment can go to the one service you margin least, so the cheapest category sets the worst case - at the shipped values an at-cost category doubles a 40-credit plan's worst case from $20 to $40.
  • Check every interval, not the monthly sticker. A discounted yearly price earns less per 30-day credit grant than the monthly one, so it is the interval that fails first. pricing-money.test.ts runs this check over every plan, price, and currency you ship.
  • Non-USD storefronts: providers bill in USD while your prices are in your base currency. If it is weak against the dollar (or zero-decimal, like JPY), re-run the check in USD terms and raise prices or creditsPerUsd.
  • Lifetime prices: a lifetime holding of any non-free plan receives its allotment exactly once, at purchase, whatever its creditInterval - the interval applies only to recurring purchases.

On this page