← 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. |