[object Object]

← back to Lifestyle Asset Intel

yolo tick #8: MSRP series + premium-to-retail ratio

19cb967b765864b23e3b888b3d52b657b1e0546f · 2026-05-10 00:11:21 -0700 · Steve Abrams

BLUEPRINT.md flagged MSRP backfill as the central v0 problem for
Hermès — they sell Birkin/Kelly/Constance "exclusively in stores",
so there's no e-commerce feed to harvest. Migration 0005 lays in
msrp_observations with a CHECK-constrained source enum
(boutique_receipt / specialist_tracker / auction_house_commentary /
user_upload / client_pdf_invoice) and a (asset, source, date) unique
key so the same observation can't be double-counted across sources.

Seed: only the Birkin 30 Togo 2022–2026 series from BLUEPRINT.md's
own table, attributed to 'auction_house_commentary' (Sotheby's
report). The other 14 canonical assets stay null until real
observations land — Steve's standing rule against synthesized data
applies here too, so no fabricated MSRPs.

valueAsset() now joins the latest msrp_observation per asset and
computes premium_to_msrp = q50 / msrp_usd to two decimals. Returns
null when no MSRP exists. Live values for seeded B30 Togo:
  q50=22300, msrp_usd=14900 (2026 reading), premium=1.50×
The number is naturally lower than Sotheby's 2024-quoted 2.4× because
2026 MSRP has caught up while 2025 secondary held — exactly the
drift signal METHODOLOGY § 1 calls out.

Asset detail page renders a "Primary-market reference" KPI block
(MSRP $ + observed year + source) with a teal-tinted "Q50 ÷ MSRP"
cell when the ratio is computable, and a contextual blurb pointing
to /methodology when MSRP is missing. CSS adds .msrp-block + the
accent-color premium cell.

37/37 tests green; smoke test now asserts the MSRP fields too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 19cb967b765864b23e3b888b3d52b657b1e0546f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 10 00:11:21 2026 -0700

    yolo tick #8: MSRP series + premium-to-retail ratio
    
    BLUEPRINT.md flagged MSRP backfill as the central v0 problem for
    Hermès — they sell Birkin/Kelly/Constance "exclusively in stores",
    so there's no e-commerce feed to harvest. Migration 0005 lays in
    msrp_observations with a CHECK-constrained source enum
    (boutique_receipt / specialist_tracker / auction_house_commentary /
    user_upload / client_pdf_invoice) and a (asset, source, date) unique
    key so the same observation can't be double-counted across sources.
    
    Seed: only the Birkin 30 Togo 2022–2026 series from BLUEPRINT.md's
    own table, attributed to 'auction_house_commentary' (Sotheby's
    report). The other 14 canonical assets stay null until real
    observations land — Steve's standing rule against synthesized data
    applies here too, so no fabricated MSRPs.
    
    valueAsset() now joins the latest msrp_observation per asset and
    computes premium_to_msrp = q50 / msrp_usd to two decimals. Returns
    null when no MSRP exists. Live values for seeded B30 Togo:
      q50=22300, msrp_usd=14900 (2026 reading), premium=1.50×
    The number is naturally lower than Sotheby's 2024-quoted 2.4× because
    2026 MSRP has caught up while 2025 secondary held — exactly the
    drift signal METHODOLOGY § 1 calls out.
    
    Asset detail page renders a "Primary-market reference" KPI block
    (MSRP $ + observed year + source) with a teal-tinted "Q50 ÷ MSRP"
    cell when the ratio is computable, and a contextual blurb pointing
    to /methodology when MSRP is missing. CSS adds .msrp-block + the
    accent-color premium cell.
    
    37/37 tests green; smoke test now asserts the MSRP fields too.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 db/migrations/0005_msrp.sql | 35 +++++++++++++++++++++++++++++++++++
 db/schema.sql               |  1 +
 db/seed.sql                 | 35 +++++++++++++++++++++++++++++++++++
 lib/valuation.js            | 27 +++++++++++++++++++++++++++
 public/css/app.css          |  4 ++++
 tests/smoke.test.js         |  6 ++++++
 views/asset.ejs             | 33 +++++++++++++++++++++++++++++++++
 7 files changed, 141 insertions(+)

diff --git a/db/migrations/0005_msrp.sql b/db/migrations/0005_msrp.sql
new file mode 100644
index 0000000..7a03a0f
--- /dev/null
+++ b/db/migrations/0005_msrp.sql
@@ -0,0 +1,35 @@
+-- 0005_msrp.sql — primary-market MSRP history for canonical assets.
+--
+-- BLUEPRINT.md flags MSRP as a critical v0 problem: Hermès sells the
+-- Birkin/Kelly/Constance "exclusively in stores" (per their own site),
+-- so there's no normal e-commerce feed to harvest. MSRPs therefore
+-- come from boutique receipts, specialist price trackers, client
+-- uploads, and high-trust market commentary (auction houses).
+--
+-- One observation per (asset, source, observed_at). The valuation
+-- engine reads the most recent observation per asset to compute the
+-- premium-to-retail ratio (Sotheby's-quoted "2.4x retail" headline).
+
+CREATE TABLE IF NOT EXISTS msrp_observations (
+  id                  uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+  canonical_asset_id  uuid NOT NULL REFERENCES canonical_assets(id) ON DELETE CASCADE,
+  msrp_usd            numeric(12,2) NOT NULL,
+  currency            text NOT NULL DEFAULT 'USD',
+  observed_at         date NOT NULL,
+  source              text NOT NULL CHECK (source IN (
+                        'boutique_receipt',
+                        'specialist_tracker',
+                        'auction_house_commentary',
+                        'user_upload',
+                        'client_pdf_invoice'
+                      )),
+  source_note         text,
+  created_at          timestamptz NOT NULL DEFAULT now(),
+  UNIQUE (canonical_asset_id, source, observed_at)
+);
+
+CREATE INDEX IF NOT EXISTS msrp_observations_asset_observed_idx
+  ON msrp_observations(canonical_asset_id, observed_at DESC);
+
+INSERT INTO schema_migrations(filename) VALUES ('0005_msrp.sql')
+  ON CONFLICT (filename) DO NOTHING;
diff --git a/db/schema.sql b/db/schema.sql
index 94af7ca..864759e 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -3,3 +3,4 @@
 \i db/migrations/0002_transactions_unique.sql
 \i db/migrations/0003_image_intake.sql
 \i db/migrations/0004_valuation_calls.sql
+\i db/migrations/0005_msrp.sql
diff --git a/db/seed.sql b/db/seed.sql
index 3411ab0..ef7f522 100644
--- a/db/seed.sql
+++ b/db/seed.sql
@@ -751,3 +751,38 @@ ON CONFLICT (index_id, as_of) DO NOTHING;
 INSERT INTO index_points (index_id, as_of, value, change_pct)
 SELECT i.id, '2025-12-31'::date, 1.168, 0.0228 FROM indices i WHERE i.slug = 'recent-stamp-premium'
 ON CONFLICT (index_id, as_of) DO NOTHING;
+
+-- ---------------------------------------------------------------------------
+-- v0 yolo tick #8: MSRP history (only what BLUEPRINT.md authoritatively
+-- gives us — Birkin 30 Togo 2022-2026). Other assets stay null until
+-- boutique receipts or specialist trackers feed in. No synthesized data.
+-- ---------------------------------------------------------------------------
+INSERT INTO msrp_observations (canonical_asset_id, msrp_usd, currency, observed_at, source, source_note)
+SELECT ca.id, 11300.00, 'USD', '2022-01-01'::date, 'auction_house_commentary',
+       'BLUEPRINT.md table — Sotheby''s reference series, US Birkin 30 Togo'
+  FROM canonical_assets ca WHERE ca.slug = 'birkin-30-togo-gold-ghw-us'
+ON CONFLICT (canonical_asset_id, source, observed_at) DO NOTHING;
+
+INSERT INTO msrp_observations (canonical_asset_id, msrp_usd, currency, observed_at, source, source_note)
+SELECT ca.id, 11600.00, 'USD', '2023-01-01'::date, 'auction_house_commentary',
+       'BLUEPRINT.md table — Sotheby''s reference series, US Birkin 30 Togo'
+  FROM canonical_assets ca WHERE ca.slug = 'birkin-30-togo-gold-ghw-us'
+ON CONFLICT (canonical_asset_id, source, observed_at) DO NOTHING;
+
+INSERT INTO msrp_observations (canonical_asset_id, msrp_usd, currency, observed_at, source, source_note)
+SELECT ca.id, 12500.00, 'USD', '2024-01-01'::date, 'auction_house_commentary',
+       'BLUEPRINT.md table — Sotheby''s reference series, US Birkin 30 Togo'
+  FROM canonical_assets ca WHERE ca.slug = 'birkin-30-togo-gold-ghw-us'
+ON CONFLICT (canonical_asset_id, source, observed_at) DO NOTHING;
+
+INSERT INTO msrp_observations (canonical_asset_id, msrp_usd, currency, observed_at, source, source_note)
+SELECT ca.id, 13900.00, 'USD', '2025-01-01'::date, 'auction_house_commentary',
+       'BLUEPRINT.md table — Sotheby''s reference series, US Birkin 30 Togo'
+  FROM canonical_assets ca WHERE ca.slug = 'birkin-30-togo-gold-ghw-us'
+ON CONFLICT (canonical_asset_id, source, observed_at) DO NOTHING;
+
+INSERT INTO msrp_observations (canonical_asset_id, msrp_usd, currency, observed_at, source, source_note)
+SELECT ca.id, 14900.00, 'USD', '2026-01-01'::date, 'auction_house_commentary',
+       'BLUEPRINT.md table — Sotheby''s reference series, US Birkin 30 Togo'
+  FROM canonical_assets ca WHERE ca.slug = 'birkin-30-togo-gold-ghw-us'
+ON CONFLICT (canonical_asset_id, source, observed_at) DO NOTHING;
diff --git a/lib/valuation.js b/lib/valuation.js
index 48d2848..1908a63 100644
--- a/lib/valuation.js
+++ b/lib/valuation.js
@@ -46,6 +46,18 @@ async function valueAsset(slug) {
     [asset.id]
   );
 
+  // Latest MSRP observation — Hermès doesn't sell online so MSRP is
+  // backfilled from boutique receipts / specialist trackers / auction
+  // commentary. Returns null if we have no authoritative data point.
+  const msrp = await one(
+    `SELECT msrp_usd, currency, observed_at, source
+       FROM msrp_observations
+      WHERE canonical_asset_id = $1
+      ORDER BY observed_at DESC
+      LIMIT 1`,
+    [asset.id]
+  );
+
   // Build a tier→weight lookup from the joined comps; falls back to constants.
   const sourceWeights = {};
   for (const c of comps) {
@@ -104,6 +116,14 @@ async function valueAsset(slug) {
   });
   const expected_dts = computeExpectedDts(liquidity_score);
 
+  // Premium-to-retail ratio — Sotheby's-style "2.4x retail" headline.
+  // Q50 / latest MSRP, rounded to 2 dp. Null if we have no MSRP.
+  const msrp_usd = msrp ? numberOrNull(msrp.msrp_usd) : null;
+  const q50 = snapshot ? numberOrNull(snapshot.q50) : null;
+  const premium_to_msrp = (msrp_usd != null && msrp_usd > 0 && q50 != null)
+    ? +(q50 / msrp_usd).toFixed(2)
+    : null;
+
   return {
     asset: {
       slug: asset.slug,
@@ -131,6 +151,13 @@ async function valueAsset(slug) {
       comp_count: snapshot.comp_count,
       methodology_version: snapshot.methodology_version
     } : null,
+    msrp: msrp ? {
+      msrp_usd,
+      currency: msrp.currency,
+      observed_at: msrp.observed_at,
+      source: msrp.source
+    } : null,
+    premium_to_msrp,
     comps: comps.map((c) => ({
       source: c.source_slug,
       source_tier: c.source_tier,
diff --git a/public/css/app.css b/public/css/app.css
index f8ba845..6d9c39b 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -173,6 +173,10 @@ tfoot tr.totals th, tfoot tr.totals td {
   font-weight: 600; color: var(--fg);
 }
 
+/* MSRP / premium-to-retail block (v0.2) */
+.msrp-block .premium-cell { background: color-mix(in srgb, var(--accent) 14%, var(--card)); border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); }
+.msrp-block .premium-val { color: var(--accent); font-size: 1.05em; }
+
 /* methodology long-form (v0.2) */
 .methodology-page { max-width: 880px; }
 .methodology h1 { font-size: 1.7rem; margin: 0.4rem 0 1rem; }
diff --git a/tests/smoke.test.js b/tests/smoke.test.js
index 178e9fd..4361213 100644
--- a/tests/smoke.test.js
+++ b/tests/smoke.test.js
@@ -86,6 +86,12 @@ test('GET /api/valuation/birkin-30-togo-gold-ghw-us returns blueprint contract s
   assert.ok(r.json.confidence_breakdown);
   assert.ok('source_quality_weight' in r.json.confidence_breakdown);
   assert.ok('authenticity_risk_penalty' in r.json.confidence_breakdown);
+  // MSRP block — the seeded Birkin 30 Togo gets BLUEPRINT.md MSRP series.
+  assert.ok(r.json.msrp, 'msrp present for seeded B30 Togo');
+  assert.ok(r.json.msrp.msrp_usd > 0);
+  assert.equal(r.json.msrp.source, 'auction_house_commentary');
+  assert.ok(typeof r.json.premium_to_msrp === 'number');
+  assert.ok(r.json.premium_to_msrp > 1.0, 'premium > 1x retail');
 });
 
 test('GET /api/valuation/<unknown-slug> returns 404 with shape', async () => {
diff --git a/views/asset.ejs b/views/asset.ejs
index eb3873e..08d3f48 100644
--- a/views/asset.ejs
+++ b/views/asset.ejs
@@ -25,6 +25,39 @@
       </ul>
     </section>
 
+    <% if (data.msrp || data.premium_to_msrp != null) { %>
+      <section class="msrp-block">
+        <h2>Primary-market reference</h2>
+        <ul class="kpis">
+          <% if (data.msrp) { %>
+            <li>
+              <span class="lbl">MSRP (<%= fmtDate(data.msrp.observed_at).slice(0,4) %>)</span>
+              <span class="val">$<%= money(data.msrp.msrp_usd) %></span>
+            </li>
+            <li>
+              <span class="lbl">MSRP source</span>
+              <span class="val mono"><%= (data.msrp.source || '').replace(/_/g,' ') %></span>
+            </li>
+          <% } else { %>
+            <li><span class="lbl">MSRP</span><span class="val muted">— no observation —</span></li>
+          <% } %>
+          <% if (data.premium_to_msrp != null) { %>
+            <li class="premium-cell">
+              <span class="lbl">Q50 ÷ MSRP</span>
+              <span class="val premium-val"><%= data.premium_to_msrp.toFixed(2) %>×</span>
+            </li>
+          <% } %>
+        </ul>
+        <% if (!data.msrp) { %>
+          <p class="muted">
+            Hermès sells the Birkin/Kelly/Constance exclusively in stores —
+            MSRP backfill comes from boutique receipts, specialist trackers, or
+            client uploads. See <a href="/methodology">METHODOLOGY § 1</a>.
+          </p>
+        <% } %>
+      </section>
+    <% } %>
+
     <% if (data.confidence_breakdown && Object.keys(data.confidence_breakdown).length) { %>
       <section class="breakdown">
         <h2>Confidence breakdown</h2>

← 44cae3a yolo tick #7: immutable valuation_calls audit log + middlewa  ·  back to Lifestyle Asset Intel  ·  yolo tick #9: OpenAPI 3.1 spec at /api/openapi.json 8639f38 →