sdm-market-signalsrun.md00_intake/stub.mdThe market signals section (marketSignals in apps/web/lib/mock/sdm.ts,
rendered by market-signals-section.tsx)
reports bid-price economics: avgBidPrice, bidRange, the low/market/high distribution (pctLow,
pctMarket, pctHigh), and a per-band performance table (byBand: winRate, revenue, csat,
sla, repeatRate). None of this is computed — bids/quotes have prices and win/loss outcomes, but
nothing buckets them into price bands or correlates each band with delivery outcomes.
avgBidPrice, bidRange
(min–max), and the share of bids in the low / market / high bands (band thresholds relative to the
market average).winRate (won ÷ submitted), revenue, and
the delivery outcomes csat / sla / repeatRate — the last three reused from
expert-quality-metrics + sla-stage-targets, not recomputed.avgBidPrice, bidRange and the low/market/high distribution are computed over the area's
quotes/proposals.lib/mock/sdm.touches: packages/services/src/server (bid-price analytics over quotes/proposals + reused metrics),
apps/web/components/dashboard/sdm/market-signals-section.tsx.01_define/output/spec.mdThe market signals section of the web SDM "Go live readiness" dashboard
(apps/web/components/dashboard/sdm/market-signals-section.tsx, rendered by
apps/web/app/(app)/sdm/dashboard/page.tsx) reports bid-price economics — avgBidPrice, bidRange,
the low/market/high distribution (pctLow, pctMarket, pctHigh), and a per-band performance table
(byBand: winRate, revenue, csat, sla, repeatRate) — entirely from apps/web/lib/mock/sdm.ts.
None of it is computed: bids carry prices (proposal.price) and win/loss outcomes (an accepted
quote), but nothing buckets them into price bands or correlates each band with delivery outcomes. This
is sequence 7 of 8 in the sdm-dashboard-data epic — an independent analytics rollup that hangs off the
area foundation (sequence 2). It advances Build the Bridge / Q2 2026 Objective 3 — Validate Technical
Infrastructure & Payout Flow by turning the last-but-one mock section into real, per-tenant,
per-SDM-area data, proving the dashboard's pricing-insight layer resolves end to end.
Add a read-only sdmMarketSignalsService in @sustentus/services/server (new
db/services/sdm-market-signals/, following the established index.ts class + instance.ts singleton
pattern, exported through db/services/index.ts) that returns one typed market-signals envelope for the
signed-in SDM's area, taking the same scope: "mine" | "tenant" the rest of the epic uses. It consumes
the area envelope's in-scope leads (sdmAreaService) — it does not re-resolve the area — and the
shared expert quality + SLA models. Then wire the section to it.
Bid set. A bid is a proposal raised against one of the area's in-scope leads; its bid price
is proposal.price. The bid set is every proposal on the area's leads (scope controls which leads
are in the area). A bid is submitted by existing; it is won when its lead's quote was
accepted (quote.acceptedAt set / quote.status accepted — equivalently the lead advanced to
work_in_progress / delivered / completed), and lost otherwise (qualified_out / quote
rejected). The lost-vs-still-open distinction does not affect win rate (won ÷ submitted); only the
numerator and denominator matter.
Bid-price distribution. Over the bid set compute avgBidPrice (mean proposal.price), bidRange
(min–max), and the share of bids in the low / market / high bands (pctLow, pctMarket,
pctHigh, summing to 100).
Band thresholds — a ±margin window around the area average, centralised. A bid is Low when its
price is below avg × (1 − margin), High when above avg × (1 + margin), else Market. The
single margin (default 0.15) is registered in the tenant-setting registry under a
marketSignals category (marketSignals.bandMarginPct) and read via a
tenantSettingService.getMarketSignalsThresholds(tenantId) accessor, mirroring the existing
coverage.* thresholds + getCoverageThresholds. This keeps the band definition in one place so this
section and any future pricing logic agree on what "low/market/high" means.
Per-band performance rollup (byBand). For each band (Low, Market, High):
winRate = won ÷ submitted bids in that band (percent, 0 when the band is empty).revenue = the realised value of the band's won bids — summed from their accepted quote
total (quote.total ?? amount + serviceFee), formatted in the dashboard's currency convention.csat, sla, repeatRate are reused, not recomputed, from the expert epic's models, aggregated
across the experts who authored the band's bids (and their leads):csat and repeatRate from getExpertQualityMetrics (the matching service) — the same source
sdm-supply-coverage already consumes.sla (per-band SLA adherence) from deriveLeadSlaTimingForLeads (the sla service), over the
band's won leads — the same timing layer the coverage + expert-strength features use.Wire the section. apps/web/app/(app)/sdm/dashboard/page.tsx passes the resolved
marketSignals envelope (driven by the page's existing "my area" vs "all" scope toggle) to
MarketSignalsSection, replacing its lib/mock/sdm source. The component's existing props shape
(avgBidPrice, bidRange, pctLow/Market/High, byBand[]) is preserved so the markup is unchanged;
only the data source moves. Render the section's defined empty state when the area has no bids
(no avg, zeroed distribution, empty byBand) — no crash, no mock fallback. Remove the marketSignals
block from apps/web/lib/mock/sdm.ts.
sdmMarketSignalsService returns a typed market-signals envelope (avgBidPrice, bidRange,
pctLow/pctMarket/pctHigh, byBand[]) from @sustentus/services/server, computed over the
area's in-scope bids (proposal.price on the area's leads), tenant-scoped, taking
scope: "mine" | "tenant" and reusing sdmAreaService for the in-scope leads.avgBidPrice, bidRange and the low/market/high distribution (summing to 100) are computed from
real proposal.price data; bids are bucketed by the ±margin window around the area average.marketSignals.bandMarginPct in the
tenant-setting registry via getMarketSignalsThresholds — defaulting to 0.15, not hard-coded in
the analytics code.winRate (won ÷ submitted, won = accepted quote) and revenue (sum of the band's
won bids' quote total) are computed from real bid/quote data.csat, sla and repeatRate are reused from the expert epic's models
(getExpertQualityMetrics for CSAT/repeat, deriveLeadSlaTimingForLeads for SLA), not recomputed
in this feature.MarketSignalsSection's props shape is unchanged and the
marketSignals block is removed from apps/web/lib/mock/sdm.ts.sdm-service-performance) — a different
lens (delivered revenue/margin by region/product/expert-type), shipped separately.matching/expert epic) and SLA timing (sla) are
consumed as-is; this feature does not fork or recompute them.marketSignals.bandMarginPct — the threshold is registry-defaulted and
resolved per tenant; back-office editing is a later concern (the registry seam is established here).apps/dashboards — that app stays the dummy source-of-truth.02_build/output/notes.mdfeat: sdm-market-signals — bid-price analytics service + wire sectionpackages/services/src/db/services/sdm-market-signals/{index.ts,instance.ts}: new read-only
sdmMarketSignalsService.getMarketSignalsForSdm(tenantId, sdmUserId, scope). Resolves the SDM's
area (same region/platform membership rule as sdmAreaService/sdmDemandService), fetches the
in-scope leads' bids (proposal.price) and their quotes, then computes the bid-price distribution
(avgBidPrice, bidRange, pctLow/pctMarket/pctHigh) and the per-band performance rollup
(byBand). Bands are a ±margin window around the area average; win rate = won ÷ submitted (won = a
proposal whose quote was accepted); revenue = the band's won quotes' totals. CSAT + repeat rate are
reused from getExpertQualityMetrics (matching) and SLA adherence from deriveLeadSlaTimingForLeads
(sla) — none recomputed here.packages/services/src/db/services/tenant-setting/{registry.ts,index.ts}: added the
marketSignals.bandMarginPct registry entry (default 0.15, marketSignals category) and the
getMarketSignalsThresholds(tenantId) accessor + MarketSignalsThresholds type — mirroring the
coverage.* thresholds, so the band margin lives in one tunable place.packages/services/src/db/services/index.ts: export sdmMarketSignalsService + its types, after
its dependency services (tenant-setting, matching, sla) per the established load-order guard.apps/web/app/(app)/sdm/dashboard/page.tsx: resolve marketSignals in the same Promise.all as
coverage/risk (degrading to an empty envelope on failure) and pass it to MarketSignalsSection,
replacing d.marketSignals. Driven by the page's existing mine vs tenant scope toggle.apps/web/lib/mock/sdm.ts: removed the marketSignals block (now real data).sdmMarketSignalsService returns the typed envelope from @sustentus/services/server, computed
over the area's in-scope bids, tenant-scoped, taking scope: "mine" | "tenant", reusing the
area's in-scope leads — same area-membership rule as sdmAreaService.avgBidPrice, bidRange and the low/market/high distribution (forced to sum to 100) are computed
from real proposal.price; bids bucketed by the ±margin window around the area average.marketSignals.bandMarginPct (registry default 0.15) via
getMarketSignalsThresholds — not hard-coded in the analytics code.winRate (won ÷ submitted, won = accepted quote) and revenue (won quotes' totals) are
computed from real bid/quote data.csat, sla, repeatRate reuse getExpertQualityMetrics (CSAT/repeat) and
deriveLeadSlaTimingForLeads (SLA) — not recomputed.MarketSignalsSection's props shape is unchanged and the marketSignals mock
block is removed.acceptedAt set, or status: "accepted") references that proposal (the quote→proposal link
is unique, so at most one quote per bid). Empty/zero-bid bands are omitted from byBand rather than
shown as all-zero rows; the distribution percentages still convey the split.red
(same not-breached definition sdm-supply-coverage uses); 0 when the band has no won leads.03_release/output/changelog.mdLive entry: apps/help/app/changelog/2026-06-24-sdm-market-signals/page.mdx
Your go live readiness dashboard's market signals section now reports real bid-price data for your area, not placeholder numbers:
03_release/output/investor-update.mdWho it's for: Service delivery managers What shipped: The go live readiness dashboard's market signals section now computes real bid-price distribution and per-band win rate, revenue, CSAT, SLA and repeat rate from live proposals and quotes. Why it matters: Advances Build the bridge, Q2 objective 3 — validate technical infrastructure & payout flow — by turning the SDM's pricing-insight layer into trustworthy data.
Price bands are tunable from one centralised setting; quality and SLA figures reuse the platform's expert models.
Dig deeper: <merged-PR URL> · <changelog entry URL>
03_release/output/release.mdtechnical/packages/services is high-level (no per-service list); the new sdm-market-signals service + marketSignals.bandMarginPct setting follow the established sdm-* / tenant-setting-registry patternsbusiness/** edit)apps/help/app/changelog/2026-06-24-sdm-market-signals/page.mdx; investor draft in 03_release/output/investor-update.md (Dig deeper links filled after merge)/code-review medium over main...HEAD (scope: this PR's changes — the sdm-strength,
matching, and sdm-coverage diffs in the range came from main and are already reviewed/merged).
No correctness findings:
total ?? amount + serviceFee); won = accepted quote referencing
the proposal (quote↔proposal is unique, so at most one per bid) — no double count.byBand rather than shown as misleading all-zero rows.getExpertQualityMetrics + deriveLeadSlaTimingForLeads (reused, not
recomputed); each looked up once per unique expert/lead across bands.scope:"mine" with no area returns the empty envelope (no widen-to-tenant).emptyBucket/average helpers declared after the class — referenced only at call
time, no TDZ; consistent with module-scope helper style. Math.min/max(...prices) spread is bounded
by an area's proposal count.sdmMarketSignalsService returns the typed envelope from @sustentus/services/server, computed
over the area's in-scope bids, tenant-scoped, scope: "mine" | "tenant", reusing sdmAreaService.avgBidPrice/bidRange/distribution (forced to sum to 100) from real proposal.price; bids
bucketed by the ±margin window around the area average.marketSignals.bandMarginPct (registry default 0.15) via
getMarketSignalsThresholds — not hard-coded.winRate (won ÷ submitted, won = accepted quote) + revenue (won quotes' totals) from
real bid/quote data.csat/sla/repeatRate reuse getExpertQualityMetrics + deriveLeadSlaTimingForLeads.marketSignals mock block removed.