/** * Pricing: the single source of truth for every price on the platform. Pure data, shared by server and UI. * * Every price is Cloudflare's published cost for the resource times MARKUP. Meters (src/lib/meters.ts), * plans (src/lib/plans.ts), prepaid packs, the Stripe setup script and the billing page all derive from here, * so changing a cost or the markup updates everything at once. */ /** Target price over cost for metered usage and prepaid packs (1.75x, about 43% gross margin). */ export const MARKUP = 1.75; /** Pro subscription, per month. Its included allowances are sized so even a maxed-out month keeps a margin. */ export const PRO_PRICE_CENTS = 1500; /** Custom domain add-on, per domain per month (Cloudflare for SaaS bills us per hostname past the first 100). */ export const CUSTOM_DOMAIN_PRICE_CENTS = 500; /** Most custom domains one account may attach across its Sites. */ export const CUSTOM_DOMAIN_LIMIT = 25; const SECONDS_PER_HOUR = 3600; const MICRO_CENTS_PER_DOLLAR = 1e8; /** Cloudflare list prices (USD), from developers.cloudflare.com pricing pages, Oct 2026. */ export const CLOUDFLARE_COSTS = { containerVcpuSecond: 0.00002, containerGibSecond: 0.0000025, containerDiskGbSecond: 0.00000007, r2GbMonth: 0.015, workersMillionRequests: 0.3, workersMillionCpuMs: 0.02, /** Blended AI token cost: the default merge-fix model (glm-5.3-flash, $0.15/M in, $0.50/M out) at 70% input. */ aiMillionTokens: 0.255, /** One platform AI call's fixed overhead, mostly the Clef decision (~2.5k input tokens at $0.24/M). */ aiCall: 0.0006, } as const; export type ContainerSpec = { id: string; vcpu: number; memoryGib: number; diskGb: number }; /** Container instance types used by cloud runners and editors (see wrangler.jsonc). */ export const CONTAINER_SPECS = { "standard-1": { id: "standard-1", vcpu: 0.5, memoryGib: 4, diskGb: 8 }, "standard-2": { id: "standard-2", vcpu: 1, memoryGib: 6, diskGb: 12 }, "standard-4": { id: "standard-4", vcpu: 4, memoryGib: 12, diskGb: 20 }, } as const satisfies Record; /** * Hourly cost of a container at full CPU. Cloudflare bills CPU only while active, so this is the worst case: * real cost is lower, which is margin we keep rather than a risk we take. */ export function containerCostPerHour(spec: ContainerSpec): number { const perSecond = spec.vcpu * CLOUDFLARE_COSTS.containerVcpuSecond + spec.memoryGib * CLOUDFLARE_COSTS.containerGibSecond + spec.diskGb * CLOUDFLARE_COSTS.containerDiskGbSecond; return perSecond * SECONDS_PER_HOUR; } /** Published rates per resource-hour, so tiers are priced by what they use (shown in the billing (i) tip). */ export const RESOURCE_RATES = { vcpuHour: CLOUDFLARE_COSTS.containerVcpuSecond * SECONDS_PER_HOUR * MARKUP, gibHour: CLOUDFLARE_COSTS.containerGibSecond * SECONDS_PER_HOUR * MARKUP, diskGbHour: CLOUDFLARE_COSTS.containerDiskGbSecond * SECONDS_PER_HOUR * MARKUP, }; export function containerPricePerHour(spec: ContainerSpec): number { return containerCostPerHour(spec) * MARKUP; } const GB = 1024 ** 3; function perSecond(spec: ContainerSpec): number { return (containerPricePerHour(spec) / SECONDS_PER_HOUR) * MICRO_CENTS_PER_DOLLAR; } /** Price per meter unit in micro-cents (1e-6 cent), above the plan's included amount. 0 means free. */ export const METER_PRICES = { "storage.artifacts": (CLOUDFLARE_COSTS.r2GbMonth * MARKUP * MICRO_CENTS_PER_DOLLAR) / GB, "storage.lfs": (CLOUDFLARE_COSTS.r2GbMonth * MARKUP * MICRO_CENTS_PER_DOLLAR) / GB, "storage.ci_cache": (CLOUDFLARE_COSTS.r2GbMonth * MARKUP * MICRO_CENTS_PER_DOLLAR) / GB, "storage.sites": (CLOUDFLARE_COSTS.r2GbMonth * MARKUP * MICRO_CENTS_PER_DOLLAR) / GB, "storage.r2": (CLOUDFLARE_COSTS.r2GbMonth * MARKUP * MICRO_CENTS_PER_DOLLAR) / GB, "compute.runner": perSecond(CONTAINER_SPECS["standard-1"]), "compute.runner_large": perSecond(CONTAINER_SPECS["standard-2"]), "compute.runner_xlarge": perSecond(CONTAINER_SPECS["standard-4"]), "compute.runner_self_hosted": 0, "compute.editor": perSecond(CONTAINER_SPECS["standard-1"]), "compute.site_requests": (CLOUDFLARE_COSTS.workersMillionRequests * MARKUP * MICRO_CENTS_PER_DOLLAR) / 1e6, "compute.site_cpu": (CLOUDFLARE_COSTS.workersMillionCpuMs * MARKUP * MICRO_CENTS_PER_DOLLAR) / 1e6, "compute.workflow_steps": 0, // R2 egress is free, so bandwidth is not resold; free plans still hard-cap it to stop abuse. "network.lfs": 0, "network.sites": 0, "network.downloads": 0, "ai.calls": CLOUDFLARE_COSTS.aiCall * MARKUP * MICRO_CENTS_PER_DOLLAR, "ai.tokens": (CLOUDFLARE_COSTS.aiMillionTokens * MARKUP * MICRO_CENTS_PER_DOLLAR) / 1e6, "ai.byok_calls": 0, "ai.byok_tokens": 0, "ops.integration_deliveries": 0, "ops.mcp_calls": 0, "ops.git_ops": 0, "ops.git_bytes": 0, "ops.action_fetches": 0, // Billed by the custom domain add-on subscription, not as metered usage. "domains.custom": 0, } as const; /** Dollars for `quantity` units of a meter. */ export function meterPriceDollars(meter: keyof typeof METER_PRICES, quantity: number): number { return (METER_PRICES[meter] * quantity) / MICRO_CENTS_PER_DOLLAR; } /** * Prepaid credit pools. Compute credit is held in standard-1 runner seconds and spent by every priced compute * meter at its price relative to standard-1 (a standard-4 second uses ~5.4 of them). Token credit is spent 1:1. */ export const CREDIT_POOLS = { compute: { id: "compute", label: "Compute credit", unit: "seconds", baseMeter: "compute.runner" }, "ai.tokens": { id: "ai.tokens", label: "AI token credit", unit: "tokens", baseMeter: "ai.tokens" }, } as const; export type CreditPoolId = keyof typeof CREDIT_POOLS; /** The credit pool a meter spends from, and how many pool units one meter unit costs; null when it can't. */ export function creditPoolFor(meter: string): { pool: CreditPoolId; factor: number } | null { const price = (METER_PRICES as Record)[meter] ?? 0; if (price <= 0) return null; if (meter === "ai.tokens") return { pool: "ai.tokens", factor: 1 }; if (meter.startsWith("compute.")) return { pool: "compute", factor: price / METER_PRICES["compute.runner"] }; return null; } export type PackDef = { id: string; pool: CreditPoolId; quantity: number; label: string; cents: number }; const HOUR = SECONDS_PER_HOUR; const MILLION = 1e6; function packCents(pool: CreditPoolId, quantity: number): number { return Math.round(meterPriceDollars(CREDIT_POOLS[pool].baseMeter, quantity) * 100); } function pack(id: string, pool: CreditPoolId, quantity: number, label: string): PackDef { return { id, pool, quantity, label, cents: packCents(pool, quantity) }; } /** Prepaid packs, priced at exactly the overage rate: buying ahead never costs more than paying as you go. */ export const PACKS: PackDef[] = [ pack("compute_10h", "compute", 10 * HOUR, "10 standard-1 hours"), pack("compute_50h", "compute", 50 * HOUR, "50 standard-1 hours"), pack("compute_200h", "compute", 200 * HOUR, "200 standard-1 hours"), pack("tokens_10m", "ai.tokens", 10 * MILLION, "10M AI tokens"), pack("tokens_50m", "ai.tokens", 50 * MILLION, "50M AI tokens"), pack("tokens_200m", "ai.tokens", 200 * MILLION, "200M AI tokens"), ];