← back to Lifestyle Asset Intel
METHODOLOGY.md
241 lines
# Methodology — `lifestyle-asset-intel`
This document is the **canonical specification** for how the platform turns
source observations into a quoted valuation, liquidity score, and expected
days-to-sell. It is the contract enterprise buyers (lenders, insurers,
resellers, marketplaces) read to understand what they are paying for, and
the contract future ML iterations must respect when they replace v0 stubs.
`methodology_version` is stamped on every `valuation_snapshot` and every
`indices.methodology_version` row so callers can pin a known good version
and audit changes over time. v0 stamps everything `v0-stub`. Versioning
follows semver-style discipline: any rule change that moves a published
quantile by more than 1% MUST bump the version.
## 1. Scope
**In scope (v0):** Hermès Birkin and Kelly handbags in U.S. region. Reference
configurations include Birkin 25/30/35 and Kelly 25/28/32 across Togo, Epsom,
Clemence, Swift in neutral and fashion colors with GHW or PHW, both Sellier
and Retourne constructions.
**Out of scope (v0):** all other handbags, watches, sneakers, jewelry,
non-U.S. regions, sample-only and pre-loved-for-restoration grades. Watches
ship in v2.0 (Chrono24 connector + ChronoPulse parity); sneakers and jewelry
follow.
## 2. Source eligibility
Sources are tiered per BLUEPRINT.md. A source is eligible for valuation iff
it is `enabled=true` in the `sources` table AND its tier-kind pair appears
in the table below. Tier 4 dealer/partner feeds are NOT eligible for
published valuations until they are linked to realized transactions (they
remain eligible as quote/floor signals).
| Tier | Kind | Class | Realized? | v0 weight |
|------|---------------|------------------------------------|-----------|-----------|
| 1 | auction | Sotheby's, Christie's | Yes | 1.000 |
| 2 | marketplace | FASHIONPHILE, The RealReal, StockX | Yes | 0.700–0.800 |
| 2 | quote | Rebag (Clair, Consign, Trade) | Bid only | 0.700 |
| 2 | marketplace | Chrono24 | Yes | 0.650 (watches) |
| 3 | marketplace | eBay (active listings) | Active | 0.350 |
| 4 | dealer | Direct dealer feed | Quote only| disabled |
Quote-class sources land in the system as **instant-liquidity bids**, never
as final settled comps. Active-listing observations are admissible only to
inform liquidity and ask-curve depth — they do NOT enter the price model.
## 3. Comparable selection
`valueAsset(slug)` retrieves transactions where `canonical_asset_id` matches.
v0 does NOT widen across similar configurations even when comp depth is low;
that means a rare config will return a low confidence score rather than a
borrowed-from-cousin estimate. v0.3 introduces image-similarity widening
(see § 7).
Selection rules (v0):
1. Same `canonical_asset_id`
2. `transacted_at` within trailing 24 months
3. `source_id` is enabled
4. `kind` ∈ {auction, marketplace} (NOT quote-only)
5. Maximum 50 comps; ordered by `transacted_at DESC`
## 4. Price normalization
Every observation must derive four price measures before entering the model:
- `gross_transaction_price` — what the buyer paid before any platform-fee
adjustments (auction hammer + buyer's premium; marketplace ask-realized
price as quoted; quote-system offer).
- `all_in_buyer_price` — gross plus duties, taxes, shipping. For v0 we
leave `all_in_buyer_price = gross_transaction_price` (no cross-border
modeling); v0.6 adds the duties/shipping table.
- `expected_net_seller_proceeds` — gross minus the source's seller fee
schedule. v0 uses constant haircuts: Sotheby's hammer-to-net ≈ -18%,
FASHIONPHILE/Rebag/TRR ≈ -22%, StockX ≈ -12%, eBay ≈ -14%. v0.4
replaces these with per-source published schedules.
- `normalized_market_value` — the row's contribution to the unweighted
comp distribution. Today `normalized_market_value = gross_transaction_price`.
Currency is stored as ISO-4217. v0 only ingests USD. Cross-currency comps
are admissible only after FX-conversion at `transacted_at`'s daily WM/Refinitiv
fix; v0.6 delivers the FX table.
## 5. Condition normalization
We store both an **ordinal grade** (0–6) and a **facet-level jsonb**.
| Grade | Label | Translation |
|-------|----------|--------------------------------------------------------------------|
| 6 | Giftable | Store-fresh, plastic intact, full set, recent stamp |
| 5 | Pristine | Like-new, minor handling marks acceptable |
| 4 | Excellent| Light wear; corners and hardware un-scratched |
| 3 | Very Good| Visible wear, no structural issues |
| 2 | Good | Wear + some structural concerns (handle darkening, lining stains) |
| 1 | Fair | Multiple issues; collector-only |
| 0 | As Is | Major repair, restoration, or non-original parts |
Source grade tables (FASHIONPHILE Giftable→Flawed; The RealReal
Pristine→As Is; StockX new-only) map deterministically into the ordinal
above. Mapping table lives in `lib/condition.js` (v0.4 — currently
unimplemented; seed grades come from the ingest agent).
Facets stored in `condition_facets` jsonb: `corner_wear`, `hardware_scratching`,
`handle_darkening`, `odor`, `lining_wear`, `shape_loss`, `repair_history`,
`store_fresh`, `accessories_complete`, `plastic_intact`, `date_stamp_year`.
The model uses both: a comp graded "Very Good with clean interior" prices
differently from "Very Good with odor".
## 6. Quantile interpretation
Every published valuation is a triplet:
- `q10` — the 10th percentile of plausible realizable prices
- `q50` — the median (the headline number)
- `q90` — the 90th percentile
Q10/Q90 are **two-sided 80% prediction intervals**, not min/max bounds. They
represent the model's uncertainty about a sale at `as_of`, conditioned on
the canonical configuration. They are NOT sample percentiles of the comps
directly — for low-depth configs, the band widens beyond the comp range.
In v0, q10/q90 are seeded as `q50 * {0.85, 1.27}` heuristics. v0.5 replaces
with quantile-regression model output.
## 7. Liquidity and expected days-to-sell
`computeLiquidity(stats)` (see `lib/valuation.js`):
```text
liquidity = 0.4 * count_factor
+ 0.4 * recency_factor
+ 0.2 * spread_factor
count_factor = log1p(n_comps) / log1p(20) clamped [0, 1]
recency_factor = 1 - days_to_newest_comp / 365 clamped [0, 1]
spread_factor = 1 - (Q90 - Q10) / avg_gross clamped [0, 1]
```
The 24-month trailing window is hard-coded; configs older than 24 months
score 0.
`computeExpectedDts(liquidity)`:
```text
dts = round(60 - liquidity * 50) clamped [7, 120] days
```
Heuristic, replaced by per-source listing-to-sale gap distribution in v0.6
once we have ask-curve data.
## 8. Compositional confidence
`computeBreakdown(...)` returns a 9-key object whose values sum to a number
in `[-0.30, 1.00]`. The published `latest_snapshot.confidence` is that sum
clamped to `[0, 1]`.
| Component | Range | v0 source |
|------------------------------------|----------------|--------------------------------------------------|
| `source_quality_weight` | 0 to +0.30 | Comps' `sources.weight`-weighted average × 0.30 |
| `comparable_similarity_weight` | 0 to +0.18 | Avg comp source weight × 0.18 (full CLIP cosine arrives in v0.3) |
| `sample_depth_weight` | 0 to +0.20 | log-saturating in n_comps over 20-comp ceiling |
| `recency_weight` | 0 to +0.15 | Linear decay over 365 days from newest comp |
| `image_match_weight` | 0 to +0.09 | Constant 0.09 (no image intake until v0.3) |
| `authenticity_risk_penalty` | -0.15 to 0 | -0.5 × `auth_risk` (clamped) |
| `condition_uncertainty_penalty` | -0.10 to 0 | -0.05 missing grade; -0.02 if any grade < 4 |
| `region_gap_penalty` | -0.10 to 0 | -0.03 if any comp.region != asset.region |
| `fee_model_uncertainty_penalty` | -0.05 to 0 | Constant -0.01 (per-source fee tables in v0.4) |
If any v0-stub component is replaced (e.g., embeddings land, replacing the
constant `comparable_similarity_weight`), `methodology_version` MUST bump.
## 9. Authenticity risk
Risk in v0 is read from `valuation_snapshots.auth_risk` (seeded). Production
v1 splits this into a separate model called by the auth-risk service and
returns one of:
- `auto_clear` — risk < 0.10, valuation publishes
- `holdout_badge` — 0.10 ≤ risk < 0.35, valuation publishes with a UI badge
- `human_review` — risk ≥ 0.35, valuation withheld pending human inspection
The agent orchestration scaffold in BLUEPRINT.md hard-codes the
`escalate_if_authenticity_risk > 0.35` rule — this is non-negotiable.
## 10. Outlier filtering
v0 does NOT filter outliers. Every eligible comp enters the calculation.
This is a deliberate posture — we'd rather show a wide band with low
confidence than silently drop a comp that the model can't explain. v0.5
introduces a documented MAD-based filter with the version bump.
## 11. Region and currency
v0 admits only `region = 'US'` and `currency = 'USD'`. Cross-region comps
are stored but excluded from the comp pool. v0.6 adds:
- FX-converted price normalization at `transacted_at` daily fix
- Region-gap penalty refinement (currently constant -0.03)
- Per-region fee schedules
## 12. Indices
Each row in `indices` defines a configuration cohort. `indices.definition`
is a jsonb that filters canonical_assets:
```json
{ "size": "30", "material": "Togo", "colors": ["Gold","Black","Etoupe","Nata"], "region": "US" }
```
`index_points.value` is the cohort's sales-weighted average `gross` over
the prior 365 days, frozen on `as_of`. v0 hand-seeds points; v1.1 stands up
the daily-recompute publisher. **Rebasing rule:** indices are re-based to
their starting `value` whenever the `definition` changes — old points stay
under the old `methodology_version`.
## 13. What v0 does NOT model
This list exists so enterprise buyers don't assume the platform handles
something it doesn't:
- Active-listing bid-ask spread
- Per-source listing-to-sale time distribution (uses a heuristic)
- Per-region duties / shipping
- FX conversion
- Bulk-portfolio concentration risk
- Insurance replacement value (which is GROSS, not NET)
- Hold-period IRR with ownership cost
- Date-stamp premium curves (treated as flat in v0; the
`recent-stamp-premium` index is the v0 lens)
Each of these has a roadmap entry. Until shipped, the API responses do not
imply coverage.
## 14. Change log
| Version | Date | Change |
|------------|------------|---------------------------------------------------------------------|
| v0-stub | 2026-05-09 | Initial methodology — confidence, liquidity, dts all live-computed. |
| v0-stub.1 | 2026-05-13 | `comparable_similarity_weight` is now source-weight-aware (was constant 0.18). Tier-1 comp pools score the full 0.18; tier-4 pools drop to ~0.04. Full CLIP embeddings still scheduled for v0.3. |