Given a booking slump, return a deterministic verdict — is it a listing problem or a market problem — with a confidence score on the verdict line, plus the evidence and (light) next steps.
Agree these first — everything downstream assumes them.
Rules decide, the LLM narrates. The verdict and confidence are computed by fixed thresholds. The LLM only turns the computed result into customer-safe language. If the LLM decides "me vs market," we fail the eval's 4–5×-same-result requirement. This is the most important line in the doc.
Five verdict states, not two. No Slump · Market · Mixed · Me · Winning — plus two override states (On Strategy, Late Not Lost) and one abort state (Inconclusive). An agent that only ever says "me or market" is wrong about a third of the time.
Pace-matched, always. Every STLY comparison is on-the-books-now vs on-the-books-at-the-same-lead-time-last-year — never vs final realized STLY. This one rule prevents the most common false alarm.
Customer-safe by default. SCs forward the output. No comp identities, no owner-block snark, language that survives a paste into a customer email.
Diagnose, don't solve. Scope is the verdict + the lever, not a solution engine. We point at "relax min-stay here" / "DSO X% on this range," we don't build the fix.
Four sources, and they converge on the same picture.
Four numbers — occ, occ-STLY, market-occ, market-occ-STLY. Two senses of underperformance: growth (not growing as fast as market) and absolute. Market context is everything (is the market up or down?). Flagged the traps: fall-foliage strategy listings, phantom occupancy (blocks read as occ in ski markets), priced-out-masquerading-as-low-ADR, RevPAR-negative "wins," like-to-like, large-unit comp dilution, weekday/weekend split.
Visibility (sync, alerts, min-stay) supersedes pricing — check it first. Booking window + pickup (7/14/30-day; 7-day is noise). "Me" tells: far-out 61+ premium missing early bookings, misconfigured customizations, luxury tag on a standard unit. "Market" tells: market pacing behind, low pickup market-wide, cancelled events. Fix via DSO, not base-price moves.
Segment/leaderboard pacing, MPI (occ vs market ×100), ADR index, RevPAR index. Reads MPI + ADR index together, judged against property tier. Notably does not use the Account Review helper for this question.
The classifier primitive with fixed, eval-stable thresholds. We adopt it verbatim as the core check and wrap it — we do not rebuild it.
The shipped Account Review logic is our verdict primitive. We adopt its core check and thresholds verbatim. We do not rebuild it — the agent is the pace-matching layer before it and the RM judgment layer after it.
Confirm whether the Account Review core check pace-matches STLY at equal lead time for future months, or compares against final realized STLY. If it does not pace-match, we must pace-match upstream and feed it matched numbers. Owner: Revanth (with Anish). Nothing downstream is trustworthy until this is answered.
0. Resolve scope (entity, window, KPI — default occ, ask)
1. Pull data spine (pace-matched 4 numbers + signals) ← we pace-match here
2. Validity gates (comp size, like-to-like, phantom, sync, tier → set confidence / abort)
3. Validate the claim (is it even slow? maybe "No Slump", stop — ~⅓ exit here)
4. Core check (Account Review thresholds, verbatim → Under / At Par / Over)
5. Market overlay (+ market direction → Me / Market / Mixed / Winning)
6. RevPAR overlay (§5 On-Track logic → catch market-wide decline & the win-that-isn't)
7. Booking-window veto (→ On Strategy / Late Not Lost) ← not in helper
8. Mechanism decompose (only if "me": visibility→settings→price→conversion) ← not in helper, STRETCH
9. Segment (weekday/weekend, sub-windows, events)
10. Confidence score (graduated, deterministic) ← not in helper
11. Narrate (LLM → customer-safe card)
Stages 4–6 are the helper running as-is. Stages 7, 8, 10 are the RM transcript logic. Stage 1 is where the two worldviews get reconciled.
It's diagnosis, not solution, so it's in — but treat it as enrichment. Drop it under time pressure and the agent is still complete on Stages 3–7 + 10.
Eight states, not two — five base verdicts plus two overrides and one abort.
| State | Meaning |
|---|---|
| No Slump | Pacing at/above own STLY and market — nothing wrong. Return proof, stop. |
| Market | You fell ≈ as far as the market — not your fault. The "wait it out" answer. |
| Mixed | Both down, you worse. Report split: market component vs self-inflicted component. |
| Me | Market held/rose, you fell. Listing-side gap. |
| Winning | You up, market down. Run underpricing caution (MPI ≫100 + low ADR index). |
| On Strategy (override) | Looks like "Me" but it's a deliberate hold-high-liquidate-late pattern (short booking window, same as LY, LY hit target). Don't cut price. |
| Late Not Lost (override) | Looks like "Me" but pickup is healthy and demand is arriving later (window compressed). Timing, not loss. |
| Inconclusive (abort) | Data too weak to call (thin comps, dilution). Say so honestly. |
Core check → state mapping: At Par + market up = No Slump · At Par + market down = Market · Under + market flat/up = Me · Under + market down = Mixed · Over = Winning. Then apply overrides/guards on top.
Every stage of the pipeline, in order — scope resolution through the customer-safe output contract.
User inputs: entity (listing / group / comp-set level), stay-date range, KPI.
Per window, for the chosen KPI, pull four numbers:
| # | Number |
|---|---|
| 1 | Listing — on the books now |
| 2 | Listing — STLY at matched lead time |
| 3 | Comp set — on the books now |
| 4 | Comp set — STLY at matched lead time |
Supporting signals pulled alongside: listing & market booking window (current + STLY), 7/14/30-day pickup (listing + market), ADR index, RevPAR index, min-stay settings, sync status / system alerts, comp-set count, base-price tier setting.
Report Builder API only exposes user-created templates. Revanth to confirm with Anish whether we get direct Report Builder API access so we can pull the pace-matched spine for any listing/window, not just pre-built templates.
Each gate adjusts confidence; some abort a window. Skipping these is what gets a verdict laughed out of a review.
| Gate | Rule | Effect |
|---|---|---|
| Comp-set size | ≥120 comps → full trust. <120 → dampen market weight (sliding). Large-unit dilution (e.g. 7–8BR pulling in 2–5BR) → flag. | Confidence multiplier |
| Like-to-like | Compare vs STLY only if listing had ≥15 bookable nights at this point LY and this year. Else market-only. | Drops STLY axis if failed |
| Market-data sanity | Detect phantom occupancy — owner blocks read as occupancy in ski/off-season markets (the Vermont-Nov "7% that's really 0%" case). Flag thin future data. | Suppress market axis for that window |
| Sync / alerts | Stale sync or active system alerts | Warn — data may be wrong before logic runs |
| Comp-tier representativeness | If comps skew to one tier, ADR index is unreliable | Down-weight ADR-based mechanism calls |
Like-to-like uses 15 bookable nights (from the helper). Comp trust floor 120 (Revanth). Confirm both, and define the sliding-dampening curve below 120.
The RM's first move (Soumyadeep): a large share of "I'm slow" users are pacing fine or ahead. Run the core check; if the listing is at/above both its STLY pace and market pace → No Slump, return the proof table, stop. Target: ~⅓ of tickets exit here. The "why" in "why am I slow" is an implicit claim we test, not a fact we accept.
Path A — has STLY (skip → At Par if STLY occ < 5%):
listing_%chg = listing_now − listing_STLY
market_%chg = market_now − market_STLY
diff = listing_%chg − market_%chg
diff < −5pp → Underperforming
diff > +5pp → Overperforming
else → At Par
(if market side missing: use listing_%chg vs STLY alone, ±5pp)
Path B — no STLY / market-only (skip → At Par if market occ < 5%):
listing > 2pp below market AND relative gap worse than −5% → Underperforming
listing 5%+ above market → Overperforming
else → At Par
diff is our me-vs-market magnitude, already computable: the self-inflicted component = |diff| beyond the market move. No new math needed.
Overlay market_%chg direction onto the core-check label:
| Core check | Market direction | Verdict | Magnitude reported |
|---|---|---|---|
| At Par | Down | Market | market = market_%chg |
| At Par | Flat / up | No Slump | — (return proof, stop) |
| Under | Flat / up | Me | me = |diff| |
| Under | Down | Mixed | market = market_%chg; me = |diff| beyond it |
| Over | any | Winning | run overpricing / RevPAR caution |
The Mixed decomposition ("market down 6pts, you're down 19, the 13 are yours") is the line that makes the verdict actionable instead of a shrug.
The market direction above is read from occupancy pace (market_%chg). Corroborate it with market pickup: if market 14/30-day pickup is low across the board, that reinforces a Market/Mixed call; if the market is still picking up healthily while the listing isn't, that pushes toward Me. Pickup agreement/disagreement also feeds Stage 10 confidence.
Applied as a guardrail on every verdict, even when occupancy is the chosen KPI:
RevPAR On-Track floor = 95% of STLY RevPAR (verbatim from helper §5).
The helper will stamp the fall-foliage listing "Underperforming" because it's below market — the exact case that is actually fine. Guard it:
If listing booking window is materially shorter than market's and it was similarly short STLY (established pattern, not new) and LY final occupancy hit target → downgrade Me → On Strategy — monitor, re-check by [date].
Pickup cross-check: if bookings are still arriving (14/30-day pickup healthy) but late relative to the average window → Late Not Lost, distinct from both Me and Market.
Booking-window reference points (Kyle): 50%-by-market-booking-window rule; monitor from 180 days out for ~90-day windows; a shrinking average lead time signals a market trend. Use 14/30-day pickup as primary; 7-day alone is noise (post-holiday lulls).
The booking window is an average, so don't judge off it alone: if the window is 30 days, look at what's happening 60 days out too. Are bookings landing within, ahead of, or behind the average window? That distribution is what distinguishes a genuine slowdown from a normal lead-time pattern, and it's the input to the Late Not Lost call above.
Cheapest-and-most-visible first (Kyle: visibility supersedes pricing). Price is last.
Overperformance caution: MPI well above 100 is not automatically good — flag possible underpricing / over-aggressive discounting.
Run Stages 4–8 per weekday/weekend (day-of-week if available) and per sub-window (30/60/90 or per-month). It's routine to be market-soft in the next 30 and self-inflicted at 60–90, or fine weekdays and bleeding weekends. Call out events/holidays in range separately — for many users those windows are the account.
Confidence is a product of the gates, not a feeling. Bucket to High/Medium/Low, put it on the verdict line.
Inputs: comp-set size (≥120 full; sliding penalty below; heavy penalty on large-unit dilution) · like-to-like available (both axes) vs market-only · lead-time / future-data thinness · market-data sanity (phantom-block risk) · comp-tier representativeness · signal agreement (do MPI, pace delta, and pickup all point the same way? 3-way agree = High; conflict = Medium at best).
A 2-listing account at 90 days out with 40 comps → Inconclusive — Low, re-check in 14 days. Saying so is a feature.
Exact weighting / formula for the score. Proposal: multiplicative gate penalties → 0–1 scalar → banded. Team to set band cutoffs.
Every tunable value in one place, with its source and confirmation status.
| Param | Value | Source | Status |
|---|---|---|---|
| STLY spread tolerance | ±5 pp | Helper §1.3 | adopt |
| Min STLY occupancy | 5% | Helper | adopt |
| Min market occupancy | 5% | Helper | adopt |
| Market absolute diff | −2 pp | Helper | adopt |
| Market relative under | −5% | Helper | adopt |
| Market relative over | +5% | Helper | adopt |
| RevPAR On-Track floor | 95% STLY RevPAR | Helper §5 | adopt |
| Like-to-like min nights | 15 | Helper §4 | confirm |
| Comp trust floor | 120 | Revanth | confirm |
| Comp dampening curve <120 | TBD | — | 🟥 define |
| Booking-window "materially shorter" band | TBD | Soumyadeep | 🟥 define |
| Pickup primary window | 14 / 30 day | Kyle | adopt |
| Confidence band cutoffs | TBD | — | 🟥 define |
The target output is short: a verdict line, 2–3 evidence sentences, one tool pointer, a confidence + comp-set footer. Getting that short output right turned out to depend on data the pipeline hasn't specified yet. Four gaps, in the order they block the build.
The card in front of us calls out a 5-listing comp set as HIGH confidence. But §6's comp trust floor of 120 was written for market/neighborhood data. Those are two different populations with opposite confidence curves:
| Basis | What it is | Confidence curve |
|---|---|---|
| Pricing comp set | ~5 hand-matched comps used for the listing's own price recommendation | Small is fine — 5 well-matched comps = HIGH |
| Market / neighborhood data | The full local pool (could be 100s of listings) | Small is unreliable — needs ≥120 to trust (§6) |
🟥 Decision needed before any threshold is coded: pick one as the card's primary comparison (current lean: pricing comp set as the headline, market data as a fallback when the comp set is thin) and write two separate confidence-threshold tables — one per basis. Coding a single threshold set against the wrong population is how the card ends up over- or under-confident.
Two lines in the target card only work if we can count individual comps, not just average them:
🟥 Confirm before writing any narration code: does the PriceLabs MCP surface expose per-comp forward pacing and per-comp historical averages, or only the comp-set aggregate? If it's aggregate-only, these two lines can't be built, and the card collapses back to a bare level gap ("48% vs 51%") — which is exactly the failure mode Section 5's design principles forbid.
Ship the short card (verdict → evidence sentences → tool pointer → confidence footer) as the default response. Keep the full evidence table (per-window, per-segment, all raw numbers) behind an expandable "show the numbers" — not inline in the default reply.
The level gap (52% vs 68%) is presentation — a headline number. The per-comp distribution and ADR index are the evidence that justifies the verdict. Never let the LLM emit a verdict sentence from the level gap alone; it must always be backed by the distribution numbers from Stage 4–6.
Walk into any working session wanting these closed.
min(growth-lag, absolute-lag)? Changes which listings flag.
One question, ten data situations. Question throughout: "Are my bookings slow for October — me or the market?" Scope: single listing · window = October · KPI = occupancy · today ≈ 15–45 days out. All figures are invented to illustrate the logic path; occupancy is pace-matched (on-the-books-now vs same-lead-time LY).
| Listing pace (ΔL) | Market pace (ΔM) | Base verdict |
|---|---|---|
| Up, but slower than market | Up | Me |
| Down ≈ same as market | Down | Market |
| Down more than market | Down | Mixed |
| Tracking STLY & market | Flat/Up | No Slump |
| Up | Down | Winning (+ underpricing caution) |
Overrides sit on top of the base verdict: a booking-window strategy pattern → On Strategy; healthy-but-late pickup → Late Not Lost; weak data → Inconclusive. Two guards can invert the obvious read entirely (cases I, J).
Reading order per row: how the listing moved vs LY, how the market moved vs LY, then diff = the listing's move minus the market's move. diff < −5 reads as Under, diff > +5 as Over, otherwise At Par — that reading, plus the market's own direction, is what produces the final verdict.
| Case | Listing: LY → now | Market: LY → now | diff | What's happening | Verdict |
|---|---|---|---|---|---|
| A | 44 → 48 ▲+4 | 52 → 63 ▲+11 | −7 | Both growing, but the listing is growing much slower than the market. | Me |
| B | 55 → 41 ▼−14 | 60 → 46 ▼−14 | 0 | Both fell by the same amount — the listing didn't lose any ground. | Market |
| C | 55 → 38 ▼−17 | 54 → 48 ▼−6 | −11 | Both fell, but the listing fell much further than the market did. | Mixed |
| D | 51 → 58 ▲+7 | 56 → 60 ▲+4 | +3 | Listing is growing at least as fast as the market. Nothing wrong. | No Slump |
| E | 50 → 62 ▲+12 | 48 → 45 ▼−3 | +15 | Listing grew while the market shrank — but check: is it underpriced? | Winning ⚠ |
| F | 28 → 30 ▲+2 | 44 → 55 ▲+11 | −9 | Reads like "Me" — but this is the same short booking window as LY, which finished on target. | On Strategy |
| G | 50 → 40 ▼−10 | 56 → 58 ▲+2 | −12 | Reads like "Me" — but pickup is strong and the whole market is booking later this year. | Late Not Lost |
| H | 30 → 35 ▲+5 | 44 → 50 ▲+6 | −1 | Numbers say At Par, but the comp set is too small/skewed to trust that read. | Inconclusive |
| I | 34 → 22 ▼−12 | 38 → 40 ▲+2 | −14 | This is the listing — but the fix is not another price cut (rates were already too high). | Me |
| J | 60 → 78 ▲+18 | 57 → 55 ▼−2 | +20 | Occupancy is up, but it was bought with a rate cut — not a clean win. | Not a clean win |
Case H's diff is technically "At Par," but with only 45 comps skewed toward smaller units, the market read itself can't be trusted — see the case note below.
Core check fires Under (Stage 4); market is up so the overlay lands on Me (Stage 5). Booking window is unchanged from LY, so the Stage 7 veto doesn't fire. ADR index 108 + MPI 76 = priced above comparable booked rates while under-occupied — mechanism decomposition (Stage 8) points to price position, then min-stay. All four signals (core check, MPI, ADR index, booking-window stability) agree, so confidence is High.
"This is mostly you — the market is up +11, you're up only +4, so you've slipped from 85% to 76% of market occupancy. Primary lever: price position; secondary: min-stay."
Core check lands At Par (diff = 0 pp exactly — Stage 4); market direction is down, so the overlay is Market (Stage 5). The Stage 6 RevPAR overlay confirms it: RevPAR is below the 95%-of-STLY floor for the listing, but the market's RevPAR is down too, which is what §5.2 "market-wide decline" checks for — this rules out a listing-side RevPAR problem. MPI barely moved (92→89), so share held. High confidence: core check, RevPAR overlay, and MPI all agree.
"It's the market, not you — October demand is down ~14 pts across the board and your share held. Cutting price won't fix a market-wide dip; hold and re-check." This is the "wait it out" answer, and it's the one that stops a needless price war.
Core check is Under; market is also down, so the overlay lands on Mixed (Stage 5). The decomposition splits diff into the market's own move (−6pp, not your fault) and the self-inflicted remainder (−11pp, the part beyond what the market explains). MPI falling from 102 to 79 — crossing from above-market to meaningfully below — corroborates a real listing-side slide, not noise.
"Mixed — the market softened 6 pts, but you're down 17; roughly 11 of those points are yours. Part wait-it-out, part fix."
Core check lands At Par and the market is up, so Stage 5's overlay exits immediately at Stage 3 — before the RevPAR/veto/mechanism stages even run. MPI rising from 91 to 97 confirms share is improving, not just holding. This is the "no work to do" branch by design — the claim ("I'm slow") is tested and rejected on the numbers alone.
"No slump — you're pacing ahead of last year and holding your share. Nothing to fix here." Roughly a third of real tickets should land here; saying "you're fine" with proof is a feature, not a cop-out.
Core check is Over → base verdict is Winning (Stage 5). But Stage 8's overperformance check reads MPI and ADR index together: MPI 138 (well above 100) paired with ADR index 88 (booked rates below comps) is the specific signature of underpricing, not skill — the caution fires precisely because both numbers point the same direction.
"You're beating a soft market — but MPI 138 with ADR index 88 suggests you're underpriced. Worth testing higher rates before assuming this is pure skill."
The core check alone says Me (diff −9pp, market rising). The Stage 7 veto is what overrides it: booking window is 14 days vs the market's 35 — but it was 13 days LY too (an established pattern, not new), and that LY pattern finished at target occupancy. All three veto conditions are met, so Me downgrades to On Strategy.
"Looks like underperformance, but this matches your deliberate late-booking pattern — identical to last year, which finished full. Monitor, re-check by [date]. Don't cut price." Firing "Me" here would be the agent's most embarrassing mistake; the veto is what prevents it.
Core check again says Me (diff −12pp). But the Stage 7 pickup cross-check catches it: pickup is strong and rising, and the whole market's booking window has compressed vs LY — a market-wide shift to later bookings, not a listing-specific drop-off. That combination routes to Late Not Lost instead of Me.
"On today's snapshot this reads as you, but your pickup is strong and the whole market is booking later this year. This is timing, not a lost-booking problem — hold, re-check in 14 days. If pickup stalls, it flips to Me."
The raw core check reads At Par, but Stage 2's validity gates catch two problems before that number is trusted: comp count is 45, well under the 120 floor, and the comp set is diluted by smaller units against a 6BR listing. Both gates fire, so confidence bottoms out at Low regardless of what diff says — the agent reports the listing's own pace (+5 vs LY) as the only number it will stand behind.
"Signal's too weak to call cleanly — the comp set is only 45 listings and skewed to smaller units, so the market comparison isn't reliable for a 6BR. Best available: you're pacing +5 vs last year. Re-check in 14 days as more October bookings land." Refusing to fake a verdict is the honest move.
Core check says Me (diff −14pp, market flat/up). Realized ADR already sitting below its own STLY would normally point to under-pricing — the naive read is "drop price." But Stage 6's priced-out check looks at the booking-window collapse instead: asking prices ran high enough, early enough, that demand stopped booking; the handful of bookings that did land were the cheap leftover days, which is what's dragging the realized-ADR average down. Verdict stays Me, but the guard changes which lever fires.
"This is you — but the fix is not another price cut. Your rates ran high enough earlier that demand dried up; the low ADR you see is just a couple of discounted days. Review the far-out pricing, don't discount further."
Core check is Over into a flat/down market — by the base mapping alone this is Winning. The Stage 6 RevPAR overlay is what stops it from being reported that way: RevPAR sits below the 95%-of-STLY floor because ADR was cut via aggressive length-of-stay discounts. Occupancy went up, but revenue per available night went down — the "win-that-isn't" guard fires and the verdict is downgraded to a caution rather than a clean Winning.
"Occupancy looks great, but you bought it with rate — RevPAR is down vs last year, so this isn't the win it appears to be. Check your LOS discounting before celebrating the occupancy." This is why RevPAR sits as a guardrail on every verdict, even when occupancy is the chosen KPI.
The same pipeline, fed only different numbers, produces "it's you," "it's the market," "you're fine," "don't cut price," and "that's not really a win" — each with a confidence level and a reason. Three cases (F, G, I) would be called wrong by any tool that reads the occupancy numbers alone — the veto and guards are the difference between a real RM verdict and a naive one. That contrast is the demo.
Verdict states recap: No Slump · Market · Mixed · Me · Winning, with overrides On Strategy / Late Not Lost and abort Inconclusive. Rules compute; LLM narrates; output is customer-safe.