Building a Bayesian Pricing Engine in TypeScript
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