Advertesting · Test Before You Invest

Dynamic Pricing Engine v3 — Specification

From a demand equalizer to a revenue science instrument, with humans approving every move.

Status Draft for review Target org AIdev sandbox Date 2026-08-13 Author Claude, for Gabriel & the Advertesting team

0Executive summary

Advertesting exists to answer one commercial question before a single dollar of construction risk is taken: at what prices does this building earn the most money while still selling on schedule? The v2 engine (accedev2) proved the mechanics — score-based evidence windows, statistically honest bias detection, full auditability — but it optimizes the wrong thing: it equalizes demand across segments. Equal demand is a proxy; the target is maximum revenue at target absorption speed.

v3 rebuilds on AIdev around four upgrades, in order of consequence:

  1. The objective becomes revenue × velocity. The engine estimates a real demand model — how choice probabilities move with price — and recommends the price vector that maximizes expected revenue subject to a sell-through constraint. Equalizing velocity falls out as a special case; it stops being the goal.
  2. All demand signals, one honest ballot. Every lead contributes exactly one ballot (preserving statistical independence), but the ballot is shaped by their whole journey: every selection, option depth, favorites, and comment sentiment — including the first negative demand signal.
  3. Humans approve every price move. The engine writes Dynamic Pricing Recommendation records — which units or segments, which direction, how much, and exactly why — and prices change only on approval. Approvals, corrections, and declines become training data.
  4. Simulation by personas, not random draws. The bench becomes a market simulator: budget-constrained buyer archetypes choosing by utility, which makes price elasticity, substitution, convergence to equilibrium, and mispricing-correction all testable — with a known ground truth to verify against.
Componentv2 (accedev2)v3 (AIdev)
ObjectiveEqualize demand share to inventory shareMaximize revenue subject to absorption target
Demand inputOne best selection per leadFull-journey ballots: selections + options + favorites + comment sentiment
Price changesAutomatic, cappedRecommendation → human approval → apply
StatisticsPer-segment z-tests on sharesz-screening kept + choice model, hierarchical shrinkage, causal price experiments
SimulationSeeded multinomial drawsUtility-driven personas with budgets and behaviors; optional LLM-agent mode
Evidence cadenceScore-point windows, Kish n_eff, decision logCarried over as-is
ConsoleCloudflare Worker + PlotlyCarried over, + Recommendations, Demand Model, Optimizer, Personas tabs
accedev2 assetsFrozen in place as reference — nothing deleted, nothing further deployed there

1Objective & success metrics

1.1 The commercial objective, stated as math

For a project with units j = 1…J, price vector p, and a demand model giving each unit an expected absorption rate Dj(p) (demand responds to all prices — buyers substitute), the engine's target is:

maximize R(p) = Σ_j p_j · D_j(p) subject to absorption: every unit reaches its sell-through target by horizon T rails: p_min,j ≤ p_j ≤ p_max,j fairness: per-period step caps, damping, approval gates

The velocity constraint enters as a Lagrangian term — an urgency dial λ: at λ = 0 the engine prices for pure revenue; as λ grows it trades margin for speed. This dial is a business control, not a statistical one, and it surfaces in the console as an explicit revenue ↔ velocity frontier the team can drag along.

Why this beats equalizing Under a logit demand model with a single price-sensitivity, revenue-optimal prices imply roughly equal absorption — v2's rule was the special case. But real demand has heterogeneous price sensitivity (an "affordable max" buyer and a luxury buyer do not trade dollars for attributes at the same rate), and then the two objectives diverge: the revenue-optimal move prices each segment to its marginal revenue, deliberately letting hot premium stock sell a bit slower at a much better price. v3 prices to marginal revenue.

1.2 What "revenue" and "velocity" mean pre-market

Advertesting runs before real closings exist, so both quantities are model-based but concretely measurable:

  • Expected revenue = Σ pj × P(unit j absorbed by horizon | demand model at current evidence). Reported with uncertainty bands, never as a bare number.
  • Velocity = demand intensity per unit (ballot-weighted arrival rate), the pre-market proxy for time-to-sell. Modeled as Poisson arrival rates with conjugate (Gamma) updating, so early estimates are honest about their width.
  • Headline KPI for the program: simulated revenue lift versus two baselines — a uniform price list and the v2 equalizer — at equal sell-through. This is the number that justifies the product: "pricing found +X% revenue at the same absorption."

2Data foundation — full-signal ballots

2.1 The independence invariant

Every statistical guarantee in the engine rests on one rule: one lead, one ballot. v2 enforced it crudely by keeping only the best selection. v3 keeps the invariant and drops the crudeness: the ballot's mass stays capped by the lead's engagement weight, but its distribution across units is shaped by everything the lead did.

2.2 Affinity model

affinity_i(j) = Σ selections of j × w(option depth: 0.1 / 0.3 / 0.5, VIP 1.0-cap) + favorite(j) × w_fav + comment(j) × w_com × sentiment (positive +1 / neutral 0 / negative −1) ballot_i(j) = mass_i · max(0, affinity_i(j)) / Σ_k max(0, affinity_i(k)) negative affinity is carried separately as a signed demand signal (§3.1)
  • Selections (all of them): breadth is attribute information. A lead picking three corner units casts nearly their whole ballot into "corner" at the segment level — sharper, not noisier, than v2's single pick.
  • Favorites (Advertesting_Favorite__c): deliberate intent between "clicked" and "configured options"; its weight sits between the selection tiers and is a site-level setting.
  • Comments (Advertesting_Unit_Comment__c + the prod sentiment pipeline): the first signed signal. A negative comment is presence-of-dislike — different information from absence-of-interest, and precisely what the down direction has been missing. Price-mention comments ("too expensive") get their own tag over time: that is direct elasticity feedback in natural language.
  • Timing: every touch carries its timestamp and the prices in force at the time (Price_at_Selection__c) — historical price context is what turns observational data into elasticity evidence (§3.3).

2.3 Calibration before trust

w_fav and w_com start at conservative defaults and are calibrated, not guessed: the Correlations workbench measures how favorites and sentiment actually predict deep engagement (e.g., "a favorite converts to an all-options selection 60% of the time" ⇒ w_fav ≈ 0.6 × w_all). Until calibrated, the new signals run in measure-only mode — visible everywhere, pricing nothing.

3Demand science

Three layers, each auditable, each feeding the next. The v2 screening layer survives intact as layer 0.

3.0 Screening (carried from v2)

Per-segment bias vs fair share, gated by z × SE on the window's effective sample size (Kish), with per-dimension z overrides and measure-only dimensions. This stays the fast diagnostic and the anomaly alarm — it is not the pricing brain anymore.

3.1 Choice model — the heart of v3

Demand is modeled as discrete choice: a lead selects among the units they can afford with probability rising in utility and falling in price (multinomial logit; the standard demand model of empirical economics):

U_ij = β' x_j − α_i · p_j + ε_ij (ε ~ Gumbel) P(lead i chooses j) = exp(V_ij) / Σ_k∈affordable(i) exp(V_ik) x_j : the unit's attribute vector — the existing Pricing Dimensions become the feature basis of a hedonic value model (β = $-value of attributes) α_i : price sensitivity, heterogeneous across latent classes (§3.2) Negative signals enter as observed "considered and rejected" outcomes, which sharpen utility estimates far more than non-observations.

What this buys, concretely: own-price elasticity (how much demand a +3% move costs), cross-price substitution (raising unit A pushes demand to lookalike B — cannibalization is finally modeled, not ignored), willingness-to-pay distributions per attribute (the hedonic β in dollars), and honest uncertainty on all of it.

3.2 Hierarchy and heterogeneity

  • Hierarchical shrinkage (partial pooling): unit-level estimates borrow strength from their segments (unit ← typology ← attribute groups). This is the principled cure for the unit-sparsity problem we kept meeting (~0.7 votes per unit per batch): small-n units get stabilized estimates instead of silence or noise-chasing.
  • Latent classes = personas: price sensitivity and tastes vary by buyer type. The estimator fits a small number of latent classes (mixed logit) — and these are mathematically the same objects as the simulation personas (§6). Fitting the model to real data is persona discovery; comparing fitted classes to authored personas closes the calibration loop.

3.3 Causal identification — the part most engines get wrong

The endogeneity trap If prices are only ever set in response to demand, then price and demand are correlated by construction and a naive elasticity estimate is garbage (often the wrong sign). The only clean cure is randomized price variation: small, bounded, randomized perturbations (±ε%) layered onto recommended prices, scheduled so every segment sees multiple price points over time. Pre-market is the perfect place for this — exploration costs no real revenue. This is Advertesting's structural advantage: a licensed laboratory for price experiments that would be expensive after launch.
  • Exploration policy: Thompson-sampling-flavored — perturb more where the posterior on elasticity is widest, always inside the rails and the human-approval gate (perturbation schedules are themselves recommendations to approve).
  • Always-valid monitoring: the team watches dashboards continuously, so sequential-testing corrections (alpha-spending / e-value style) back the "significant" labels, not fixed-horizon p-values that stop being valid the moment you peek.
  • Multiple-comparison control: carried from v2 (per-dimension z), extended with false-discovery framing on the screening tables.

4Price optimization

  • Optimizer: maximize expected revenue under the absorption constraint over the choice model, solved numerically (fixed-point / projected gradient — closed forms exist for homogeneous logit and validate the code). Outputs a full recommended price vector with per-unit predicted demand, revenue impact, and credible intervals.
  • Cadence: optimization runs on the evidence cadence carried from v2 (every N score points of new deduped evidence, sliding K-window), plus on-demand from the console.
  • Guardrails (all carried, all still needed): launch-price rails (±%), per-period step caps, direction-flip damping, and — new — the approval gate of §5 in front of every applied change. The engine never touches a price directly in v3's default mode.
  • Granularity: the optimizer prices units (it must — substitution is unit-level), but recommendations are grouped by driver for human review: "these 7 Type-15B units +2.1% because…" — one decision, many units, full fan-out visible.

5Dynamic Pricing Recommendations — human-in-the-loop

5.1 Object: Dynamic_Pricing_Recommendation__c

FieldTypePurpose
Site / Runlookup / numberProvenance: which evidence window produced it
ScopepicklistUnit or Segment (segment recs fan out to member units)
Unit / Segment descriptorlookup / textTarget: a unit, or "Floor_Plan_Type = Type 15B"
Current → Recommended pricecurrency ×2, %The move, absolute and relative
Expected revenue Δ / velocity Δcurrency / numberModel-predicted consequences, with intervals
Confidencepercent + interval textPosterior credibility, honestly stated
Reasonlong textThe full evidence chain in plain language: demand stats, elasticity, substitution effects, which signals (selections / favorites / sentiment) drove it, model version
StatuspicklistPending → Approved | Approved with Correction | Declined, then Applied / Expired
Corrected price + correction reasoncurrency + picklist/textWhen humans adjust before approving — the most valuable label of all
Decline reasonpicklist + textStructured taxonomy (too aggressive / wrong direction / timing / data doubt / business override)
Reviewed by / at, Applied atuser / datetimesFull audit trail
Feature snapshotlong text (JSON)Frozen model inputs at recommendation time — the training-data payload

5.2 Workflow

Engine writes recommendations → reviewers act in the console (or on the Salesforce record) → an apply job executes approved prices and stamps the price-change log with the recommendation reference. Nothing changes price without a human verdict; recommendations left unreviewed past their evidence window expire rather than execute stale. A per-site auto-apply switch exists for simulation runs only, so the bench keeps its speed.

5.3 Learning from the verdicts

Every reviewed recommendation is a labeled example, and the schema is designed for training from day one:

  • Calibration: systematic corrections are measurable bias ("humans trim recommended decreases by ~40%") and feed straight back as adjustment multipliers.
  • Constraint mining: clustered declines reveal unwritten rules ("never move penthouses more than 1% at a time") that become explicit, configurable guardrails.
  • Approval model: with volume, a lightweight model of P(approve | rec features) pre-ranks and pre-filters what the engine proposes — the engine learns the team's judgment, visibly and auditable.
  • Honest note: this loop needs tens-to-hundreds of verdicts before it teaches anything. Phase one is simply to log perfectly; learning activates when the data has mass.

6Persona simulation lab

6.1 Why personas, in one sentence each

  • Elasticity becomes real: budget-capped personas re-decide after every repricing, so demand pushes back and convergence-to-equilibrium is finally testable.
  • Correlated demand becomes real: a "growing family" wants 3bd ∩ quiet ∩ mid-floor as a bundle — exactly the entangled demand that stress-tests attribution.
  • Mispricing detection becomes testable: "bargain hunters" chase price-below-value gaps; seed a deliberate pricing error and verify the engine finds and corrects it.

6.2 Config: Advertesting_Test_Persona__c

Per persona (site child): mix %, budget rule (price percentile band), price sensitivity α, utility terms in the familiar seed syntax (Corner_Unit__c=true:+3 …), and behavior rates — selections per lead, favorite propensity, comment propensity with a sentiment profile, option depth, VIP probability. Gabriel's six examples (affordable max, bargain hunters, growing families, downsizers, WFH singles, premium buyers) ship as the starter set; the flat "noise" persona reproduces today's silence-run baseline.

6.3 Generator

Each simulated lead: draw persona → filter to affordable units → choose by softmax over utility (a logit model — the same mathematical family the estimator fits, which is the point: known ground truth in, estimated parameters out, compare) → generate the journey: selections, options, favorites, comments with sentiment. An optional LLM-agent mode role-plays personas to produce realistic free-text comments through the actual sentiment pipeline — texture and pipeline testing, small batches, not the statistical workhorse.

6.4 Acceptance suite (the definition of "the engine works")

  1. Silence: flat persona ⇒ no recommendations beyond the false-alarm budget.
  2. Detection & attribution: seeded persona demand ⇒ the right dimensions flagged, entangled ones correctly shrunk.
  3. Mispricing correction: one unit priced −8% below hedonic value + bargain hunters ⇒ recommendation to raise it toward value.
  4. Convergence: with known utilities the true optimal price vector is computable ⇒ engine recommendations converge to it within tolerance, without oscillation.
  5. Revenue lift: engine pricing beats uniform pricing and the v2 equalizer on simulated revenue at equal absorption — the headline number.
The hidden prize A persona population calibrated against real Advertesting data (via the latent classes of §3.2) stops being test tooling: it becomes a digital twin of the buyer market — run a brand-new project's draft price list against it before launch, preview "all 2bd +4%" what-ifs, and stress-test absorption scenarios. That is a sellable capability, not a test rig.

7Architecture on AIdev

7.1 Separation of concerns

TierRunsResponsibilities
Salesforce (AIdev)Apex + objectsData capture (ballot signals), evidence bookkeeping (score cadence, decision log), screening layer, Recommendation records + approval workflow + apply job, persona/bench config, price-change audit. No heavy math in Apex — governor limits make it the wrong home for estimation.
Pricing BrainCloudflare WorkerChoice-model estimation, hierarchical fitting, optimizer, exploration scheduler. Reads via the Connected-App pattern proven on accedev2; writes Recommendations back through the API. Versioned models; every recommendation stamped with its model version.
Console v2Worker + Plotly SPAEverything the team loved, plus: Recommendations (review / correct / approve / decline inline), Demand Model (fit diagnostics, elasticity curves, WTP by attribute), Optimizer (frontier explorer with the urgency dial, what-if pricing), Personas (mix + utility editors). Carried: Overview, Run Decisions, Trends, Correlations, Unit Moves, Price Changes, Bench, How It Works.

7.2 AIdev readiness checklist (known sharp edges)

  • AIdev was freshly refreshed (new org id): unmanaged metadata is wiped — everything deploys clean, nothing can be assumed present.
  • Einstein / Generative AI enablement is dropped by refresh and, until re-enabled, breaks all Apex test runs and the comment-sentiment prompt template. First task of Phase 0.
  • Comment sentiment package (trigger + queueable + template) redeploys from the prod-parity repo; favorites and comments objects exist from the prod clone.
  • New Connected App for the Brain/console (key/secret minted in metadata, as before); permission set for the suite; FLS granted explicitly for every deployed field (the "No such column" lesson, now doctrine).
  • Data bed for the bench: cloned MSH units exist in AIdev; bench leads are generated on demand by the loader. Real prod campaign data stays untouched in prod.
  • accedev2: left intact as a frozen reference. Repos remain the source of truth for anything worth porting.

8Phasing & effort

PhaseDeliversEffort*
0AIdev readiness: Einstein enablement, sentiment deploy, Connected App, permset, data bed½ day
1Data foundation: ballot model (selections + favorites + sentiment), evidence cadence, screening engine, decision log — the lean v2 core rebuilt on full signals1–1½ days
2Recommendation object + approval workflow + apply job + console Recommendations tab (engine runs recommend-only from day one)1 day
3Persona lab: persona object, logit generator with journeys, bench v3, acceptance scenarios 1–31–1½ days
4Pricing Brain: choice-model estimation, hierarchical shrinkage, optimizer + frontier UI, randomized exploration scheduler; acceptance scenarios 4–52–3 days
5Feedback learning v1: verdict analytics, calibration reports, approval pre-ranking (activates once verdict volume exists)1 day

*Working-session estimates for scoping, not commitments. Phases 1–3 produce a reviewable, demo-able system before the heavy science of Phase 4 lands; each phase ends with tests green and the console updated.

9Open questions for review

  1. Bench site on AIdev: run simulations against the cloned Mont-Saint-Hilaire inventory, or build a dedicated synthetic tower so the bench never shadows a real project?
  2. Approvers: who reviews recommendations (you, the analyst, both — any approval quorum)? Should Declined require a reason to save (I recommend yes — it's the training label)?
  3. Auto-apply policy: strictly recommend-only for real projects forever, or eventually threshold-gated auto-apply (e.g., |Δ| ≤ 1% and confidence ≥ 95%) once trust is earned? (v3 ships recommend-only; the switch exists only for simulations.)
  4. Exploration consent: randomized ±ε% price perturbations are scientifically essential (§3.3) — are you comfortable approving them as scheduled recommendation batches, and what ε feels acceptable for a live Advertesting campaign (I suggest ε ≤ 2%)?
  5. Absorption target: how do you want to express the velocity constraint pre-market — "every unit reaches X demand intensity within Y weeks", or a portfolio-level absorption curve?
  6. Labels & language: the Recommendation object will be seen by non-analyst colleagues — French labels / bilingual descriptions per your Salesforce component standards, or English-only like the console?
  7. LLM budget: agent-mode simulation and LLM-drafted recommendation reasons both consume API tokens — greenlight as optional features with a monthly cap, or defer?

10Glossary for reviewers

Discrete choice / multinomial logit (MNL)
The standard economic model of "a person picks one option from a menu": each option gets a utility score; higher utility and lower price raise its probability. Fitting it to Advertesting ballots yields elasticity, substitution, and willingness-to-pay.
Hedonic pricing
Decomposing a unit's value into the dollar value of its attributes (a corner is worth $X, a 4th floor $Y). The fitted β of the choice model, read in dollars.
Elasticity (own- and cross-)
How much demand changes when a price changes — for the unit itself (own) and for its substitutes (cross). The engine's steering wheel.
Endogeneity
The statistical trap where prices already depend on demand, so "price vs demand" correlations lie. Cured only by deliberately randomized price variation.
Thompson sampling
An exploration strategy: probe more where you know least, automatically balancing learning against earning.
Hierarchical shrinkage / partial pooling
Small-sample estimates (one unit) borrow strength from their group (its typology, its segment), preventing both noise-chasing and silence.
Latent classes / mixed logit
Letting the model discover that buyers come in types with different tastes and price sensitivities — the statistical mirror of the personas.
Kish effective sample size (n_eff)
What a weighted sample is really "worth" in unweighted observations; all significance bars are computed on it.
Always-valid (sequential) inference
Significance guarantees that survive continuous dashboard-watching, unlike classical p-values which assume you look exactly once.
Revenue–velocity frontier
The curve of best-achievable trade-offs between total revenue and sell-out speed; the urgency dial λ picks the operating point.