SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%

Ask vs RIV: −50 % review threshold (§174) — deeper discounts are flagged riv_review and excluded from deal rails, screener preset, Deal Radar, digest and Rare Radar; 'Needs review' label

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 13 days ago (Sep 11, 2026) parent 17ff07f

13 changed files +46 −17

modified apps/web/src/app/methodology/page.tsx +2 −1
@@ -1,7 +1,7 @@
1 1 import type { Metadata } from 'next';
2 2 import Link from 'next/link';
3 3 import type { ReactNode } from 'react';
4 −import { ASK_ANOMALY_HIGH_RATIO, ASK_ANOMALY_LOW_RATIO, ASK_MIN_CONFIDENCE, ASK_MIN_MATCH_CONFIDENCE, ASK_MIN_SAMPLE, DEAL_THRESHOLD, MAX_PLAUSIBLE_CHANGE, PREMIUM_THRESHOLD } from '@rareindex/valuation';
4 +import { ASK_ANOMALY_HIGH_RATIO, ASK_ANOMALY_LOW_RATIO, ASK_MIN_CONFIDENCE, ASK_MIN_MATCH_CONFIDENCE, ASK_MIN_SAMPLE, DEAL_REVIEW_THRESHOLD, DEAL_THRESHOLD, MAX_PLAUSIBLE_CHANGE, PREMIUM_THRESHOLD } from '@rareindex/valuation';
5 5 import { PageHeader } from '@/components/ui/page-header';
6 6 import { Badge, Card } from '@/components/ui/primitives';
7 7 import { cn } from '@/lib/format';
@@ -199,6 +199,7 @@ export default function MethodologyPage() {
199 199 ['Deal', `discount ≤ ${pct(DEAL_THRESHOLD)} (ask at least ${pct(-DEAL_THRESHOLD)} below RIV)`],
200 200 ['Fair', `between ${pct(DEAL_THRESHOLD)} and ${pct(PREMIUM_THRESHOLD)}`],
201 201 ['Premium', `discount ≥ ${pct(PREMIUM_THRESHOLD)}`],
202 + ['Needs review', `more than ${Math.round(-DEAL_REVIEW_THRESHOLD * 100)} % below RIV — kept as a number, flagged riv_review, excluded from every deal rail, Deal Radar, digest and Rare Radar until reviewed (§174)`],
202 203 ['Data/identity anomaly', `ratio outside ${ASK_ANOMALY_LOW_RATIO}×–${ASK_ANOMALY_HIGH_RATIO}× — wrong variant, lot, currency, typo or mismatch until reviewed`],
203 204 ['Not compared', 'one of the gates failed; the reason is shown (e.g. riv_sample, riv_basis_guide)'],
204 205 ]}
modified apps/web/src/components/ui/primitives.tsx +7 −0
@@ -53,6 +53,13 @@ export function VsRiv({ discount, className, digits = 1, showLabel = true }: { d
53 53 </span>
54 54 );
55 55 }
56 + if (discount < -0.5) {
57 + return (
58 + <span className={cn('inline-flex items-center gap-1 rounded-sm bg-alert-bg px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-alert', className)} title={`Asking price ${fmtPct(Math.abs(discount), digits, false)} below RIV — more than 50 % below a valuation is almost always a different item, grade or lot. Held for review, not a deal.`}>
59 + Needs review · {fmtPct(discount, 0)}
60 + </span>
61 + );
62 + }
56 63 const tone = discount <= -0.1 ? 'text-gain' : discount >= 0.1 ? 'text-loss' : 'text-flat';
57 64 const word = discount <= -0.1 ? 'below RIV' : discount >= 0.1 ? 'above RIV' : 'near RIV';
58 65 return (
modified apps/web/src/lib/account/queries.ts +1 −1
@@ -236,7 +236,7 @@ export async function dealRadar(userId: string, opts: { minDiscount?: number; li
236 236 join assets a on a.id = l.asset_id
237 237 join asset_stats s on s.asset_id = a.id
238 238 where l.availability = 'available'
239 − and l.discount_to_riv is not null and l.discount_to_riv <= ${-minDiscount}
239 + and l.discount_to_riv is not null and l.discount_to_riv <= ${-minDiscount} and l.discount_to_riv >= -0.5 and not ('riv_review' = any(l.flags))
240 240 and s.riv_confidence >= 0.5 and s.riv_sample_size >= 5
241 241 and (a.category_slug in (select category_slug from universe) or a.id in (select asset_id from watched))
242 242 order by watched desc, l.discount_to_riv asc
modified apps/web/src/lib/ai/scanner.ts +2 −1
@@ -113,7 +113,8 @@ export async function runScan(input: ScanInput): Promise<ScanResult> {
113 113 const gradedMismatch = Boolean(listing.grader && listing.grader !== 'raw' && best.context.variants.length && !best.context.variants.some((v) => (v.grader as string | null) === listing.grader && (v.grade as string | null) === listing.grade));
114 114 const a = assessAsk({ askUsd: fx?.usd ?? null, rivUsd: best.rivUsd, confidence: best.rivConfidence, sampleSize: best.rivSampleSize, sameVariant: gradedMismatch ? false : undefined });
115 115 listing.discountToRiv = a.discount;
116 − listing.verdict = a.verdict === 'deal' ? 'below_fair_value' : a.verdict === 'premium' ? 'above_fair_value' : a.verdict === 'fair' ? 'in_range' : a.verdict === 'anomaly' ? 'anomaly' : 'unknown';
116 + listing.verdict = a.verdict === 'deal' ? 'below_fair_value' : a.verdict === 'premium' ? 'above_fair_value' : a.verdict === 'fair' ? 'in_range' : a.verdict === 'anomaly' || a.verdict === 'review' ? 'anomaly' : 'unknown';
117 + if (a.verdict === 'review') notes.push('The ask is more than 50 % below the matched valuation: almost always a different item, grade or lot. Held for review, not a deal.');
117 118 if (a.verdict === 'anomaly') notes.push('The asking price is implausible against the matched valuation (below 10 % or above 10× RIV): most likely a different variant, a lot, a currency or an identity mismatch — review before treating it as a deal.');
118 119 else if (a.verdict === 'ungated') notes.push(`The ask was not compared with the valuation (${a.reasons.join(', ')}).`);
119 120 }
modified apps/web/src/lib/queries/assets.ts +3 −3
@@ -204,9 +204,9 @@ export async function rankedAssets(kind: RankedKind, opts: { scope?: string[]; l
204 204 watched: { where: sql`s.watchers > 0`, order: sql`s.watchers DESC, s.views_30d DESC` },
205 205 newest: { where: sql`true`, order: sql`a.created_at DESC` },
206 206 expensive: { where: sql`s.riv_usd IS NOT NULL`, order: sql`s.riv_usd DESC` },
207 − // value_opportunity = (best gated ask − RIV) / RIV: negative = below the valuation. A deal is ≤ −10 %; the
208 − // worker never stores implausible ratios, the ≥ −0.9 bound is a second safety net (§84, §196).
209 − opportunity: { where: sql`s.value_opportunity IS NOT NULL AND s.value_opportunity <= -0.1 AND s.value_opportunity >= -0.9 AND s.riv_sample_size >= 5 AND s.riv_confidence >= 0.5`, order: sql`s.value_opportunity ASC` },
207 + // value_opportunity = (best gated ask − RIV) / RIV: negative = below the valuation. A deal is between −10 % and
208 + // −50 %; deeper discounts are review cases (§174), implausible ratios never reach the table (§84, §196).
209 + opportunity: { where: sql`s.value_opportunity IS NOT NULL AND s.value_opportunity <= -0.1 AND s.value_opportunity >= -0.5 AND s.riv_sample_size >= 5 AND s.riv_confidence >= 0.5`, order: sql`s.value_opportunity ASC` },
210 210 liquid: { where: sql`s.liquidity_score IS NOT NULL`, order: sql`s.liquidity_score DESC` },
211 211 // Fallback rails used while valuations are still being computed (§192: honest, data-backed)
212 212 newest_priced: { where: sql`s.riv_usd IS NOT NULL`, order: sql`s.updated_at DESC` },
modified apps/web/src/lib/queries/market-lists.ts +2 −1
@@ -97,7 +97,8 @@ export async function listListings(f: ListingsFilters): Promise<{ items: Listing
97 97 if (f.grader) where.push(sql`l.grader = ${f.grader}`);
98 98 if (f.minUsd != null) where.push(sql`l.price_usd >= ${f.minUsd}`);
99 99 if (f.maxUsd != null) where.push(sql`l.price_usd <= ${f.maxUsd}`);
100 − if (f.minDiscount != null) where.push(sql`l.discount_to_riv <= ${-Math.abs(f.minDiscount)}`); // stored negative = below RIV
100 + if (f.minDiscount != null) where.push(sql`l.discount_to_riv <= ${-Math.abs(f.minDiscount)} AND NOT ('riv_review' = ANY(l.flags))`); // stored negative = below RIV; review cases excluded
101 + if (f.sort === 'discount') where.push(sql`NOT ('riv_review' = ANY(l.flags))`);
101 102 if (f.listingType) where.push(sql`l.listing_type = ${f.listingType}`);
102 103 if (f.q && f.q.trim().length >= 2) where.push(sql`(a.title ILIKE ${'%' + f.q.trim() + '%'} OR l.raw_title ILIKE ${'%' + f.q.trim() + '%'})`);
103 104 const orders: Record<NonNullable<ListingsFilters['sort']>, SQL> = {
modified apps/web/src/lib/queries/screener.ts +1 −1
@@ -108,7 +108,7 @@ function whereFor(f: ScreenerFilters): SQL {
108 108 w.push(sql`(title ILIKE ${'%' + q + '%'} OR title % ${q} OR set_name ILIKE ${'%' + q + '%'})`);
109 109 }
110 110 // the "Potentially Underpriced" sort/preset must never surface an ungated or implausible discount
111 − if (f.sort === 'opportunity') w.push(sql`value_opportunity IS NOT NULL AND value_opportunity >= -0.9 AND value_opportunity <= -0.1 AND riv_sample_size >= 5 AND riv_confidence >= 0.5`);
111 + if (f.sort === 'opportunity') w.push(sql`value_opportunity IS NOT NULL AND value_opportunity >= -0.5 AND value_opportunity <= -0.1 AND riv_sample_size >= 5 AND riv_confidence >= 0.5`);
112 112 return joinAnd(w);
113 113 }
114 114
modified apps/web/src/lib/screener.ts +1 −1
@@ -149,7 +149,7 @@ export const PRESETS: ScreenerPreset[] = [
149 149 { id: 'drawdown', label: 'Largest Drawdowns', description: 'RIV furthest below the all-time-high verified sale (≥ 5 sales, medium+ confidence).', filters: { sort: 'drawdown', dir: 'asc', drawdownMax: -0.3, confidenceMin: 0.5 } },
150 150 { id: 'ath', label: 'Near ATH', description: 'RIV within 5 % of the all-time-high verified sale.', filters: { sort: 'riv', dir: 'desc', drawdownMin: -0.05, confidenceMin: 0.5 } },
151 151 { id: 'volume', label: 'Increasing Volume', description: 'Sales in the last 30 days at least 2× the trailing 12-month monthly average.', filters: { sort: 'sales30d', dir: 'desc', volumeAccelMin: 2, sales1yMin: 6 } },
152 − { id: 'underpriced', label: 'Potentially Underpriced', description: 'Best gated ask 10–90 % below a transaction-based RIV (≥ 5 sales, medium+ confidence). Analytical data, not advice.', filters: { sort: 'opportunity', dir: 'asc', spreadMax: -0.1, confidenceMin: 0.5 } },
152 + { id: 'underpriced', label: 'Potentially Underpriced', description: 'Best gated ask 10–50 % below a transaction-based RIV (≥ 5 sales, medium+ confidence). Analytical data, not advice.', filters: { sort: 'opportunity', dir: 'asc', spreadMax: -0.1, confidenceMin: 0.5 } },
153 153 ];
154 154
155 155 export function isPlausibleChange(v: number | null | undefined): v is number {
modified packages/valuation/src/scores.ts +10 −3
@@ -55,8 +55,10 @@ export const ASK_MIN_MATCH_CONFIDENCE = 0.7;
55 55 /** Threshold below/above which an ask is called a deal / a premium. */
56 56 export const DEAL_THRESHOLD = -0.1;
57 57 export const PREMIUM_THRESHOLD = 0.1;
58 +/** An ask more than 50 % below RIV is almost always a mismatch (§174): kept as a number, surfaced as "needs review", never as a deal. */
59 +export const DEAL_REVIEW_THRESHOLD = -0.5;
58 60
59 −export type AskVerdict = 'deal' | 'fair' | 'premium' | 'anomaly' | 'ungated';
61 +export type AskVerdict = 'deal' | 'fair' | 'premium' | 'review' | 'anomaly' | 'ungated';
60 62
61 63 export interface AskAssessment {
62 64 /** (ask − RIV) / RIV, 4 decimals; null when ungated or anomalous */
@@ -102,6 +104,7 @@ export function assessAsk(i: AskAssessmentInput): AskAssessment {
102 104 return { discount: null, ratio: round(ratio, 4), verdict: 'anomaly', reasons: [ratio < 1 ? 'ask_implausibly_low' : 'ask_implausibly_high'] };
103 105 }
104 106 const discount = round(ratio - 1, 4);
107 + if (discount < DEAL_REVIEW_THRESHOLD) return { discount, ratio: round(ratio, 4), verdict: 'review', reasons: ['discount_exceeds_review_threshold'] };
105 108 return { discount, ratio: round(ratio, 4), verdict: discount <= DEAL_THRESHOLD ? 'deal' : discount >= PREMIUM_THRESHOLD ? 'premium' : 'fair', reasons };
106 109 }
107 110
@@ -111,7 +114,7 @@ export function assessAsk(i: AskAssessmentInput): AskAssessment {
111 114 */
112 115 export function valueOpportunity(askUsd: number | null, riv: number | null, confidence: number | null, sampleSize?: number | null): number | null {
113 116 const a = assessAsk({ askUsd, rivUsd: riv, confidence, sampleSize: sampleSize ?? undefined });
114 − return a.verdict === 'anomaly' || a.verdict === 'ungated' ? null : a.discount;
117 + return a.verdict === 'anomaly' || a.verdict === 'ungated' || a.verdict === 'review' ? null : a.discount;
115 118 }
116 119
117 120 /**
@@ -120,7 +123,7 @@ export function valueOpportunity(askUsd: number | null, riv: number | null, conf
120 123 * multiplicative so one weak leg (e.g. an illiquid asset) cannot be compensated by a huge discount.
121 124 */
122 125 export function dealScore(input: { discount: number | null; confidence: number | null; sampleSize: number | null; liquidity: number | null; matchConfidence?: number | null }): number {
123 − if (input.discount === null || input.discount >= 0) return 0;
126 + if (input.discount === null || input.discount >= 0 || input.discount < DEAL_REVIEW_THRESHOLD) return 0;
124 127 const depth = clamp(-input.discount / 0.5, 0, 1) ** 0.7; // 50 % below → 1, concave so 10 % is already meaningful
125 128 const conf = 0.4 + 0.6 * clamp(input.confidence ?? 0, 0, 1);
126 129 const sample = 0.5 + 0.5 * clamp(Math.log10(1 + (input.sampleSize ?? 0)) / Math.log10(41), 0, 1); // 40 sales → 1
@@ -137,6 +140,10 @@ export const MAX_PLAUSIBLE_CHANGE = 5;
137 140 export function isPlausibleChange(value: number | null | undefined): boolean {
138 141 return value !== null && value !== undefined && Number.isFinite(value) && Math.abs(value) <= MAX_PLAUSIBLE_CHANGE;
139 142 }
143 +/** A discount that may be shown as a deal (not an anomaly, not beyond the review threshold). */
144 +export function isActionableDiscount(value: number | null | undefined): boolean {
145 + return value !== null && value !== undefined && Number.isFinite(value) && value >= DEAL_REVIEW_THRESHOLD && isPlausibleDiscount(value);
146 +}
140 147 export function isPlausibleDiscount(value: number | null | undefined): boolean {
141 148 if (value === null || value === undefined || !Number.isFinite(value)) return false;
142 149 const ratio = 1 + value;
modified packages/valuation/src/valuation.test.ts +8 −0
@@ -118,6 +118,14 @@ describe('ask vs RIV gate (§83–§84, §196)', () => {
118 118 expect(assessAsk({ askUsd: 900, rivUsd: 1000, confidence: 0.9, sampleSize: 30, feesUsd: 150 }).verdict).toBe('fair');
119 119 expect(assessAsk({ askUsd: 900, rivUsd: 1000, confidence: 0.9, sampleSize: 30 }).verdict).toBe('deal');
120 120 });
121 + it('sends asks more than 50 % below RIV to review instead of calling them deals (§174)', () => {
122 + const a = assessAsk({ askUsd: 115.8, rivUsd: 1077.54, confidence: 0.8, sampleSize: 58 }); // a booster pack ask matched to a booster-box RIV
123 + expect(a.verdict).toBe('review');
124 + expect(a.discount).toBeCloseTo(-0.8925, 3);
125 + expect(valueOpportunity(115.8, 1077.54, 0.8, 58)).toBeNull();
126 + expect(dealScore({ discount: -0.89, confidence: 0.8, sampleSize: 58, liquidity: 60 })).toBe(0);
127 + expect(assessAsk({ askUsd: 520, rivUsd: 1000, confidence: 0.8, sampleSize: 58 }).verdict).toBe('deal');
128 + });
121 129 it('deal score is 0 without a gated discount and rewards confidence, depth and liquidity', () => {
122 130 expect(dealScore({ discount: null, confidence: 0.9, sampleSize: 30, liquidity: 70 })).toBe(0);
123 131 expect(dealScore({ discount: 0.2, confidence: 0.9, sampleSize: 30, liquidity: 70 })).toBe(0);
modified workers/account/digest.ts +1 −1
@@ -44,7 +44,7 @@ export async function runDigests(db: Database, now = new Date()): Promise<number
44 44 `)) as unknown as Array<{ title: string; slug: string; riv_usd: number | null; change_7d: number | null; change_1d: number | null }>;
45 45 const deals = (await db.execute(sql`
46 46 select a.title, a.slug, l.price_usd, l.discount_to_riv from listings l join assets a on a.id = l.asset_id join asset_stats s on s.asset_id = a.id
47 − where l.availability = 'available' and l.discount_to_riv <= -0.15 and s.riv_confidence >= 0.5 and s.riv_sample_size >= 5
47 + where l.availability = 'available' and l.discount_to_riv <= -0.15 and l.discount_to_riv >= -0.5 and not ('riv_review' = any(l.flags)) and s.riv_confidence >= 0.5 and s.riv_sample_size >= 5
48 48 and a.category_slug in (
49 49 select distinct a2.category_slug from collection_items ci join collections c on c.id = ci.collection_id join assets a2 on a2.id = ci.asset_id where c.user_id = ${m.id}
50 50 union select wi.target_id from watchlist_items wi join watchlists w on w.id = wi.watchlist_id where w.user_id = ${m.id} and wi.target_type = 'category'
modified workers/indices/run.ts +1 −1
@@ -262,7 +262,7 @@ export async function runRadar(opts: { now?: Date } = {}): Promise<number> {
262 262 const cheap = await db().execute(sql`
263 263 select l.id, l.asset_id, l.discount_to_riv::float as d, l.price_usd::float as ask, s.riv_usd::float as riv, s.riv_confidence::float as conf
264 264 from listings l join asset_stats s on s.asset_id = l.asset_id
265 − where l.availability = 'available' and l.discount_to_riv <= -0.25 and s.riv_confidence >= 0.6 and l.price_usd > 0 limit 2000`);
265 + where l.availability = 'available' and l.discount_to_riv <= -0.25 and l.discount_to_riv >= -0.5 and not ('riv_review' = any(l.flags)) and s.riv_confidence >= 0.6 and s.riv_sample_size >= 5 and l.price_usd > 0 limit 2000`);
266 266 for (const r of cheap as unknown as Array<{ id: string; asset_id: string; d: number; ask: number; riv: number; conf: number }>) {
267 267 await db().insert(radarFindings).values({ id: newId('event'), assetId: r.asset_id, kind: 'price_discrepancy', score: -r.d * r.conf, evidence: { askUsd: r.ask, rivUsd: r.riv, discount: r.d, confidence: r.conf }, entityType: 'listing', entityId: r.id, detectedAt: now, expiresAt: new Date(now.getTime() + 7 * DAY) }).onConflictDoUpdate({ target: [radarFindings.kind, radarFindings.entityType, radarFindings.entityId], set: { detectedAt: now, score: -r.d * r.conf, evidence: { askUsd: r.ask, rivUsd: r.riv, discount: r.d, confidence: r.conf } } });
268 268 n++;
modified workers/valuation/run.ts +7 −3
@@ -1,6 +1,6 @@
1 1 import { and, desc, eq, gte, inArray, isNull, or, sql } from 'drizzle-orm';
2 2 import { assetStats, assetVariants, assets, gradePremiums, listings, populationReports, priceObservations, priceSnapshots, sales, sources, valuations, variantStats } from '@rareindex/database';
3 −import { ASK_ANOMALY_HIGH_RATIO, ASK_ANOMALY_LOW_RATIO, ASK_MIN_CONFIDENCE, ASK_MIN_MATCH_CONFIDENCE, ASK_MIN_SAMPLE, adjustmentFactor, assetDataQuality, computeGradePremiums, computeValuation, detectOutliers, liquidityScore, momentumScore, rarityScore, trendingScore, type GradePremium, type SaleInput } from '@rareindex/valuation';
3 +import { ASK_ANOMALY_HIGH_RATIO, ASK_ANOMALY_LOW_RATIO, ASK_MIN_CONFIDENCE, ASK_MIN_MATCH_CONFIDENCE, ASK_MIN_SAMPLE, DEAL_REVIEW_THRESHOLD, adjustmentFactor, assetDataQuality, computeGradePremiums, computeValuation, detectOutliers, liquidityScore, momentumScore, rarityScore, trendingScore, type GradePremium, type SaleInput } from '@rareindex/valuation';
4 4 import { logger, median, newId, pctChange, toDateOnly } from '@rareindex/shared';
5 5 import { db } from '../lib/db.ts';
6 6 import { auditMany } from '../lib/audit.ts';
@@ -156,7 +156,11 @@ export async function valueAsset(assetId: string, opts: { now?: Date; rebuildHis
156 156 const scope = and(eq(listings.variantId, v.id), eq(listings.availability, 'available'), sql`${listings.priceUsd} > 0`, sql`${listings.listingType} <> 'auction'`);
157 157 await db()
158 158 .update(listings)
159 − .set({ discountToRiv: sql`round((${listings.priceUsd} - ${out.riv}) / ${out.riv}, 4)`, flags: sql`array_remove(${listings.flags}, 'riv_anomaly')` })
159 + .set({
160 + discountToRiv: sql`round((${listings.priceUsd} - ${out.riv}) / ${out.riv}, 4)`,
161 + // > 50 % below RIV (§174): keep the number, flag `riv_review`, never surface as a deal
162 + flags: sql`case when ${listings.priceUsd} < ${out.riv * (1 + DEAL_REVIEW_THRESHOLD)} then (case when 'riv_review' = any(${listings.flags}) then array_remove(${listings.flags}, 'riv_anomaly') else array_append(array_remove(${listings.flags}, 'riv_anomaly'), 'riv_review') end) else array_remove(array_remove(${listings.flags}, 'riv_anomaly'), 'riv_review') end`,
163 + })
160 164 .where(and(scope, sql`${listings.priceUsd} between ${lo} and ${hi}`, sql`${listings.confidence} >= ${ASK_MIN_MATCH_CONFIDENCE}`, sql`not (${listings.grader} is not null and ${listings.grade} is null)`));
161 165 await db()
162 166 .update(listings)
@@ -205,7 +209,7 @@ export async function valueAsset(assetId: string, opts: { now?: Date; rebuildHis
205 209 const trending = trendingScore({ priceMomentum: mom.m30, volumeMomentum: momentumScore({ priceChange: null, volumeNow: sales30d, volumePrev: salesPrev30 }), searchMomentum: null, listingMomentum, newsMomentum: null });
206 210 // Best gated ask across the asset's variants (negative = below its own variant's RIV). No fallback:
207 211 // comparing the cheapest ask of ANY variant against the headline RIV was the −8,500 % bug.
208 − const [bestListing] = await db().select({ d: sql<number | null>`min(${listings.discountToRiv})` }).from(listings).where(and(eq(listings.assetId, assetId), eq(listings.availability, 'available'), sql`${listings.discountToRiv} is not null`));
212 + const [bestListing] = await db().select({ d: sql<number | null>`min(${listings.discountToRiv})` }).from(listings).where(and(eq(listings.assetId, assetId), eq(listings.availability, 'available'), sql`${listings.discountToRiv} is not null`, sql`not ('riv_review' = any(${listings.flags}))`));
209 213 const fields = { brand: asset.brand, set: asset.setName, number: asset.number, year: asset.year, variant: asset.variant, image: asset.heroImageUrl, description: asset.description, identifiers: Object.keys(asset.identifiers).length ? 1 : null };
210 214 const dq = assetDataQuality({ fieldsPresent: Object.values(fields).filter((x) => x !== null && x !== undefined).length, fieldsTotal: Object.keys(fields).length, sourceTrustAvg: sourcesCount ? median([...new Set(saleRows.map((s) => s.sourceId))].map((s) => trust.get(s) ?? 0.5)) : null, identificationConfidence: rep ? Number(median(validSales.map((s) => Number(s.confidence))) ?? 0.8) : null, hasImage: Boolean(asset.heroImageUrl), salesCount });
211 215 const statsRow = {
212 216