PricingSimBlog

Building a Bayesian Pricing Engine in TypeScript

February 10, 2025 · 10 min read · PricingSim Team

When I started building PricingSim, I faced a core problem: how do you run a meaningful pricing experiment with 30–100 sales per month?

Traditional A/B tests need 200–1,000 conversions per variant for significance. At 50 monthly sales, that's a 2-year wait. Useless.

Bayesian inference changes the question: instead of "is this statistically significant?", it asks "what's the probability Price B generates more revenue than Price A?" That's answerable with 20–40 data points.

The Core Model

We model price-demand as a log-linear regression:

log(Q/Q_ref) = ε · log(P/P_ref) + noise

Where ε is price elasticity. Our prior: Normal(-1.0, 0.5²) — most digital products sit near unit elasticity. The prior shrinks with sparse data and lets the data speak as observations accumulate.

Normal-InvGamma Conjugate Update

We use the NIG conjugate prior — closed-form posterior, no MCMC needed:

function nigUpdate(x: number[], y: number[], priorMu = -1.0, priorSd = 0.5): NIGPosterior {
  const lam0 = 1 / (priorSd * priorSd)
  const N = x.length
  let Sxx = 0, Sxy = 0, Syy = 0
  for (let i = 0; i < N; i++) {
    Sxx += x[i]*x[i]; Sxy += x[i]*y[i]; Syy += y[i]*y[i]
  }
  const lamPost = lam0 + Sxx
  const muPost  = (priorMu * lam0 + Sxy) / lamPost
  const aPost   = priorA + N / 2
  const bPost   = priorB + 0.5*Syy + 0.5*priorMu*priorMu*lam0 - 0.5*muPost*muPost*lamPost
  return { mu: muPost, lam: lamPost, a: aPost, b: bPost }
}

5 observations → prior dominates. 50 observations → data dominates. Exactly the right behavior.

Conservative Recommendation Rule

The optimizer maximizes E[Revenue] subject to a downside constraint: the 5th-percentile outcome must stay above 95% of current revenue.

function findOptimalPrice(pRef, rRef, samples) {
  const floor = rRef * 0.95  // max 5% downside
  let bestPrice = 0, bestMean = -Infinity

  for (let mult = 1.1; mult <= 2.5; mult += 0.05) {
    const dist = predictRevenue(pRef * mult, pRef, rRef, samples)
    if (dist.p05 < floor) continue  // reject — too risky
    if (dist.mean > bestMean) { bestMean = dist.mean; bestPrice = pRef * mult }
  }
  return bestPrice
}

Spike Detection (MAD Filter)

A ProductHunt launch creates a sales spike at a discounted price that ruins the elasticity estimate. We flag outliers using Median Absolute Deviation (better than std dev because it's not inflated by the outliers themselves):

// Modified Z-score > 3.0 = spike (Iglewicz & Hoaglin, 1993)
is_spike = mad > 0 ? (0.6745 * Math.abs(qty - median) / mad) > 3.0 : false

Stack: Pure TypeScript, No External Math Libs

Zero external dependencies — no ml-matrix, no tensorflow.js. The NIG update, Cornish-Fisher quantile approximation, and Box-Muller sampler are hand-implemented. The engine runs in a Next.js App Router Route Handler, backed by Supabase with Row Level Security isolating each user's data.

Try PricingSim free: pricingsim.com

Try PricingSim free

Connect your store and run your first pricing experiment in under 10 minutes.

Get started free →

Related tools & guides

Price Elasticity Calculator — estimate revenue impact before running an experimentGuide: The Solo Seller's Complete Guide to Pricing ExperimentsGuide: Stripe Price Testing Without CodeGuide: Gumroad Price Updates & Churn RiskPricingSim Pricing — free tier + Pro at $29/month
← All posts