SPB Git

spb/rareindex

Public

Valuation safety (phase 1): ask-vs-RIV anomaly gate, variant-aware discounts, grade parsing fixes

- scores: assessAsk()/dealScore()/plausibility gates — an ask is compared only with a
  transaction-based RIV of the SAME variant (>=5 sales, confidence >=0.5, match >=0.7);
  asks <10% or >10x RIV are flagged riv_anomaly, never surfaced as deals
- valuation worker: discounts reset each pass, representative variant prefers the default
  (raw/base) variant when it qualifies, changes measured on the representative variant's
  own series, no cross-variant fallback for value_opportunity, numeric(8,6) overflow guard
- taxonomy: parse PSA/DNA n, (8.5), Authentic-only slabs; a grader without a readable grade
  gets its own 'grader|unknown' variant instead of polluting raw
- resolver/writers: refineGrade() re-parses titles for sales, listings, observations, lots
- web: VsRiv primitive (Data/identity anomaly label), opportunity/gainers/losers/listings
  sort + filter directions fixed (negative = below RIV), scanner verdict via assessAsk with FX
- indices: RARE publishes only when its 25-constituent minimum is met
- security: atomic ai_quotas upsert with unique key, scanner choose ownership check,
  anonymous research thread authz
- db: migration 0006 (hot-path indexes, ai_quotas unique); ri regrade repair command

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 11, 2026) parent 1f956d0 Browse files

31 changed files +9,556 −101

modified apps/web/package.json +1 −0
@@ -18,6 +18,7 @@
18 "@rareindex/search": "workspace:*", 18 "@rareindex/search": "workspace:*",
19 "@rareindex/shared": "workspace:*", 19 "@rareindex/shared": "workspace:*",
20 "@rareindex/taxonomy": "workspace:*", 20 "@rareindex/taxonomy": "workspace:*",
21+ "@rareindex/valuation": "workspace:*",
21 "@tanstack/react-query": "^5.90.0", 22 "@tanstack/react-query": "^5.90.0",
22 "drizzle-orm": "^0.45.0", 23 "drizzle-orm": "^0.45.0",
23 "lucide-react": "^1.0.0", 24 "lucide-react": "^1.0.0",
modified apps/web/src/app/api/scanner/[id]/choose/route.ts +4 −1
@@ -1,6 +1,7 @@
1 import { NextResponse } from 'next/server'; 1 import { NextResponse } from 'next/server';
2 import { z } from 'zod'; 2 import { z } from 'zod';
3 import { chooseCandidate } from '@/lib/ai/scanner'; 3 import { chooseCandidate } from '@/lib/ai/scanner';
4+import { clientIpHash, currentUserId, getAnonId } from '@/lib/ai/request';
4 5
5 export const runtime = 'nodejs'; 6 export const runtime = 'nodejs';
6 7
@@ -8,6 +9,8 @@ export async function POST(req: Request, ctx: { params: Promise<{ id: string }>
8 const { id } = await ctx.params; 9 const { id } = await ctx.params;
9 const body = z.object({ assetId: z.string().nullable() }).safeParse(await req.json().catch(() => ({}))); 10 const body = z.object({ assetId: z.string().nullable() }).safeParse(await req.json().catch(() => ({})));
10 if (!body.success || !/^scan_[a-z0-9]+$/.test(id)) return NextResponse.json({ error: 'Invalid request' }, { status: 400 }); 11 if (!body.success || !/^scan_[a-z0-9]+$/.test(id)) return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
11− await chooseCandidate(id, body.data.assetId); 12+ const [userId, anonId, ipHash] = await Promise.all([currentUserId(), getAnonId(false), clientIpHash()]);
13+ const ok = await chooseCandidate(id, body.data.assetId, { userId, anonId, ipHash });
14+ if (!ok) return NextResponse.json({ error: 'Not found' }, { status: 404 });
12 return NextResponse.json({ ok: true }); 15 return NextResponse.json({ ok: true });
13 } 16 }
modified apps/web/src/app/page.tsx +1 −1
@@ -205,7 +205,7 @@ async function Discovery() {
205 <RadarRail items={radar} hideWhenEmpty /> 205 <RadarRail items={radar} hideWhenEmpty />
206 <LotsRail items={lots} hideWhenEmpty /> 206 <LotsRail items={lots} hideWhenEmpty />
207 <AssetRail title="Newly Added" subtitle="Latest canonical assets — guide prices labelled, RIV when available" href="/explore?sort=newest" items={newest} metric="guide" /> 207 <AssetRail title="Newly Added" subtitle="Latest canonical assets — guide prices labelled, RIV when available" href="/explore?sort=newest" items={newest} metric="guide" />
208− {opportunities.length ? <AssetRail title="Value Opportunities" subtitle="Listings materially below RIV — analytical data, not advice" href="/listings?sort=discount" items={opportunities} metric="opportunity" /> : null} 208+ {opportunities.length ? <AssetRail title="Value Opportunities" subtitle="Asks ≥ 10 % below a transaction-based RIV of the same variant (≥ 5 sales, medium+ confidence). Analytical data, not advice; implausible asks are held back as data anomalies." href="/listings?sort=discount&disc=10" items={opportunities} metric="opportunity" /> : null}
209 {watched.length ? <AssetRail title="Most Watched" subtitle="Assets on the most watchlists" href="/explore?sort=trending" items={watched} metric="watchers" /> : null} 209 {watched.length ? <AssetRail title="Most Watched" subtitle="Assets on the most watchlists" href="/explore?sort=trending" items={watched} metric="watchers" /> : null}
210 <MarketInsights rows={markets} /> 210 <MarketInsights rows={markets} />
211 </> 211 </>
modified apps/web/src/components/asset/sales-cards.tsx +2 −5
@@ -1,7 +1,7 @@
1 import type { SaleRow, ListingRow } from '@/lib/queries/assets'; 1 import type { SaleRow, ListingRow } from '@/lib/queries/assets';
2 import { fmtDate, fmtMoney, fmtRelative, cn } from '@/lib/format'; 2 import { fmtDate, fmtMoney, fmtRelative, cn } from '@/lib/format';
3 import { humanize } from '@/lib/taxonomy'; 3 import { humanize } from '@/lib/taxonomy';
4−import { Badge, Delta, EmptyState } from '@/components/ui/primitives'; 4+import { Badge, Delta, EmptyState, VsRiv } from '@/components/ui/primitives';
5 import { GradeBadge, SourceLink, Thumb } from '@/components/market/bits'; 5 import { GradeBadge, SourceLink, Thumb } from '@/components/market/bits';
6 6
7 /** Phone-first card lists for sales and listings (tables stay for md+). */ 7 /** Phone-first card lists for sales and listings (tables stay for md+). */
@@ -48,10 +48,7 @@ export function ListingsCards({ items, className, emptyDescription = 'Live listi
48 <div className="flex items-baseline justify-between gap-2"> 48 <div className="flex items-baseline justify-between gap-2">
49 <span className="num text-[15px] font-semibold text-fg">{fmtMoney(l.priceUsd)}</span> 49 <span className="num text-[15px] font-semibold text-fg">{fmtMoney(l.priceUsd)}</span>
50 {l.discountToRiv !== null ? ( 50 {l.discountToRiv !== null ? (
51− <span className="inline-flex items-center gap-1 text-[11px]"> 51+ <VsRiv discount={l.discountToRiv} className="text-[11px]" />
52− <span className="text-subtle">vs RIV</span>
53− <Delta value={-l.discountToRiv} />
54− </span>
55 ) : ( 52 ) : (
56 <span className="text-[11px] text-subtle">{fmtRelative(l.lastSeenAt)}</span> 53 <span className="text-[11px] text-subtle">{fmtRelative(l.lastSeenAt)}</span>
57 )} 54 )}
modified apps/web/src/components/market/asset-list.tsx +3 −3
@@ -2,7 +2,7 @@ import Link from 'next/link';
2 import type { AssetCard as AssetCardData } from '@/lib/queries/assets'; 2 import type { AssetCard as AssetCardData } from '@/lib/queries/assets';
3 import { fmtMoney, fmtNum, fmtRelative, confidenceLabel, cn } from '@/lib/format'; 3 import { fmtMoney, fmtNum, fmtRelative, confidenceLabel, cn } from '@/lib/format';
4 import { catName } from '@/lib/taxonomy'; 4 import { catName } from '@/lib/taxonomy';
5−import { Delta, Table, th, td, tdNum, EmptyState } from '@/components/ui/primitives'; 5+import { Delta, Table, th, td, tdNum, EmptyState, VsRiv } from '@/components/ui/primitives';
6 import { Thumb } from './bits'; 6 import { Thumb } from './bits';
7 import { PriceLine, TileImage } from './asset-tile'; 7 import { PriceLine, TileImage } from './asset-tile';
8 8
@@ -81,7 +81,7 @@ function cell(a: AssetCardData, c: AssetColumn) {
81 case 'trending': 81 case 'trending':
82 return a.trendingScore === null ? <span className="text-subtle">—</span> : a.trendingScore.toFixed(1); 82 return a.trendingScore === null ? <span className="text-subtle">—</span> : a.trendingScore.toFixed(1);
83 case 'opportunity': 83 case 'opportunity':
84− return a.valueOpportunity === null ? <span className="text-subtle">—</span> : <Delta value={-a.valueOpportunity} />; 84+ return <VsRiv discount={a.valueOpportunity} showLabel={false} />;
85 case 'watchers': 85 case 'watchers':
86 return fmtNum(a.watchers); 86 return fmtNum(a.watchers);
87 case 'updated': 87 case 'updated':
@@ -142,7 +142,7 @@ export function AssetCard({ a, className, metric = 'change30d' }: { a: AssetCard
142 metric === 'latestSale' ? ( 142 metric === 'latestSale' ? (
143 a.latestSaleUsd !== null && a.rivUsd !== null ? <span className="num text-[11px] text-muted">last {fmtMoney(a.latestSaleUsd)}</span> : a.number ? <span className="num text-[11px] text-subtle">#{a.number}</span> : null 143 a.latestSaleUsd !== null && a.rivUsd !== null ? <span className="num text-[11px] text-muted">last {fmtMoney(a.latestSaleUsd)}</span> : a.number ? <span className="num text-[11px] text-subtle">#{a.number}</span> : null
144 ) : metric === 'opportunity' ? ( 144 ) : metric === 'opportunity' ? (
145− a.valueOpportunity !== null ? <span className="text-[11px] text-muted">vs RIV <Delta value={-a.valueOpportunity} className="text-[11px]" /></span> : null 145+ a.valueOpportunity !== null ? <VsRiv discount={a.valueOpportunity} className="text-[11px]" /> : null
146 ) : metric === 'trending' ? ( 146 ) : metric === 'trending' ? (
147 a.trendingScore !== null ? <span className="num text-[11px] text-muted">trend {a.trendingScore.toFixed(1)}</span> : null 147 a.trendingScore !== null ? <span className="num text-[11px] text-muted">trend {a.trendingScore.toFixed(1)}</span> : null
148 ) : value !== null ? ( 148 ) : value !== null ? (
modified apps/web/src/components/market/asset-tile.tsx +2 −4
@@ -3,7 +3,7 @@ import type { ReactNode } from 'react';
3 import type { AssetCard as AssetCardData } from '@/lib/queries/assets'; 3 import type { AssetCard as AssetCardData } from '@/lib/queries/assets';
4 import { cn, confidenceLabel, fmtMoney, fmtNum, fmtRelative } from '@/lib/format'; 4 import { cn, confidenceLabel, fmtMoney, fmtNum, fmtRelative } from '@/lib/format';
5 import { catName } from '@/lib/taxonomy'; 5 import { catName } from '@/lib/taxonomy';
6−import { Delta } from '@/components/ui/primitives'; 6+import { Delta, VsRiv } from '@/components/ui/primitives';
7 7
8 export type TileMetric = 'change7d' | 'change1d' | 'change30d' | 'trending' | 'watchers' | 'riv' | 'opportunity' | 'new' | 'guide' | 'latestSale'; 8 export type TileMetric = 'change7d' | 'change1d' | 'change30d' | 'trending' | 'watchers' | 'riv' | 'opportunity' | 'new' | 'guide' | 'latestSale';
9 9
@@ -61,9 +61,7 @@ function MetricNode({ a, metric }: { a: AssetCardData; metric: TileMetric }) {
61 return <span className="num text-[11px] text-muted">{fmtNum(a.watchers)} watching</span>; 61 return <span className="num text-[11px] text-muted">{fmtNum(a.watchers)} watching</span>;
62 case 'opportunity': 62 case 'opportunity':
63 return ( 63 return (
64− <span className="text-[11px] text-muted"> 64+ <VsRiv discount={a.valueOpportunity} className="text-[12px]" />
65− vs RIV <Delta value={a.valueOpportunity === null ? null : -a.valueOpportunity} className="text-[12px]" />
66− </span>
67 ); 65 );
68 case 'new': 66 case 'new':
69 return <span className="text-[11px] text-muted">added {fmtRelative(a.updatedAt)}</span>; 67 return <span className="text-[11px] text-muted">added {fmtRelative(a.updatedAt)}</span>;
modified apps/web/src/components/market/sales-table.tsx +3 −3
@@ -2,7 +2,7 @@ import type { SaleRow, ListingRow } from '@/lib/queries/assets';
2 import type { LotRow } from '@/lib/queries/market-lists'; 2 import type { LotRow } from '@/lib/queries/market-lists';
3 import { fmtDate, fmtMoney, fmtRelative, fmtPct, cn } from '@/lib/format'; 3 import { fmtDate, fmtMoney, fmtRelative, fmtPct, cn } from '@/lib/format';
4 import { humanize } from '@/lib/taxonomy'; 4 import { humanize } from '@/lib/taxonomy';
5−import { Table, th, td, tdNum, EmptyState, Badge, Delta } from '@/components/ui/primitives'; 5+import { Table, th, td, tdNum, EmptyState, Badge, Delta, VsRiv } from '@/components/ui/primitives';
6 import { AssetTitleCell, GradeBadge, PriceCell, SourceLink, Thumb } from './bits'; 6 import { AssetTitleCell, GradeBadge, PriceCell, SourceLink, Thumb } from './bits';
7 7
8 /** Card list for sales on small screens (image, title, grade, price, source). */ 8 /** Card list for sales on small screens (image, title, grade, price, source). */
@@ -119,7 +119,7 @@ export function ListingsCards({ items, className }: { items: ListingRow[]; class
119 </span> 119 </span>
120 <span className="num flex shrink-0 flex-col items-end leading-tight"> 120 <span className="num flex shrink-0 flex-col items-end leading-tight">
121 <span className="text-[14px] font-semibold text-fg">{fmtMoney(l.priceUsd)}</span> 121 <span className="text-[14px] font-semibold text-fg">{fmtMoney(l.priceUsd)}</span>
122− {l.discountToRiv !== null ? <Delta value={-l.discountToRiv} className="text-[11px]" /> : <span className="text-[10px] text-subtle">{humanize(l.listingType)}</span>} 122+ {l.discountToRiv !== null ? <VsRiv discount={l.discountToRiv} className="text-[11px]" showLabel={false} /> : <span className="text-[10px] text-subtle">{humanize(l.listingType)}</span>}
123 </span> 123 </span>
124 </a> 124 </a>
125 </li> 125 </li>
@@ -186,7 +186,7 @@ export function ListingsTable({ items, showAsset = true, className, emptyDescrip
186 <span className="text-subtle" title="No valuation to compare against">—</span> 186 <span className="text-subtle" title="No valuation to compare against">—</span>
187 ) : ( 187 ) : (
188 <span className="inline-flex flex-col items-end leading-tight"> 188 <span className="inline-flex flex-col items-end leading-tight">
189− <Delta value={-l.discountToRiv} /> 189+ <VsRiv discount={l.discountToRiv} showLabel={false} />
190 {l.assetRivUsd !== undefined && l.assetRivUsd !== null ? <span className="text-[10px] text-subtle">RIV {fmtMoney(l.assetRivUsd)}</span> : null} 190 {l.assetRivUsd !== undefined && l.assetRivUsd !== null ? <span className="text-[10px] text-subtle">RIV {fmtMoney(l.assetRivUsd)}</span> : null}
191 </span> 191 </span>
192 )} 192 )}
modified apps/web/src/components/scanner/scanner-client.tsx +1 −1
@@ -260,7 +260,7 @@ function ScanResults({ r, chosen, onChoose }: { r: ScanResponse; chosen: string
260 <CardHeader title="Parsed listing" subtitle={r.listing.sourceId} action={<a href={r.listing.sourceUrl} target="_blank" rel="noreferrer noopener" className="text-muted hover:text-fg">Open source ↗</a>} /> 260 <CardHeader title="Parsed listing" subtitle={r.listing.sourceId} action={<a href={r.listing.sourceUrl} target="_blank" rel="noreferrer noopener" className="text-muted hover:text-fg">Open source ↗</a>} />
261 <div className="grid gap-4 p-4 sm:grid-cols-3"> 261 <div className="grid gap-4 p-4 sm:grid-cols-3">
262 <Stat label="Asking price" value={r.listing.price !== null ? fmtMoney(r.listing.price, r.listing.currency ?? 'USD') : '—'} sub={r.listing.rawTitle} /> 262 <Stat label="Asking price" value={r.listing.price !== null ? fmtMoney(r.listing.price, r.listing.currency ?? 'USD') : '—'} sub={r.listing.rawTitle} />
263− <Stat label="vs RareIndex Valuation" value={r.listing.discountToRiv !== null ? <span className={r.listing.discountToRiv < 0 ? 'text-gain' : 'text-loss'}>{fmtPct(r.listing.discountToRiv, 1)}</span> : '—'} sub={{ below_fair_value: 'Below fair value range', in_range: 'Within fair value range', above_fair_value: 'Above fair value range', unknown: 'No valuation to compare' }[r.listing.verdict]} /> 263+ <Stat label="vs RareIndex Valuation" value={r.listing.verdict === 'anomaly' ? <span className="rounded-sm bg-alert-bg px-1.5 py-0.5 text-[11px] font-medium uppercase tracking-wide text-alert">Data/identity anomaly</span> : r.listing.discountToRiv !== null ? <span className={r.listing.discountToRiv <= -0.1 ? 'text-gain' : r.listing.discountToRiv >= 0.1 ? 'text-loss' : 'text-flat'}>{fmtPct(r.listing.discountToRiv, 1)}</span> : '—'} sub={{ below_fair_value: 'Below fair value range', in_range: 'Within fair value range', above_fair_value: 'Above fair value range', anomaly: 'Implausible against the valuation — review the variant, lot, currency and identity', unknown: 'No valuation to compare' }[r.listing.verdict] ?? 'No valuation to compare'} />
264 <Stat label="Grade on listing" value={r.listing.grader ? `${r.listing.grader.toUpperCase()} ${r.listing.grade ?? ''}` : 'Raw / unknown'} /> 264 <Stat label="Grade on listing" value={r.listing.grader ? `${r.listing.grader.toUpperCase()} ${r.listing.grade ?? ''}` : 'Raw / unknown'} />
265 </div> 265 </div>
266 </Card> 266 </Card>
modified apps/web/src/components/ui/primitives.tsx +25 −0
@@ -38,6 +38,31 @@ export function Delta({ value, digits = 2, className, arrow = false }: { value:
38 ); 38 );
39 } 39 }
40 40
41+/**
42+ * Ask vs RareIndex Valuation (§83–§84, §196). `discount` = (ask − RIV) / RIV as stored: negative = the ask
43+ * is below the valuation. Anything implausible (ask < 10 % or > 10× RIV) is a data/identity anomaly and is
44+ * labelled as such instead of being shown as a percentage — never as a "deal".
45+ */
46+export function VsRiv({ discount, className, digits = 1, showLabel = true }: { discount: number | null | undefined; className?: string; digits?: number; showLabel?: boolean }) {
47+ if (discount === null || discount === undefined || !Number.isFinite(discount)) return <span className={cn('text-subtle', className)} title="No comparable valuation for this variant">—</span>;
48+ const ratio = 1 + discount;
49+ if (ratio < 0.1 || ratio > 10) {
50+ return (
51+ <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="The asking price is implausible against this valuation (wrong variant, lot, currency or identity). Not a deal until reviewed.">
52+ Data/identity anomaly
53+ </span>
54+ );
55+ }
56+ const tone = discount <= -0.1 ? 'text-gain' : discount >= 0.1 ? 'text-loss' : 'text-flat';
57+ const word = discount <= -0.1 ? 'below RIV' : discount >= 0.1 ? 'above RIV' : 'near RIV';
58+ return (
59+ <span className={cn('num inline-flex items-baseline gap-1 font-medium', tone, className)} title={`Asking price ${fmtPct(Math.abs(discount), digits, false)} ${word}. Analytical data, not advice.`}>
60+ {fmtPct(discount, digits)}
61+ {showLabel ? <span className="text-[0.85em] font-normal text-subtle">vs RIV</span> : null}
62+ </span>
63+ );
64+}
65+
41 export function Badge({ children, tone = 'neutral', className, dot = false }: { children: ReactNode; tone?: 'neutral' | 'gain' | 'loss' | 'index' | 'rarity' | 'alert' | 'gold'; className?: string; dot?: boolean }) { 66 export function Badge({ children, tone = 'neutral', className, dot = false }: { children: ReactNode; tone?: 'neutral' | 'gain' | 'loss' | 'index' | 'rarity' | 'alert' | 'gold'; className?: string; dot?: boolean }) {
42 const tones: Record<string, string> = { 67 const tones: Record<string, string> = {
43 neutral: 'bg-inset text-muted', 68 neutral: 'bg-inset text-muted',
modified apps/web/src/lib/ai/request.ts +8 −8
@@ -2,7 +2,7 @@ import 'server-only';
2 import { createHmac, randomUUID } from 'node:crypto'; 2 import { createHmac, randomUUID } from 'node:crypto';
3 import { cookies, headers } from 'next/headers'; 3 import { cookies, headers } from 'next/headers';
4 import { getCurrentUser } from '@/lib/auth/session'; 4 import { getCurrentUser } from '@/lib/auth/session';
5−import { getDb, aiQuotas, sql } from '@rareindex/database'; 5+import { getDb, sql } from '@rareindex/database';
6 6
7 const SECRET = () => process.env.SESSION_SECRET ?? 'dev-only'; 7 const SECRET = () => process.env.SESSION_SECRET ?? 'dev-only';
8 8
@@ -56,13 +56,13 @@ export async function consumeQuota(feature: keyof typeof QUOTAS, opts: { userId:
56 const limit = opts.userId ? QUOTAS[feature]!.user : QUOTAS[feature]!.anonymous; 56 const limit = opts.userId ? QUOTAS[feature]!.user : QUOTAS[feature]!.anonymous;
57 const key = opts.userId ? `user:${opts.userId}` : `ip:${opts.ipHash}`; 57 const key = opts.userId ? `user:${opts.userId}` : `ip:${opts.ipHash}`;
58 const date = new Date().toISOString().slice(0, 10); 58 const date = new Date().toISOString().slice(0, 10);
59− const db = getDb(); 59+ // Atomic increment (unique (key, feature, date)): concurrent requests can no longer create
60− const [row] = await db.execute(sql`select count from ai_quotas where key = ${key} and feature = ${feature} and date = ${date} limit 1`); 60+ // duplicate counters and slip past the limit.
61− const used = Number((row as { count?: number } | undefined)?.count ?? 0); 61+ const [row] = await getDb().execute(sql`insert into ai_quotas (key, feature, date, count) values (${key}, ${feature}, ${date}, 1)
62− if (used >= limit) return { ok: false, used, limit }; 62+ on conflict (key, feature, date) do update set count = ai_quotas.count + 1 returning count`);
63− if (row) await db.execute(sql`update ai_quotas set count = count + 1 where key = ${key} and feature = ${feature} and date = ${date}`); 63+ const used = Number((row as { count?: number } | undefined)?.count ?? 1);
64− else await db.insert(aiQuotas).values({ key, feature, date, count: 1 }); 64+ if (used > limit) return { ok: false, used: used - 1, limit };
65− return { ok: true, used: used + 1, limit }; 65+ return { ok: true, used, limit };
66 } 66 }
67 67
68 export function aiErrorMessage(err: unknown): { status: number; message: string } { 68 export function aiErrorMessage(err: unknown): { status: number; message: string } {
modified apps/web/src/lib/ai/research-tools.ts +2 −1
@@ -203,11 +203,12 @@ export async function executeTool(name: string, rawInput: unknown): Promise<unkn
203 const i = input as z.infer<typeof TOOL_SCHEMAS.screen>; 203 const i = input as z.infer<typeof TOOL_SCHEMAS.screen>;
204 const col = sql.raw(`s.${WINDOWS[i.window as Window]}`); 204 const col = sql.raw(`s.${WINDOWS[i.window as Window]}`);
205 const sortCol = { change: col, value: sql`s.riv_usd`, liquidity: sql`s.liquidity_score`, sales: sql`s.sales_count`, trending: sql`s.trending_score`, opportunity: sql`s.value_opportunity` }[i.sort as string] ?? col; 205 const sortCol = { change: col, value: sql`s.riv_usd`, liquidity: sql`s.liquidity_score`, sales: sql`s.sales_count`, trending: sql`s.trending_score`, opportunity: sql`s.value_opportunity` }[i.sort as string] ?? col;
206+ const sortDir = i.sort === 'opportunity' ? sql`ASC NULLS LAST` : sql`DESC NULLS LAST`; // opportunity: most below RIV first
206 const rows = await run(sql`select ${ASSET_COLS} from assets a join asset_stats s on s.asset_id = a.id where s.riv_sample_size >= ${i.min_sales} ${scope(i.category)} 207 const rows = await run(sql`select ${ASSET_COLS} from assets a join asset_stats s on s.asset_id = a.id where s.riv_sample_size >= ${i.min_sales} ${scope(i.category)}
207 ${i.min_price_usd != null ? sql`and s.riv_usd >= ${i.min_price_usd}` : sql``} ${i.max_price_usd != null ? sql`and s.riv_usd <= ${i.max_price_usd}` : sql``} 208 ${i.min_price_usd != null ? sql`and s.riv_usd >= ${i.min_price_usd}` : sql``} ${i.max_price_usd != null ? sql`and s.riv_usd <= ${i.max_price_usd}` : sql``}
208 ${i.min_change != null ? sql`and ${col} >= ${i.min_change}` : sql``} ${i.max_change != null ? sql`and ${col} <= ${i.max_change}` : sql``} 209 ${i.min_change != null ? sql`and ${col} >= ${i.min_change}` : sql``} ${i.max_change != null ? sql`and ${col} <= ${i.max_change}` : sql``}
209 ${i.max_sales_1y != null ? sql`and s.sales_1y <= ${i.max_sales_1y}` : sql``} ${i.min_liquidity != null ? sql`and s.liquidity_score >= ${i.min_liquidity}` : sql``} 210 ${i.max_sales_1y != null ? sql`and s.sales_1y <= ${i.max_sales_1y}` : sql``} ${i.min_liquidity != null ? sql`and s.liquidity_score >= ${i.min_liquidity}` : sql``}
210− order by ${sortCol} desc nulls last limit ${i.limit}`); 211+ order by ${sortCol} ${sortDir} limit ${i.limit}`);
211 return { filters: i, count: rows.length, results: rows }; 212 return { filters: i, count: rows.length, results: rows };
212 } 213 }
213 case 'record_sales': { 214 case 'record_sales': {
modified apps/web/src/lib/ai/research.ts +3 −1
@@ -48,7 +48,9 @@ export async function loadSession(sessionId: string, owner: { anonId: string | n
48 const [s] = await db.select().from(researchSessions).where(eq(researchSessions.id, sessionId)).limit(1); 48 const [s] = await db.select().from(researchSessions).where(eq(researchSessions.id, sessionId)).limit(1);
49 if (!s) return null; 49 if (!s) return null;
50 if (s.userId && s.userId !== owner.userId) return null; 50 if (s.userId && s.userId !== owner.userId) return null;
51− if (!s.userId && s.anonId && s.anonId !== owner.anonId) return null; 51+ // anonymous threads are readable only by the browser that created them; a thread without an
52+ // anon id (cookie could not be set) is not readable by anyone afterwards
53+ if (!s.userId && (!owner.anonId || s.anonId !== owner.anonId)) return null;
52 const messages = await db.select().from(researchMessages).where(eq(researchMessages.sessionId, sessionId)).orderBy(researchMessages.createdAt); 54 const messages = await db.select().from(researchMessages).where(eq(researchMessages.sessionId, sessionId)).orderBy(researchMessages.createdAt);
53 return { session: s, messages }; 55 return { session: s, messages };
54 } 56 }
modified apps/web/src/lib/ai/scanner.ts +21 −5
@@ -5,6 +5,8 @@ import { getGrader, parseGradeFromTitle } from '@rareindex/taxonomy';
5 import { identifyCollectible, getRouter, type ImageInput, type Identification } from '@rareindex/ai'; 5 import { identifyCollectible, getRouter, type ImageInput, type Identification } from '@rareindex/ai';
6 import { lookupViaApi } from './lookup'; 6 import { lookupViaApi } from './lookup';
7 import { findCandidates, assetContext, type Candidate } from './candidates'; 7 import { findCandidates, assetContext, type Candidate } from './candidates';
8+import { assessAsk } from '@rareindex/valuation';
9+import { toUsdAt } from '@/lib/account/fx';
8 10
9 export interface ScanResult { 11 export interface ScanResult {
10 sessionId: string; 12 sessionId: string;
@@ -33,7 +35,7 @@ export interface ListingSummary {
33 identifiers: Record<string, string>; 35 identifiers: Record<string, string>;
34 /** price vs RIV when both known: negative = below fair value */ 36 /** price vs RIV when both known: negative = below fair value */
35 discountToRiv: number | null; 37 discountToRiv: number | null;
36− verdict: 'below_fair_value' | 'in_range' | 'above_fair_value' | 'unknown'; 38+ verdict: 'below_fair_value' | 'in_range' | 'above_fair_value' | 'anomaly' | 'unknown';
37 } 39 }
38 40
39 interface ScanInput { 41 interface ScanInput {
@@ -104,9 +106,16 @@ export async function runScan(input: ScanInput): Promise<ScanResult> {
104 const best = top && top.score >= 0.25 ? { ...top, context: await assetContext(top.assetId) } : null; 106 const best = top && top.score >= 0.25 ? { ...top, context: await assetContext(top.assetId) } : null;
105 if (!best) notes.push(candidates.length ? 'Low-confidence match: review the candidates below.' : 'No matching asset in the RareIndex catalog yet. The pipeline adds assets as connectors ingest sources.'); 107 if (!best) notes.push(candidates.length ? 'Low-confidence match: review the candidates below.' : 'No matching asset in the RareIndex catalog yet. The pipeline adds assets as connectors ingest sources.');
106 108
107− if (listing && best?.rivUsd && listing.price !== null && listing.currency === 'USD') { 109+ if (listing && best?.rivUsd && listing.price !== null && listing.currency) {
108− listing.discountToRiv = (listing.price - best.rivUsd) / best.rivUsd; 110+ // Compare in USD at today's ECB rate; gate exactly like the valuation worker (transaction-based RIV,
109− listing.verdict = listing.discountToRiv <= -0.15 ? 'below_fair_value' : listing.discountToRiv >= 0.2 ? 'above_fair_value' : 'in_range'; 111+ // ≥ 5 sales, medium+ confidence) and refuse implausible ratios instead of calling them deals.
112+ const fx = listing.currency === 'USD' ? { usd: listing.price } : await toUsdAt(listing.price, listing.currency, null);
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+ const a = assessAsk({ askUsd: fx?.usd ?? null, rivUsd: best.rivUsd, confidence: best.rivConfidence, sampleSize: best.rivSampleSize, sameVariant: gradedMismatch ? false : undefined });
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';
117+ 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+ else if (a.verdict === 'ungated') notes.push(`The ask was not compared with the valuation (${a.reasons.join(', ')}).`);
110 } 119 }
111 120
112 const durationMs = Date.now() - started; 121 const durationMs = Date.now() - started;
@@ -134,8 +143,15 @@ export async function runScan(input: ScanInput): Promise<ScanResult> {
134 return { sessionId: id, mode: input.mode, guess, guessConfidence: guess?.confidence ?? null, candidates, best, listing, model, usdEst, durationMs, notes }; 143 return { sessionId: id, mode: input.mode, guess, guessConfidence: guess?.confidence ?? null, candidates, best, listing, model, usdEst, durationMs, notes };
135 } 144 }
136 145
137−export async function chooseCandidate(sessionId: string, assetId: string | null): Promise<void> { 146+/** Record the user's confirmation of a candidate. Only the session's owner (user, anon cookie or, failing both, the same IP hash) may write it. */
147+export async function chooseCandidate(sessionId: string, assetId: string | null, owner: { userId: string | null; anonId: string | null; ipHash: string }): Promise<boolean> {
148+ const [s] = await getDb().select({ userId: scannerSessions.userId, anonId: scannerSessions.anonId, ipHash: scannerSessions.ipHash, candidates: scannerSessions.candidates }).from(scannerSessions).where(eq(scannerSessions.id, sessionId)).limit(1);
149+ if (!s) return false;
150+ const owns = s.userId ? s.userId === owner.userId : s.anonId ? s.anonId === owner.anonId : s.ipHash === owner.ipHash;
151+ if (!owns) return false;
152+ if (assetId !== null && !s.candidates.some((c) => c.assetId === assetId)) return false; // only a proposed candidate can be chosen
138 await getDb().update(scannerSessions).set({ chosenAssetId: assetId, chosenAt: new Date() }).where(eq(scannerSessions.id, sessionId)); 153 await getDb().update(scannerSessions).set({ chosenAssetId: assetId, chosenAt: new Date() }).where(eq(scannerSessions.id, sessionId));
154+ return true;
139 } 155 }
140 156
141 /** Resolve a marketplace URL through the connector framework (lookup → normalize), via the API service. */ 157 /** Resolve a marketplace URL through the connector framework (lookup → normalize), via the API service. */
modified apps/web/src/lib/queries/assets.ts +8 −4
@@ -135,7 +135,7 @@ const SORTS: Record<ExploreSort, SQL> = {
135 trending: sql`s.trending_score DESC NULLS LAST`, 135 trending: sql`s.trending_score DESC NULLS LAST`,
136 newest: sql`a.created_at DESC`, 136 newest: sql`a.created_at DESC`,
137 latest_sale: sql`s.latest_sale_at DESC NULLS LAST`, 137 latest_sale: sql`s.latest_sale_at DESC NULLS LAST`,
138− opportunity: sql`s.value_opportunity DESC NULLS LAST`, 138+ opportunity: sql`s.value_opportunity ASC NULLS LAST`, // most below RIV first
139 name: sql`a.name ASC`, 139 name: sql`a.name ASC`,
140 number: sql`a.set_name ASC NULLS LAST, (substring(a.number from '^[0-9]+'))::int ASC NULLS LAST, a.number ASC NULLS LAST`, 140 number: sql`a.set_name ASC NULLS LAST, (substring(a.number from '^[0-9]+'))::int ASC NULLS LAST, a.number ASC NULLS LAST`,
141 }; 141 };
@@ -193,13 +193,17 @@ export async function rankedAssets(kind: RankedKind, opts: { scope?: string[]; l
193 const chg = opts.window === '1d' ? sql`s.change_1d` : opts.window === '30d' ? sql`s.change_30d` : sql`s.change_7d`; 193 const chg = opts.window === '1d' ? sql`s.change_1d` : opts.window === '30d' ? sql`s.change_30d` : sql`s.change_7d`;
194 const spec: Record<typeof kind, { where: SQL; order: SQL }> = { 194 const spec: Record<typeof kind, { where: SQL; order: SQL }> = {
195 trending: { where: sql`s.trending_score IS NOT NULL`, order: sql`s.trending_score DESC` }, 195 trending: { where: sql`s.trending_score IS NOT NULL`, order: sql`s.trending_score DESC` },
196− gainers: { where: sql`${chg} IS NOT NULL AND ${chg} > 0 AND s.riv_sample_size >= 3`, order: sql`${chg} DESC` }, 196+ // Movers need a valuation that can move: ≥ 5 transactions, medium confidence, a plausible move (≤ ±500 %) and
197− losers: { where: sql`${chg} IS NOT NULL AND ${chg} < 0 AND s.riv_sample_size >= 3`, order: sql`${chg} ASC` }, 197+ // at least one sale in the last year (a stale series cannot "move").
198+ gainers: { where: sql`${chg} IS NOT NULL AND ${chg} > 0 AND ${chg} <= 5 AND s.riv_sample_size >= 5 AND s.riv_confidence >= 0.5 AND coalesce(s.sales_1y, 0) >= 3`, order: sql`${chg} DESC` },
199+ losers: { where: sql`${chg} IS NOT NULL AND ${chg} < 0 AND ${chg} >= -0.99 AND s.riv_sample_size >= 5 AND s.riv_confidence >= 0.5 AND coalesce(s.sales_1y, 0) >= 3`, order: sql`${chg} ASC` },
198 volume: { where: sql`s.volume_30d_usd IS NOT NULL AND s.volume_30d_usd > 0`, order: sql`s.volume_30d_usd DESC` }, 200 volume: { where: sql`s.volume_30d_usd IS NOT NULL AND s.volume_30d_usd > 0`, order: sql`s.volume_30d_usd DESC` },
199 watched: { where: sql`s.watchers > 0`, order: sql`s.watchers DESC, s.views_30d DESC` }, 201 watched: { where: sql`s.watchers > 0`, order: sql`s.watchers DESC, s.views_30d DESC` },
200 newest: { where: sql`true`, order: sql`a.created_at DESC` }, 202 newest: { where: sql`true`, order: sql`a.created_at DESC` },
201 expensive: { where: sql`s.riv_usd IS NOT NULL`, order: sql`s.riv_usd DESC` }, 203 expensive: { where: sql`s.riv_usd IS NOT NULL`, order: sql`s.riv_usd DESC` },
202− opportunity: { where: sql`s.value_opportunity IS NOT NULL AND s.value_opportunity > 0`, order: sql`s.value_opportunity DESC` }, 204+ // value_opportunity = (best gated ask − RIV) / RIV: negative = below the valuation. A deal is ≤ −10 %; the
205+ // worker never stores implausible ratios, the ≥ −0.9 bound is a second safety net (§84, §196).
206+ 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` },
203 liquid: { where: sql`s.liquidity_score IS NOT NULL`, order: sql`s.liquidity_score DESC` }, 207 liquid: { where: sql`s.liquidity_score IS NOT NULL`, order: sql`s.liquidity_score DESC` },
204 // Fallback rails used while valuations are still being computed (§192: honest, data-backed) 208 // Fallback rails used while valuations are still being computed (§192: honest, data-backed)
205 newest_priced: { where: sql`s.riv_usd IS NOT NULL`, order: sql`s.updated_at DESC` }, 209 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 −2
@@ -97,14 +97,14 @@ export async function listListings(f: ListingsFilters): Promise<{ items: Listing
97 if (f.grader) where.push(sql`l.grader = ${f.grader}`); 97 if (f.grader) where.push(sql`l.grader = ${f.grader}`);
98 if (f.minUsd != null) where.push(sql`l.price_usd >= ${f.minUsd}`); 98 if (f.minUsd != null) where.push(sql`l.price_usd >= ${f.minUsd}`);
99 if (f.maxUsd != null) where.push(sql`l.price_usd <= ${f.maxUsd}`); 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 >= ${f.minDiscount}`); 100+ if (f.minDiscount != null) where.push(sql`l.discount_to_riv <= ${-Math.abs(f.minDiscount)}`); // stored negative = below RIV
101 if (f.listingType) where.push(sql`l.listing_type = ${f.listingType}`); 101 if (f.listingType) where.push(sql`l.listing_type = ${f.listingType}`);
102 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() + '%'})`); 102 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 const orders: Record<NonNullable<ListingsFilters['sort']>, SQL> = { 103 const orders: Record<NonNullable<ListingsFilters['sort']>, SQL> = {
104 newest: sql`l.first_seen_at DESC`, 104 newest: sql`l.first_seen_at DESC`,
105 price_asc: sql`l.price_usd ASC NULLS LAST`, 105 price_asc: sql`l.price_usd ASC NULLS LAST`,
106 price_desc: sql`l.price_usd DESC NULLS LAST`, 106 price_desc: sql`l.price_usd DESC NULLS LAST`,
107− discount: sql`l.discount_to_riv DESC NULLS LAST`, 107+ discount: sql`l.discount_to_riv ASC NULLS LAST`, // most below RIV first
108 ending: sql`l.ends_at ASC NULLS LAST`, 108 ending: sql`l.ends_at ASC NULLS LAST`,
109 }; 109 };
110 const r = await rows<Record<string, unknown>>(sql` 110 const r = await rows<Record<string, unknown>>(sql`
added packages/database/migrations/0006_skinny_kulan_gath.sql +13 −0
@@ -0,0 +1,13 @@
1+DROP INDEX "ai_quotas_uq";--> statement-breakpoint
2+-- collapse duplicate daily counters created by the former non-atomic read-then-write before the key becomes unique
3+DELETE FROM "ai_quotas" a USING "ai_quotas" b WHERE a.key = b.key AND a.feature = b.feature AND a.date = b.date AND a.ctid < b.ctid;--> statement-breakpoint
4+CREATE INDEX "asset_stats_change_7d_idx" ON "asset_stats" USING btree ("change_7d");--> statement-breakpoint
5+CREATE INDEX "asset_stats_change_30d_idx" ON "asset_stats" USING btree ("change_30d");--> statement-breakpoint
6+CREATE INDEX "asset_stats_change_1y_idx" ON "asset_stats" USING btree ("change_1y");--> statement-breakpoint
7+CREATE INDEX "asset_stats_opportunity_idx" ON "asset_stats" USING btree ("value_opportunity") WHERE value_opportunity is not null;--> statement-breakpoint
8+CREATE INDEX "asset_stats_sales_30d_idx" ON "asset_stats" USING btree ("sales_30d");--> statement-breakpoint
9+CREATE INDEX "variant_stats_asset_idx" ON "variant_stats" USING btree ("asset_id");--> statement-breakpoint
10+CREATE INDEX "listings_variant_avail_idx" ON "listings" USING btree ("variant_id","availability");--> statement-breakpoint
11+CREATE INDEX "listings_discount_idx" ON "listings" USING btree ("discount_to_riv") WHERE discount_to_riv is not null and availability = 'available';--> statement-breakpoint
12+CREATE INDEX "sales_valid_date_idx" ON "sales" USING btree ("sale_date") WHERE status = 'valid';--> statement-breakpoint
13+CREATE UNIQUE INDEX "ai_quotas_uq" ON "ai_quotas" USING btree ("key","feature","date");
\ No newline at end of file
added packages/database/migrations/meta/0006_snapshot.json +9035 −0
@@ -0,0 +1,9035 @@
1+{
2+ "id": "179d4792-c6f0-4fb6-b655-fb8cf497ddda",
3+ "prevId": "c11ee010-c4bb-44df-aeea-a93ad2c1d1bb",
4+ "version": "7",
5+ "dialect": "postgresql",
6+ "tables": {
7+ "public.brands": {
8+ "name": "brands",
9+ "schema": "",
10+ "columns": {
11+ "slug": {
12+ "name": "slug",
13+ "type": "text",
14+ "primaryKey": true,
15+ "notNull": true
16+ },
17+ "name": {
18+ "name": "name",
19+ "type": "text",
20+ "primaryKey": false,
21+ "notNull": true
22+ },
23+ "category_slugs": {
24+ "name": "category_slugs",
25+ "type": "text[]",
26+ "primaryKey": false,
27+ "notNull": true,
28+ "default": "'{}'::text[]"
29+ },
30+ "country": {
31+ "name": "country",
32+ "type": "text",
33+ "primaryKey": false,
34+ "notNull": false
35+ },
36+ "founded_year": {
37+ "name": "founded_year",
38+ "type": "integer",
39+ "primaryKey": false,
40+ "notNull": false
41+ },
42+ "metadata": {
43+ "name": "metadata",
44+ "type": "jsonb",
45+ "primaryKey": false,
46+ "notNull": true,
47+ "default": "'{}'::jsonb"
48+ },
49+ "created_at": {
50+ "name": "created_at",
51+ "type": "timestamp with time zone",
52+ "primaryKey": false,
53+ "notNull": true,
54+ "default": "now()"
55+ }
56+ },
57+ "indexes": {
58+ "brands_name_idx": {
59+ "name": "brands_name_idx",
60+ "columns": [
61+ {
62+ "expression": "name",
63+ "isExpression": false,
64+ "asc": true,
65+ "nulls": "last"
66+ }
67+ ],
68+ "isUnique": false,
69+ "concurrently": false,
70+ "method": "btree",
71+ "with": {}
72+ }
73+ },
74+ "foreignKeys": {},
75+ "compositePrimaryKeys": {},
76+ "uniqueConstraints": {},
77+ "policies": {},
78+ "checkConstraints": {},
79+ "isRLSEnabled": false
80+ },
81+ "public.categories": {
82+ "name": "categories",
83+ "schema": "",
84+ "columns": {
85+ "slug": {
86+ "name": "slug",
87+ "type": "text",
88+ "primaryKey": true,
89+ "notNull": true
90+ },
91+ "parent_slug": {
92+ "name": "parent_slug",
93+ "type": "text",
94+ "primaryKey": false,
95+ "notNull": false
96+ },
97+ "family_slug": {
98+ "name": "family_slug",
99+ "type": "text",
100+ "primaryKey": false,
101+ "notNull": true
102+ },
103+ "name": {
104+ "name": "name",
105+ "type": "text",
106+ "primaryKey": false,
107+ "notNull": true
108+ },
109+ "short_name": {
110+ "name": "short_name",
111+ "type": "text",
112+ "primaryKey": false,
113+ "notNull": false
114+ },
115+ "description": {
116+ "name": "description",
117+ "type": "text",
118+ "primaryKey": false,
119+ "notNull": false
120+ },
121+ "level": {
122+ "name": "level",
123+ "type": "integer",
124+ "primaryKey": false,
125+ "notNull": true,
126+ "default": 0
127+ },
128+ "phase": {
129+ "name": "phase",
130+ "type": "integer",
131+ "primaryKey": false,
132+ "notNull": true,
133+ "default": 3
134+ },
135+ "active": {
136+ "name": "active",
137+ "type": "boolean",
138+ "primaryKey": false,
139+ "notNull": true,
140+ "default": true
141+ },
142+ "condition_scale": {
143+ "name": "condition_scale",
144+ "type": "text",
145+ "primaryKey": false,
146+ "notNull": false
147+ },
148+ "graders": {
149+ "name": "graders",
150+ "type": "text[]",
151+ "primaryKey": false,
152+ "notNull": true,
153+ "default": "'{}'::text[]"
154+ },
155+ "index_ticker": {
156+ "name": "index_ticker",
157+ "type": "text",
158+ "primaryKey": false,
159+ "notNull": false
160+ },
161+ "attribute_schema": {
162+ "name": "attribute_schema",
163+ "type": "jsonb",
164+ "primaryKey": false,
165+ "notNull": true,
166+ "default": "'{}'::jsonb"
167+ },
168+ "sort_order": {
169+ "name": "sort_order",
170+ "type": "integer",
171+ "primaryKey": false,
172+ "notNull": true,
173+ "default": 0
174+ },
175+ "compliance_flags": {
176+ "name": "compliance_flags",
177+ "type": "text[]",
178+ "primaryKey": false,
179+ "notNull": true,
180+ "default": "'{}'::text[]"
181+ },
182+ "icon": {
183+ "name": "icon",
184+ "type": "text",
185+ "primaryKey": false,
186+ "notNull": false
187+ },
188+ "created_at": {
189+ "name": "created_at",
190+ "type": "timestamp with time zone",
191+ "primaryKey": false,
192+ "notNull": true,
193+ "default": "now()"
194+ },
195+ "updated_at": {
196+ "name": "updated_at",
197+ "type": "timestamp with time zone",
198+ "primaryKey": false,
199+ "notNull": true,
200+ "default": "now()"
201+ }
202+ },
203+ "indexes": {
204+ "categories_parent_idx": {
205+ "name": "categories_parent_idx",
206+ "columns": [
207+ {
208+ "expression": "parent_slug",
209+ "isExpression": false,
210+ "asc": true,
211+ "nulls": "last"
212+ }
213+ ],
214+ "isUnique": false,
215+ "concurrently": false,
216+ "method": "btree",
217+ "with": {}
218+ },
219+ "categories_family_idx": {
220+ "name": "categories_family_idx",
221+ "columns": [
222+ {
223+ "expression": "family_slug",
224+ "isExpression": false,
225+ "asc": true,
226+ "nulls": "last"
227+ }
228+ ],
229+ "isUnique": false,
230+ "concurrently": false,
231+ "method": "btree",
232+ "with": {}
233+ }
234+ },
235+ "foreignKeys": {},
236+ "compositePrimaryKeys": {},
237+ "uniqueConstraints": {},
238+ "policies": {},
239+ "checkConstraints": {},
240+ "isRLSEnabled": false
241+ },
242+ "public.franchises": {
243+ "name": "franchises",
244+ "schema": "",
245+ "columns": {
246+ "slug": {
247+ "name": "slug",
248+ "type": "text",
249+ "primaryKey": true,
250+ "notNull": true
251+ },
252+ "name": {
253+ "name": "name",
254+ "type": "text",
255+ "primaryKey": false,
256+ "notNull": true
257+ },
258+ "owner": {
259+ "name": "owner",
260+ "type": "text",
261+ "primaryKey": false,
262+ "notNull": false
263+ },
264+ "category_slugs": {
265+ "name": "category_slugs",
266+ "type": "text[]",
267+ "primaryKey": false,
268+ "notNull": true,
269+ "default": "'{}'::text[]"
270+ },
271+ "metadata": {
272+ "name": "metadata",
273+ "type": "jsonb",
274+ "primaryKey": false,
275+ "notNull": true,
276+ "default": "'{}'::jsonb"
277+ },
278+ "created_at": {
279+ "name": "created_at",
280+ "type": "timestamp with time zone",
281+ "primaryKey": false,
282+ "notNull": true,
283+ "default": "now()"
284+ }
285+ },
286+ "indexes": {},
287+ "foreignKeys": {},
288+ "compositePrimaryKeys": {},
289+ "uniqueConstraints": {},
290+ "policies": {},
291+ "checkConstraints": {},
292+ "isRLSEnabled": false
293+ },
294+ "public.graders": {
295+ "name": "graders",
296+ "schema": "",
297+ "columns": {
298+ "slug": {
299+ "name": "slug",
300+ "type": "text",
301+ "primaryKey": true,
302+ "notNull": true
303+ },
304+ "name": {
305+ "name": "name",
306+ "type": "text",
307+ "primaryKey": false,
308+ "notNull": true
309+ },
310+ "category_slugs": {
311+ "name": "category_slugs",
312+ "type": "text[]",
313+ "primaryKey": false,
314+ "notNull": true,
315+ "default": "'{}'::text[]"
316+ },
317+ "scale": {
318+ "name": "scale",
319+ "type": "jsonb",
320+ "primaryKey": false,
321+ "notNull": true,
322+ "default": "'{}'::jsonb"
323+ },
324+ "population_url": {
325+ "name": "population_url",
326+ "type": "text",
327+ "primaryKey": false,
328+ "notNull": false
329+ },
330+ "verify_url": {
331+ "name": "verify_url",
332+ "type": "text",
333+ "primaryKey": false,
334+ "notNull": false
335+ },
336+ "active": {
337+ "name": "active",
338+ "type": "boolean",
339+ "primaryKey": false,
340+ "notNull": true,
341+ "default": true
342+ }
343+ },
344+ "indexes": {},
345+ "foreignKeys": {},
346+ "compositePrimaryKeys": {},
347+ "uniqueConstraints": {},
348+ "policies": {},
349+ "checkConstraints": {},
350+ "isRLSEnabled": false
351+ },
352+ "public.sets": {
353+ "name": "sets",
354+ "schema": "",
355+ "columns": {
356+ "slug": {
357+ "name": "slug",
358+ "type": "text",
359+ "primaryKey": true,
360+ "notNull": true
361+ },
362+ "name": {
363+ "name": "name",
364+ "type": "text",
365+ "primaryKey": false,
366+ "notNull": true
367+ },
368+ "code": {
369+ "name": "code",
370+ "type": "text",
371+ "primaryKey": false,
372+ "notNull": false
373+ },
374+ "category_slug": {
375+ "name": "category_slug",
376+ "type": "text",
377+ "primaryKey": false,
378+ "notNull": true
379+ },
380+ "franchise_slug": {
381+ "name": "franchise_slug",
382+ "type": "text",
383+ "primaryKey": false,
384+ "notNull": false
385+ },
386+ "brand_slug": {
387+ "name": "brand_slug",
388+ "type": "text",
389+ "primaryKey": false,
390+ "notNull": false
391+ },
392+ "release_year": {
393+ "name": "release_year",
394+ "type": "integer",
395+ "primaryKey": false,
396+ "notNull": false
397+ },
398+ "release_date": {
399+ "name": "release_date",
400+ "type": "text",
401+ "primaryKey": false,
402+ "notNull": false
403+ },
404+ "language": {
405+ "name": "language",
406+ "type": "text",
407+ "primaryKey": false,
408+ "notNull": false
409+ },
410+ "total_items": {
411+ "name": "total_items",
412+ "type": "integer",
413+ "primaryKey": false,
414+ "notNull": false
415+ },
416+ "identifiers": {
417+ "name": "identifiers",
418+ "type": "jsonb",
419+ "primaryKey": false,
420+ "notNull": true,
421+ "default": "'{}'::jsonb"
422+ },
423+ "metadata": {
424+ "name": "metadata",
425+ "type": "jsonb",
426+ "primaryKey": false,
427+ "notNull": true,
428+ "default": "'{}'::jsonb"
429+ },
430+ "created_at": {
431+ "name": "created_at",
432+ "type": "timestamp with time zone",
433+ "primaryKey": false,
434+ "notNull": true,
435+ "default": "now()"
436+ }
437+ },
438+ "indexes": {
439+ "sets_category_idx": {
440+ "name": "sets_category_idx",
441+ "columns": [
442+ {
443+ "expression": "category_slug",
444+ "isExpression": false,
445+ "asc": true,
446+ "nulls": "last"
447+ }
448+ ],
449+ "isUnique": false,
450+ "concurrently": false,
451+ "method": "btree",
452+ "with": {}
453+ },
454+ "sets_category_code_uq": {
455+ "name": "sets_category_code_uq",
456+ "columns": [
457+ {
458+ "expression": "category_slug",
459+ "isExpression": false,
460+ "asc": true,
461+ "nulls": "last"
462+ },
463+ {
464+ "expression": "code",
465+ "isExpression": false,
466+ "asc": true,
467+ "nulls": "last"
468+ }
469+ ],
470+ "isUnique": true,
471+ "concurrently": false,
472+ "method": "btree",
473+ "with": {}
474+ }
475+ },
476+ "foreignKeys": {},
477+ "compositePrimaryKeys": {},
478+ "uniqueConstraints": {},
479+ "policies": {},
480+ "checkConstraints": {},
481+ "isRLSEnabled": false
482+ },
483+ "public.taxonomy_proposals": {
484+ "name": "taxonomy_proposals",
485+ "schema": "",
486+ "columns": {
487+ "id": {
488+ "name": "id",
489+ "type": "text",
490+ "primaryKey": true,
491+ "notNull": true
492+ },
493+ "proposed_slug": {
494+ "name": "proposed_slug",
495+ "type": "text",
496+ "primaryKey": false,
497+ "notNull": true
498+ },
499+ "name": {
500+ "name": "name",
501+ "type": "text",
502+ "primaryKey": false,
503+ "notNull": true
504+ },
505+ "parent_slug": {
506+ "name": "parent_slug",
507+ "type": "text",
508+ "primaryKey": false,
509+ "notNull": false
510+ },
511+ "evidence": {
512+ "name": "evidence",
513+ "type": "jsonb",
514+ "primaryKey": false,
515+ "notNull": true,
516+ "default": "'{}'::jsonb"
517+ },
518+ "volume_estimate": {
519+ "name": "volume_estimate",
520+ "type": "integer",
521+ "primaryKey": false,
522+ "notNull": false
523+ },
524+ "status": {
525+ "name": "status",
526+ "type": "text",
527+ "primaryKey": false,
528+ "notNull": true,
529+ "default": "'pending'"
530+ },
531+ "decided_by": {
532+ "name": "decided_by",
533+ "type": "text",
534+ "primaryKey": false,
535+ "notNull": false
536+ },
537+ "decided_at": {
538+ "name": "decided_at",
539+ "type": "text",
540+ "primaryKey": false,
541+ "notNull": false
542+ },
543+ "created_at": {
544+ "name": "created_at",
545+ "type": "timestamp with time zone",
546+ "primaryKey": false,
547+ "notNull": true,
548+ "default": "now()"
549+ }
550+ },
551+ "indexes": {},
552+ "foreignKeys": {},
553+ "compositePrimaryKeys": {},
554+ "uniqueConstraints": {},
555+ "policies": {},
556+ "checkConstraints": {},
557+ "isRLSEnabled": false
558+ },
559+ "public.certificate_sightings": {
560+ "name": "certificate_sightings",
561+ "schema": "",
562+ "columns": {
563+ "id": {
564+ "name": "id",
565+ "type": "text",
566+ "primaryKey": true,
567+ "notNull": true
568+ },
569+ "certificate_id": {
570+ "name": "certificate_id",
571+ "type": "text",
572+ "primaryKey": false,
573+ "notNull": true
574+ },
575+ "kind": {
576+ "name": "kind",
577+ "type": "text",
578+ "primaryKey": false,
579+ "notNull": true
580+ },
581+ "target_id": {
582+ "name": "target_id",
583+ "type": "text",
584+ "primaryKey": false,
585+ "notNull": false
586+ },
587+ "source_id": {
588+ "name": "source_id",
589+ "type": "text",
590+ "primaryKey": false,
591+ "notNull": true
592+ },
593+ "connector_id": {
594+ "name": "connector_id",
595+ "type": "text",
596+ "primaryKey": false,
597+ "notNull": false
598+ },
599+ "source_url": {
600+ "name": "source_url",
601+ "type": "text",
602+ "primaryKey": false,
603+ "notNull": false
604+ },
605+ "price_usd": {
606+ "name": "price_usd",
607+ "type": "numeric(18, 4)",
608+ "primaryKey": false,
609+ "notNull": false
610+ },
611+ "currency": {
612+ "name": "currency",
613+ "type": "text",
614+ "primaryKey": false,
615+ "notNull": false
616+ },
617+ "price": {
618+ "name": "price",
619+ "type": "numeric(18, 4)",
620+ "primaryKey": false,
621+ "notNull": false
622+ },
623+ "observed_at": {
624+ "name": "observed_at",
625+ "type": "timestamp with time zone",
626+ "primaryKey": false,
627+ "notNull": true
628+ },
629+ "created_at": {
630+ "name": "created_at",
631+ "type": "timestamp with time zone",
632+ "primaryKey": false,
633+ "notNull": true,
634+ "default": "now()"
635+ }
636+ },
637+ "indexes": {
638+ "certificate_sightings_cert_idx": {
639+ "name": "certificate_sightings_cert_idx",
640+ "columns": [
641+ {
642+ "expression": "certificate_id",
643+ "isExpression": false,
644+ "asc": true,
645+ "nulls": "last"
646+ },
647+ {
648+ "expression": "observed_at",
649+ "isExpression": false,
650+ "asc": true,
651+ "nulls": "last"
652+ }
653+ ],
654+ "isUnique": false,
655+ "concurrently": false,
656+ "method": "btree",
657+ "with": {}
658+ },
659+ "certificate_sightings_uq": {
660+ "name": "certificate_sightings_uq",
661+ "columns": [
662+ {
663+ "expression": "certificate_id",
664+ "isExpression": false,
665+ "asc": true,
666+ "nulls": "last"
667+ },
668+ {
669+ "expression": "kind",
670+ "isExpression": false,
671+ "asc": true,
672+ "nulls": "last"
673+ },
674+ {
675+ "expression": "target_id",
676+ "isExpression": false,
677+ "asc": true,
678+ "nulls": "last"
679+ }
680+ ],
681+ "isUnique": true,
682+ "concurrently": false,
683+ "method": "btree",
684+ "with": {}
685+ }
686+ },
687+ "foreignKeys": {},
688+ "compositePrimaryKeys": {},
689+ "uniqueConstraints": {},
690+ "policies": {},
691+ "checkConstraints": {},
692+ "isRLSEnabled": false
693+ },
694+ "public.certificates": {
695+ "name": "certificates",
696+ "schema": "",
697+ "columns": {
698+ "id": {
699+ "name": "id",
700+ "type": "text",
701+ "primaryKey": true,
702+ "notNull": true
703+ },
704+ "grader": {
705+ "name": "grader",
706+ "type": "text",
707+ "primaryKey": false,
708+ "notNull": true
709+ },
710+ "cert_number": {
711+ "name": "cert_number",
712+ "type": "text",
713+ "primaryKey": false,
714+ "notNull": true
715+ },
716+ "asset_id": {
717+ "name": "asset_id",
718+ "type": "text",
719+ "primaryKey": false,
720+ "notNull": false
721+ },
722+ "variant_id": {
723+ "name": "variant_id",
724+ "type": "text",
725+ "primaryKey": false,
726+ "notNull": false
727+ },
728+ "grade": {
729+ "name": "grade",
730+ "type": "text",
731+ "primaryKey": false,
732+ "notNull": false
733+ },
734+ "qualifier": {
735+ "name": "qualifier",
736+ "type": "text",
737+ "primaryKey": false,
738+ "notNull": false
739+ },
740+ "verify_url": {
741+ "name": "verify_url",
742+ "type": "text",
743+ "primaryKey": false,
744+ "notNull": false
745+ },
746+ "first_seen_at": {
747+ "name": "first_seen_at",
748+ "type": "timestamp with time zone",
749+ "primaryKey": false,
750+ "notNull": true
751+ },
752+ "last_seen_at": {
753+ "name": "last_seen_at",
754+ "type": "timestamp with time zone",
755+ "primaryKey": false,
756+ "notNull": true
757+ },
758+ "sightings": {
759+ "name": "sightings",
760+ "type": "integer",
761+ "primaryKey": false,
762+ "notNull": true,
763+ "default": 0
764+ },
765+ "source_ids": {
766+ "name": "source_ids",
767+ "type": "text[]",
768+ "primaryKey": false,
769+ "notNull": true,
770+ "default": "'{}'::text[]"
771+ },
772+ "last_source_url": {
773+ "name": "last_source_url",
774+ "type": "text",
775+ "primaryKey": false,
776+ "notNull": false
777+ },
778+ "last_price_usd": {
779+ "name": "last_price_usd",
780+ "type": "numeric(18, 4)",
781+ "primaryKey": false,
782+ "notNull": false
783+ },
784+ "verified_at": {
785+ "name": "verified_at",
786+ "type": "timestamp with time zone",
787+ "primaryKey": false,
788+ "notNull": false
789+ },
790+ "verification": {
791+ "name": "verification",
792+ "type": "jsonb",
793+ "primaryKey": false,
794+ "notNull": true,
795+ "default": "'{}'::jsonb"
796+ },
797+ "created_at": {
798+ "name": "created_at",
799+ "type": "timestamp with time zone",
800+ "primaryKey": false,
801+ "notNull": true,
802+ "default": "now()"
803+ }
804+ },
805+ "indexes": {
806+ "certificates_grader_cert_uq": {
807+ "name": "certificates_grader_cert_uq",
808+ "columns": [
809+ {
810+ "expression": "grader",
811+ "isExpression": false,
812+ "asc": true,
813+ "nulls": "last"
814+ },
815+ {
816+ "expression": "cert_number",
817+ "isExpression": false,
818+ "asc": true,
819+ "nulls": "last"
820+ }
821+ ],
822+ "isUnique": true,
823+ "concurrently": false,
824+ "method": "btree",
825+ "with": {}
826+ },
827+ "certificates_asset_idx": {
828+ "name": "certificates_asset_idx",
829+ "columns": [
830+ {
831+ "expression": "asset_id",
832+ "isExpression": false,
833+ "asc": true,
834+ "nulls": "last"
835+ }
836+ ],
837+ "isUnique": false,
838+ "concurrently": false,
839+ "method": "btree",
840+ "with": {}
841+ },
842+ "certificates_last_seen_idx": {
843+ "name": "certificates_last_seen_idx",
844+ "columns": [
845+ {
846+ "expression": "last_seen_at",
847+ "isExpression": false,
848+ "asc": true,
849+ "nulls": "last"
850+ }
851+ ],
852+ "isUnique": false,
853+ "concurrently": false,
854+ "method": "btree",
855+ "with": {}
856+ }
857+ },
858+ "foreignKeys": {},
859+ "compositePrimaryKeys": {},
860+ "uniqueConstraints": {},
861+ "policies": {},
862+ "checkConstraints": {},
863+ "isRLSEnabled": false
864+ },
865+ "public.connector_backfills": {
866+ "name": "connector_backfills",
867+ "schema": "",
868+ "columns": {
869+ "id": {
870+ "name": "id",
871+ "type": "text",
872+ "primaryKey": true,
873+ "notNull": true
874+ },
875+ "connector_id": {
876+ "name": "connector_id",
877+ "type": "text",
878+ "primaryKey": false,
879+ "notNull": true
880+ },
881+ "status": {
882+ "name": "status",
883+ "type": "text",
884+ "primaryKey": false,
885+ "notNull": true,
886+ "default": "'running'"
887+ },
888+ "started_at": {
889+ "name": "started_at",
890+ "type": "timestamp with time zone",
891+ "primaryKey": false,
892+ "notNull": true
893+ },
894+ "updated_at": {
895+ "name": "updated_at",
896+ "type": "timestamp with time zone",
897+ "primaryKey": false,
898+ "notNull": true,
899+ "default": "now()"
900+ },
901+ "finished_at": {
902+ "name": "finished_at",
903+ "type": "timestamp with time zone",
904+ "primaryKey": false,
905+ "notNull": false
906+ },
907+ "backfill_start_date": {
908+ "name": "backfill_start_date",
909+ "type": "date",
910+ "primaryKey": false,
911+ "notNull": false
912+ },
913+ "backfill_end_date": {
914+ "name": "backfill_end_date",
915+ "type": "date",
916+ "primaryKey": false,
917+ "notNull": false
918+ },
919+ "reached_date": {
920+ "name": "reached_date",
921+ "type": "date",
922+ "primaryKey": false,
923+ "notNull": false
924+ },
925+ "pages_processed": {
926+ "name": "pages_processed",
927+ "type": "integer",
928+ "primaryKey": false,
929+ "notNull": true,
930+ "default": 0
931+ },
932+ "total_pages": {
933+ "name": "total_pages",
934+ "type": "integer",
935+ "primaryKey": false,
936+ "notNull": false
937+ },
938+ "items_processed": {
939+ "name": "items_processed",
940+ "type": "integer",
941+ "primaryKey": false,
942+ "notNull": true,
943+ "default": 0
944+ },
945+ "last_cursor": {
946+ "name": "last_cursor",
947+ "type": "jsonb",
948+ "primaryKey": false,
949+ "notNull": true,
950+ "default": "'{}'::jsonb"
951+ },
952+ "last_successful_page": {
953+ "name": "last_successful_page",
954+ "type": "integer",
955+ "primaryKey": false,
956+ "notNull": false
957+ },
958+ "runs": {
959+ "name": "runs",
960+ "type": "integer",
961+ "primaryKey": false,
962+ "notNull": true,
963+ "default": 0
964+ },
965+ "errors": {
966+ "name": "errors",
967+ "type": "integer",
968+ "primaryKey": false,
969+ "notNull": true,
970+ "default": 0
971+ },
972+ "retry_count": {
973+ "name": "retry_count",
974+ "type": "integer",
975+ "primaryKey": false,
976+ "notNull": true,
977+ "default": 0
978+ },
979+ "last_error": {
980+ "name": "last_error",
981+ "type": "text",
982+ "primaryKey": false,
983+ "notNull": false
984+ },
985+ "percent": {
986+ "name": "percent",
987+ "type": "real",
988+ "primaryKey": false,
989+ "notNull": false
990+ }
991+ },
992+ "indexes": {
993+ "connector_backfills_connector_idx": {
994+ "name": "connector_backfills_connector_idx",
995+ "columns": [
996+ {
997+ "expression": "connector_id",
998+ "isExpression": false,
999+ "asc": true,
1000+ "nulls": "last"
1001+ },
1002+ {
1003+ "expression": "status",
1004+ "isExpression": false,
1005+ "asc": true,
1006+ "nulls": "last"
1007+ }
1008+ ],
1009+ "isUnique": false,
1010+ "concurrently": false,
1011+ "method": "btree",
1012+ "with": {}
1013+ }
1014+ },
1015+ "foreignKeys": {},
1016+ "compositePrimaryKeys": {},
1017+ "uniqueConstraints": {},
1018+ "policies": {},
1019+ "checkConstraints": {},
1020+ "isRLSEnabled": false
1021+ },
1022+ "public.connector_field_stats": {
1023+ "name": "connector_field_stats",
1024+ "schema": "",
1025+ "columns": {
1026+ "connector_id": {
1027+ "name": "connector_id",
1028+ "type": "text",
1029+ "primaryKey": false,
1030+ "notNull": true
1031+ },
1032+ "day": {
1033+ "name": "day",
1034+ "type": "date",
1035+ "primaryKey": false,
1036+ "notNull": true
1037+ },
1038+ "field": {
1039+ "name": "field",
1040+ "type": "text",
1041+ "primaryKey": false,
1042+ "notNull": true
1043+ },
1044+ "total": {
1045+ "name": "total",
1046+ "type": "integer",
1047+ "primaryKey": false,
1048+ "notNull": true,
1049+ "default": 0
1050+ },
1051+ "nulls": {
1052+ "name": "nulls",
1053+ "type": "integer",
1054+ "primaryKey": false,
1055+ "notNull": true,
1056+ "default": 0
1057+ }
1058+ },
1059+ "indexes": {},
1060+ "foreignKeys": {},
1061+ "compositePrimaryKeys": {
1062+ "connector_field_stats_connector_id_day_field_pk": {
1063+ "name": "connector_field_stats_connector_id_day_field_pk",
1064+ "columns": [
1065+ "connector_id",
1066+ "day",
1067+ "field"
1068+ ]
1069+ }
1070+ },
1071+ "uniqueConstraints": {},
1072+ "policies": {},
1073+ "checkConstraints": {},
1074+ "isRLSEnabled": false
1075+ },
1076+ "public.connector_health": {
1077+ "name": "connector_health",
1078+ "schema": "",
1079+ "columns": {
1080+ "connector_id": {
1081+ "name": "connector_id",
1082+ "type": "text",
1083+ "primaryKey": true,
1084+ "notNull": true
1085+ },
1086+ "computed_at": {
1087+ "name": "computed_at",
1088+ "type": "timestamp with time zone",
1089+ "primaryKey": false,
1090+ "notNull": true
1091+ },
1092+ "status": {
1093+ "name": "status",
1094+ "type": "text",
1095+ "primaryKey": false,
1096+ "notNull": true
1097+ },
1098+ "health": {
1099+ "name": "health",
1100+ "type": "jsonb",
1101+ "primaryKey": false,
1102+ "notNull": true,
1103+ "default": "'{}'::jsonb"
1104+ }
1105+ },
1106+ "indexes": {},
1107+ "foreignKeys": {},
1108+ "compositePrimaryKeys": {},
1109+ "uniqueConstraints": {},
1110+ "policies": {},
1111+ "checkConstraints": {},
1112+ "isRLSEnabled": false
1113+ },
1114+ "public.connector_runs": {
1115+ "name": "connector_runs",
1116+ "schema": "",
1117+ "columns": {
1118+ "id": {
1119+ "name": "id",
1120+ "type": "text",
1121+ "primaryKey": true,
1122+ "notNull": true
1123+ },
1124+ "connector_id": {
1125+ "name": "connector_id",
1126+ "type": "text",
1127+ "primaryKey": false,
1128+ "notNull": true
1129+ },
1130+ "trigger": {
1131+ "name": "trigger",
1132+ "type": "text",
1133+ "primaryKey": false,
1134+ "notNull": true,
1135+ "default": "'schedule'"
1136+ },
1137+ "started_at": {
1138+ "name": "started_at",
1139+ "type": "timestamp with time zone",
1140+ "primaryKey": false,
1141+ "notNull": true
1142+ },
1143+ "finished_at": {
1144+ "name": "finished_at",
1145+ "type": "timestamp with time zone",
1146+ "primaryKey": false,
1147+ "notNull": false
1148+ },
1149+ "status": {
1150+ "name": "status",
1151+ "type": "text",
1152+ "primaryKey": false,
1153+ "notNull": true,
1154+ "default": "'running'"
1155+ },
1156+ "pages_attempted": {
1157+ "name": "pages_attempted",
1158+ "type": "integer",
1159+ "primaryKey": false,
1160+ "notNull": true,
1161+ "default": 0
1162+ },
1163+ "pages_success": {
1164+ "name": "pages_success",
1165+ "type": "integer",
1166+ "primaryKey": false,
1167+ "notNull": true,
1168+ "default": 0
1169+ },
1170+ "records_raw": {
1171+ "name": "records_raw",
1172+ "type": "integer",
1173+ "primaryKey": false,
1174+ "notNull": true,
1175+ "default": 0
1176+ },
1177+ "records_normalized": {
1178+ "name": "records_normalized",
1179+ "type": "integer",
1180+ "primaryKey": false,
1181+ "notNull": true,
1182+ "default": 0
1183+ },
1184+ "records_duplicate": {
1185+ "name": "records_duplicate",
1186+ "type": "integer",
1187+ "primaryKey": false,
1188+ "notNull": true,
1189+ "default": 0
1190+ },
1191+ "records_rejected": {
1192+ "name": "records_rejected",
1193+ "type": "integer",
1194+ "primaryKey": false,
1195+ "notNull": true,
1196+ "default": 0
1197+ },
1198+ "engine_stats": {
1199+ "name": "engine_stats",
1200+ "type": "jsonb",
1201+ "primaryKey": false,
1202+ "notNull": true,
1203+ "default": "'{}'::jsonb"
1204+ },
1205+ "anomalies": {
1206+ "name": "anomalies",
1207+ "type": "jsonb",
1208+ "primaryKey": false,
1209+ "notNull": true,
1210+ "default": "'[]'::jsonb"
1211+ },
1212+ "error": {
1213+ "name": "error",
1214+ "type": "text",
1215+ "primaryKey": false,
1216+ "notNull": false
1217+ },
1218+ "cost_credits": {
1219+ "name": "cost_credits",
1220+ "type": "real",
1221+ "primaryKey": false,
1222+ "notNull": true,
1223+ "default": 0
1224+ },
1225+ "cost_usd_est": {
1226+ "name": "cost_usd_est",
1227+ "type": "real",
1228+ "primaryKey": false,
1229+ "notNull": true,
1230+ "default": 0
1231+ },
1232+ "cursor": {
1233+ "name": "cursor",
1234+ "type": "jsonb",
1235+ "primaryKey": false,
1236+ "notNull": true,
1237+ "default": "'{}'::jsonb"
1238+ }
1239+ },
1240+ "indexes": {
1241+ "connector_runs_connector_started_idx": {
1242+ "name": "connector_runs_connector_started_idx",
1243+ "columns": [
1244+ {
1245+ "expression": "connector_id",
1246+ "isExpression": false,
1247+ "asc": true,
1248+ "nulls": "last"
1249+ },
1250+ {
1251+ "expression": "started_at",
1252+ "isExpression": false,
1253+ "asc": true,
1254+ "nulls": "last"
1255+ }
1256+ ],
1257+ "isUnique": false,
1258+ "concurrently": false,
1259+ "method": "btree",
1260+ "with": {}
1261+ }
1262+ },
1263+ "foreignKeys": {},
1264+ "compositePrimaryKeys": {},
1265+ "uniqueConstraints": {},
1266+ "policies": {},
1267+ "checkConstraints": {},
1268+ "isRLSEnabled": false
1269+ },
1270+ "public.connectors": {
1271+ "name": "connectors",
1272+ "schema": "",
1273+ "columns": {
1274+ "id": {
1275+ "name": "id",
1276+ "type": "text",
1277+ "primaryKey": true,
1278+ "notNull": true
1279+ },
1280+ "source_id": {
1281+ "name": "source_id",
1282+ "type": "text",
1283+ "primaryKey": false,
1284+ "notNull": true
1285+ },
1286+ "display_name": {
1287+ "name": "display_name",
1288+ "type": "text",
1289+ "primaryKey": false,
1290+ "notNull": true
1291+ },
1292+ "engine_priority": {
1293+ "name": "engine_priority",
1294+ "type": "text[]",
1295+ "primaryKey": false,
1296+ "notNull": true,
1297+ "default": "'{}'::text[]"
1298+ },
1299+ "categories": {
1300+ "name": "categories",
1301+ "type": "text[]",
1302+ "primaryKey": false,
1303+ "notNull": true,
1304+ "default": "'{}'::text[]"
1305+ },
1306+ "regions": {
1307+ "name": "regions",
1308+ "type": "text[]",
1309+ "primaryKey": false,
1310+ "notNull": true,
1311+ "default": "'{}'::text[]"
1312+ },
1313+ "languages": {
1314+ "name": "languages",
1315+ "type": "text[]",
1316+ "primaryKey": false,
1317+ "notNull": true,
1318+ "default": "'{}'::text[]"
1319+ },
1320+ "currency": {
1321+ "name": "currency",
1322+ "type": "text[]",
1323+ "primaryKey": false,
1324+ "notNull": true,
1325+ "default": "'{}'::text[]"
1326+ },
1327+ "supports_listings": {
1328+ "name": "supports_listings",
1329+ "type": "boolean",
1330+ "primaryKey": false,
1331+ "notNull": true,
1332+ "default": false
1333+ },
1334+ "supports_sold": {
1335+ "name": "supports_sold",
1336+ "type": "boolean",
1337+ "primaryKey": false,
1338+ "notNull": true,
1339+ "default": false
1340+ },
1341+ "supports_auctions": {
1342+ "name": "supports_auctions",
1343+ "type": "boolean",
1344+ "primaryKey": false,
1345+ "notNull": true,
1346+ "default": false
1347+ },
1348+ "supports_images": {
1349+ "name": "supports_images",
1350+ "type": "boolean",
1351+ "primaryKey": false,
1352+ "notNull": true,
1353+ "default": true
1354+ },
1355+ "supports_catalog": {
1356+ "name": "supports_catalog",
1357+ "type": "boolean",
1358+ "primaryKey": false,
1359+ "notNull": true,
1360+ "default": false
1361+ },
1362+ "supports_population": {
1363+ "name": "supports_population",
1364+ "type": "boolean",
1365+ "primaryKey": false,
1366+ "notNull": true,
1367+ "default": false
1368+ },
1369+ "refresh_frequency_minutes": {
1370+ "name": "refresh_frequency_minutes",
1371+ "type": "integer",
1372+ "primaryKey": false,
1373+ "notNull": true,
1374+ "default": 1440
1375+ },
1376+ "priority": {
1377+ "name": "priority",
1378+ "type": "text",
1379+ "primaryKey": false,
1380+ "notNull": true,
1381+ "default": "'medium'"
1382+ },
1383+ "status": {
1384+ "name": "status",
1385+ "type": "text",
1386+ "primaryKey": false,
1387+ "notNull": true,
1388+ "default": "'active'"
1389+ },
1390+ "schema_version": {
1391+ "name": "schema_version",
1392+ "type": "text",
1393+ "primaryKey": false,
1394+ "notNull": true,
1395+ "default": "'1.0'"
1396+ },
1397+ "connector_version": {
1398+ "name": "connector_version",
1399+ "type": "text",
1400+ "primaryKey": false,
1401+ "notNull": true,
1402+ "default": "'1.0.0'"
1403+ },
1404+ "config": {
1405+ "name": "config",
1406+ "type": "jsonb",
1407+ "primaryKey": false,
1408+ "notNull": true,
1409+ "default": "'{}'::jsonb"
1410+ },
1411+ "meta": {
1412+ "name": "meta",
1413+ "type": "jsonb",
1414+ "primaryKey": false,
1415+ "notNull": true,
1416+ "default": "'{}'::jsonb"
1417+ },
1418+ "last_run_at": {
1419+ "name": "last_run_at",
1420+ "type": "timestamp with time zone",
1421+ "primaryKey": false,
1422+ "notNull": false
1423+ },
1424+ "last_success_at": {
1425+ "name": "last_success_at",
1426+ "type": "timestamp with time zone",
1427+ "primaryKey": false,
1428+ "notNull": false
1429+ },
1430+ "next_run_at": {
1431+ "name": "next_run_at",
1432+ "type": "timestamp with time zone",
1433+ "primaryKey": false,
1434+ "notNull": false
1435+ },
1436+ "created_at": {
1437+ "name": "created_at",
1438+ "type": "timestamp with time zone",
1439+ "primaryKey": false,
1440+ "notNull": true,
1441+ "default": "now()"
1442+ },
1443+ "updated_at": {
1444+ "name": "updated_at",
1445+ "type": "timestamp with time zone",
1446+ "primaryKey": false,
1447+ "notNull": true,
1448+ "default": "now()"
1449+ }
1450+ },
1451+ "indexes": {},
1452+ "foreignKeys": {},
1453+ "compositePrimaryKeys": {},
1454+ "uniqueConstraints": {},
1455+ "policies": {},
1456+ "checkConstraints": {},
1457+ "isRLSEnabled": false
1458+ },
1459+ "public.costs": {
1460+ "name": "costs",
1461+ "schema": "",
1462+ "columns": {
1463+ "id": {
1464+ "name": "id",
1465+ "type": "text",
1466+ "primaryKey": true,
1467+ "notNull": true
1468+ },
1469+ "occurred_at": {
1470+ "name": "occurred_at",
1471+ "type": "timestamp with time zone",
1472+ "primaryKey": false,
1473+ "notNull": true
1474+ },
1475+ "kind": {
1476+ "name": "kind",
1477+ "type": "text",
1478+ "primaryKey": false,
1479+ "notNull": true
1480+ },
1481+ "provider": {
1482+ "name": "provider",
1483+ "type": "text",
1484+ "primaryKey": false,
1485+ "notNull": false
1486+ },
1487+ "connector_id": {
1488+ "name": "connector_id",
1489+ "type": "text",
1490+ "primaryKey": false,
1491+ "notNull": false
1492+ },
1493+ "category_slug": {
1494+ "name": "category_slug",
1495+ "type": "text",
1496+ "primaryKey": false,
1497+ "notNull": false
1498+ },
1499+ "endpoint": {
1500+ "name": "endpoint",
1501+ "type": "text",
1502+ "primaryKey": false,
1503+ "notNull": false
1504+ },
1505+ "user_id": {
1506+ "name": "user_id",
1507+ "type": "text",
1508+ "primaryKey": false,
1509+ "notNull": false
1510+ },
1511+ "units": {
1512+ "name": "units",
1513+ "type": "real",
1514+ "primaryKey": false,
1515+ "notNull": true,
1516+ "default": 1
1517+ },
1518+ "credits": {
1519+ "name": "credits",
1520+ "type": "real",
1521+ "primaryKey": false,
1522+ "notNull": true,
1523+ "default": 0
1524+ },
1525+ "usd_est": {
1526+ "name": "usd_est",
1527+ "type": "real",
1528+ "primaryKey": false,
1529+ "notNull": true,
1530+ "default": 0
1531+ },
1532+ "metadata": {
1533+ "name": "metadata",
1534+ "type": "jsonb",
1535+ "primaryKey": false,
1536+ "notNull": true,
1537+ "default": "'{}'::jsonb"
1538+ }
1539+ },
1540+ "indexes": {
1541+ "costs_occurred_idx": {
1542+ "name": "costs_occurred_idx",
1543+ "columns": [
1544+ {
1545+ "expression": "occurred_at",
1546+ "isExpression": false,
1547+ "asc": true,
1548+ "nulls": "last"
1549+ }
1550+ ],
1551+ "isUnique": false,
1552+ "concurrently": false,
1553+ "method": "btree",
1554+ "with": {}
1555+ },
1556+ "costs_connector_idx": {
1557+ "name": "costs_connector_idx",
1558+ "columns": [
1559+ {
1560+ "expression": "connector_id",
1561+ "isExpression": false,
1562+ "asc": true,
1563+ "nulls": "last"
1564+ }
1565+ ],
1566+ "isUnique": false,
1567+ "concurrently": false,
1568+ "method": "btree",
1569+ "with": {}
1570+ }
1571+ },
1572+ "foreignKeys": {},
1573+ "compositePrimaryKeys": {},
1574+ "uniqueConstraints": {},
1575+ "policies": {},
1576+ "checkConstraints": {},
1577+ "isRLSEnabled": false
1578+ },
1579+ "public.crawl_state": {
1580+ "name": "crawl_state",
1581+ "schema": "",
1582+ "columns": {
1583+ "url_hash": {
1584+ "name": "url_hash",
1585+ "type": "text",
1586+ "primaryKey": true,
1587+ "notNull": true
1588+ },
1589+ "connector_id": {
1590+ "name": "connector_id",
1591+ "type": "text",
1592+ "primaryKey": false,
1593+ "notNull": true
1594+ },
1595+ "url": {
1596+ "name": "url",
1597+ "type": "text",
1598+ "primaryKey": false,
1599+ "notNull": true
1600+ },
1601+ "etag": {
1602+ "name": "etag",
1603+ "type": "text",
1604+ "primaryKey": false,
1605+ "notNull": false
1606+ },
1607+ "last_modified": {
1608+ "name": "last_modified",
1609+ "type": "text",
1610+ "primaryKey": false,
1611+ "notNull": false
1612+ },
1613+ "content_hash": {
1614+ "name": "content_hash",
1615+ "type": "text",
1616+ "primaryKey": false,
1617+ "notNull": false
1618+ },
1619+ "last_fetched_at": {
1620+ "name": "last_fetched_at",
1621+ "type": "timestamp with time zone",
1622+ "primaryKey": false,
1623+ "notNull": false
1624+ },
1625+ "last_changed_at": {
1626+ "name": "last_changed_at",
1627+ "type": "timestamp with time zone",
1628+ "primaryKey": false,
1629+ "notNull": false
1630+ },
1631+ "fetch_count": {
1632+ "name": "fetch_count",
1633+ "type": "integer",
1634+ "primaryKey": false,
1635+ "notNull": true,
1636+ "default": 0
1637+ },
1638+ "change_count": {
1639+ "name": "change_count",
1640+ "type": "integer",
1641+ "primaryKey": false,
1642+ "notNull": true,
1643+ "default": 0
1644+ },
1645+ "change_interval_hours": {
1646+ "name": "change_interval_hours",
1647+ "type": "real",
1648+ "primaryKey": false,
1649+ "notNull": false
1650+ },
1651+ "next_fetch_at": {
1652+ "name": "next_fetch_at",
1653+ "type": "timestamp with time zone",
1654+ "primaryKey": false,
1655+ "notNull": false
1656+ },
1657+ "last_status": {
1658+ "name": "last_status",
1659+ "type": "integer",
1660+ "primaryKey": false,
1661+ "notNull": false
1662+ },
1663+ "failures": {
1664+ "name": "failures",
1665+ "type": "integer",
1666+ "primaryKey": false,
1667+ "notNull": true,
1668+ "default": 0
1669+ }
1670+ },
1671+ "indexes": {
1672+ "crawl_state_next_idx": {
1673+ "name": "crawl_state_next_idx",
1674+ "columns": [
1675+ {
1676+ "expression": "connector_id",
1677+ "isExpression": false,
1678+ "asc": true,
1679+ "nulls": "last"
1680+ },
1681+ {
1682+ "expression": "next_fetch_at",
1683+ "isExpression": false,
1684+ "asc": true,
1685+ "nulls": "last"
1686+ }
1687+ ],
1688+ "isUnique": false,
1689+ "concurrently": false,
1690+ "method": "btree",
1691+ "with": {}
1692+ }
1693+ },
1694+ "foreignKeys": {},
1695+ "compositePrimaryKeys": {},
1696+ "uniqueConstraints": {},
1697+ "policies": {},
1698+ "checkConstraints": {},
1699+ "isRLSEnabled": false
1700+ },
1701+ "public.sources": {
1702+ "name": "sources",
1703+ "schema": "",
1704+ "columns": {
1705+ "id": {
1706+ "name": "id",
1707+ "type": "text",
1708+ "primaryKey": true,
1709+ "notNull": true
1710+ },
1711+ "name": {
1712+ "name": "name",
1713+ "type": "text",
1714+ "primaryKey": false,
1715+ "notNull": true
1716+ },
1717+ "source_type": {
1718+ "name": "source_type",
1719+ "type": "text",
1720+ "primaryKey": false,
1721+ "notNull": true
1722+ },
1723+ "url": {
1724+ "name": "url",
1725+ "type": "text",
1726+ "primaryKey": false,
1727+ "notNull": false
1728+ },
1729+ "countries": {
1730+ "name": "countries",
1731+ "type": "text[]",
1732+ "primaryKey": false,
1733+ "notNull": true,
1734+ "default": "'{}'::text[]"
1735+ },
1736+ "languages": {
1737+ "name": "languages",
1738+ "type": "text[]",
1739+ "primaryKey": false,
1740+ "notNull": true,
1741+ "default": "'{}'::text[]"
1742+ },
1743+ "currencies": {
1744+ "name": "currencies",
1745+ "type": "text[]",
1746+ "primaryKey": false,
1747+ "notNull": true,
1748+ "default": "'{}'::text[]"
1749+ },
1750+ "trust_score": {
1751+ "name": "trust_score",
1752+ "type": "numeric(8, 6)",
1753+ "primaryKey": false,
1754+ "notNull": true,
1755+ "default": 0.5
1756+ },
1757+ "trust_factors": {
1758+ "name": "trust_factors",
1759+ "type": "jsonb",
1760+ "primaryKey": false,
1761+ "notNull": true,
1762+ "default": "'{}'::jsonb"
1763+ },
1764+ "attribution_required": {
1765+ "name": "attribution_required",
1766+ "type": "boolean",
1767+ "primaryKey": false,
1768+ "notNull": true,
1769+ "default": true
1770+ },
1771+ "terms_url": {
1772+ "name": "terms_url",
1773+ "type": "text",
1774+ "primaryKey": false,
1775+ "notNull": false
1776+ },
1777+ "active": {
1778+ "name": "active",
1779+ "type": "boolean",
1780+ "primaryKey": false,
1781+ "notNull": true,
1782+ "default": true
1783+ },
1784+ "created_at": {
1785+ "name": "created_at",
1786+ "type": "timestamp with time zone",
1787+ "primaryKey": false,
1788+ "notNull": true,
1789+ "default": "now()"
1790+ },
1791+ "updated_at": {
1792+ "name": "updated_at",
1793+ "type": "timestamp with time zone",
1794+ "primaryKey": false,
1795+ "notNull": true,
1796+ "default": "now()"
1797+ }
1798+ },
1799+ "indexes": {},
1800+ "foreignKeys": {},
1801+ "compositePrimaryKeys": {},
1802+ "uniqueConstraints": {},
1803+ "policies": {},
1804+ "checkConstraints": {},
1805+ "isRLSEnabled": false
1806+ },
1807+ "public.audit_log": {
1808+ "name": "audit_log",
1809+ "schema": "",
1810+ "columns": {
1811+ "id": {
1812+ "name": "id",
1813+ "type": "text",
1814+ "primaryKey": true,
1815+ "notNull": true
1816+ },
1817+ "entity_type": {
1818+ "name": "entity_type",
1819+ "type": "text",
1820+ "primaryKey": false,
1821+ "notNull": true
1822+ },
1823+ "entity_id": {
1824+ "name": "entity_id",
1825+ "type": "text",
1826+ "primaryKey": false,
1827+ "notNull": true
1828+ },
1829+ "action": {
1830+ "name": "action",
1831+ "type": "text",
1832+ "primaryKey": false,
1833+ "notNull": true
1834+ },
1835+ "reason": {
1836+ "name": "reason",
1837+ "type": "text",
1838+ "primaryKey": false,
1839+ "notNull": true
1840+ },
1841+ "actor": {
1842+ "name": "actor",
1843+ "type": "text",
1844+ "primaryKey": false,
1845+ "notNull": true,
1846+ "default": "'system'"
1847+ },
1848+ "details": {
1849+ "name": "details",
1850+ "type": "jsonb",
1851+ "primaryKey": false,
1852+ "notNull": true,
1853+ "default": "'{}'::jsonb"
1854+ },
1855+ "created_at": {
1856+ "name": "created_at",
1857+ "type": "timestamp with time zone",
1858+ "primaryKey": false,
1859+ "notNull": true,
1860+ "default": "now()"
1861+ }
1862+ },
1863+ "indexes": {
1864+ "audit_entity_idx": {
1865+ "name": "audit_entity_idx",
1866+ "columns": [
1867+ {
1868+ "expression": "entity_type",
1869+ "isExpression": false,
1870+ "asc": true,
1871+ "nulls": "last"
1872+ },
1873+ {
1874+ "expression": "entity_id",
1875+ "isExpression": false,
1876+ "asc": true,
1877+ "nulls": "last"
1878+ }
1879+ ],
1880+ "isUnique": false,
1881+ "concurrently": false,
1882+ "method": "btree",
1883+ "with": {}
1884+ }
1885+ },
1886+ "foreignKeys": {},
1887+ "compositePrimaryKeys": {},
1888+ "uniqueConstraints": {},
1889+ "policies": {},
1890+ "checkConstraints": {},
1891+ "isRLSEnabled": false
1892+ },
1893+ "public.events": {
1894+ "name": "events",
1895+ "schema": "",
1896+ "columns": {
1897+ "id": {
1898+ "name": "id",
1899+ "type": "text",
1900+ "primaryKey": true,
1901+ "notNull": true
1902+ },
1903+ "type": {
1904+ "name": "type",
1905+ "type": "text",
1906+ "primaryKey": false,
1907+ "notNull": true
1908+ },
1909+ "entity_type": {
1910+ "name": "entity_type",
1911+ "type": "text",
1912+ "primaryKey": false,
1913+ "notNull": false
1914+ },
1915+ "entity_id": {
1916+ "name": "entity_id",
1917+ "type": "text",
1918+ "primaryKey": false,
1919+ "notNull": false
1920+ },
1921+ "payload": {
1922+ "name": "payload",
1923+ "type": "jsonb",
1924+ "primaryKey": false,
1925+ "notNull": true,
1926+ "default": "'{}'::jsonb"
1927+ },
1928+ "created_at": {
1929+ "name": "created_at",
1930+ "type": "timestamp with time zone",
1931+ "primaryKey": false,
1932+ "notNull": true,
1933+ "default": "now()"
1934+ }
1935+ },
1936+ "indexes": {
1937+ "events_type_created_idx": {
1938+ "name": "events_type_created_idx",
1939+ "columns": [
1940+ {
1941+ "expression": "type",
1942+ "isExpression": false,
1943+ "asc": true,
1944+ "nulls": "last"
1945+ },
1946+ {
1947+ "expression": "created_at",
1948+ "isExpression": false,
1949+ "asc": true,
1950+ "nulls": "last"
1951+ }
1952+ ],
1953+ "isUnique": false,
1954+ "concurrently": false,
1955+ "method": "btree",
1956+ "with": {}
1957+ },
1958+ "events_entity_idx": {
1959+ "name": "events_entity_idx",
1960+ "columns": [
1961+ {
1962+ "expression": "entity_type",
1963+ "isExpression": false,
1964+ "asc": true,
1965+ "nulls": "last"
1966+ },
1967+ {
1968+ "expression": "entity_id",
1969+ "isExpression": false,
1970+ "asc": true,
1971+ "nulls": "last"
1972+ }
1973+ ],
1974+ "isUnique": false,
1975+ "concurrently": false,
1976+ "method": "btree",
1977+ "with": {}
1978+ }
1979+ },
1980+ "foreignKeys": {},
1981+ "compositePrimaryKeys": {},
1982+ "uniqueConstraints": {},
1983+ "policies": {},
1984+ "checkConstraints": {},
1985+ "isRLSEnabled": false
1986+ },
1987+ "public.normalized_records": {
1988+ "name": "normalized_records",
1989+ "schema": "",
1990+ "columns": {
1991+ "id": {
1992+ "name": "id",
1993+ "type": "text",
1994+ "primaryKey": true,
1995+ "notNull": true
1996+ },
1997+ "raw_record_id": {
1998+ "name": "raw_record_id",
1999+ "type": "text",
2000+ "primaryKey": false,
2001+ "notNull": true
2002+ },
2003+ "connector_id": {
2004+ "name": "connector_id",
2005+ "type": "text",
2006+ "primaryKey": false,
2007+ "notNull": true
2008+ },
2009+ "source_id": {
2010+ "name": "source_id",
2011+ "type": "text",
2012+ "primaryKey": false,
2013+ "notNull": true
2014+ },
2015+ "kind": {
2016+ "name": "kind",
2017+ "type": "text",
2018+ "primaryKey": false,
2019+ "notNull": true
2020+ },
2021+ "payload": {
2022+ "name": "payload",
2023+ "type": "jsonb",
2024+ "primaryKey": false,
2025+ "notNull": true
2026+ },
2027+ "seq": {
2028+ "name": "seq",
2029+ "type": "integer",
2030+ "primaryKey": false,
2031+ "notNull": true,
2032+ "default": 0
2033+ },
2034+ "asset_id": {
2035+ "name": "asset_id",
2036+ "type": "text",
2037+ "primaryKey": false,
2038+ "notNull": false
2039+ },
2040+ "variant_id": {
2041+ "name": "variant_id",
2042+ "type": "text",
2043+ "primaryKey": false,
2044+ "notNull": false
2045+ },
2046+ "match_method": {
2047+ "name": "match_method",
2048+ "type": "text",
2049+ "primaryKey": false,
2050+ "notNull": false
2051+ },
2052+ "match_confidence": {
2053+ "name": "match_confidence",
2054+ "type": "numeric(8, 6)",
2055+ "primaryKey": false,
2056+ "notNull": false
2057+ },
2058+ "status": {
2059+ "name": "status",
2060+ "type": "text",
2061+ "primaryKey": false,
2062+ "notNull": true,
2063+ "default": "'pending'"
2064+ },
2065+ "reject_reason": {
2066+ "name": "reject_reason",
2067+ "type": "text",
2068+ "primaryKey": false,
2069+ "notNull": false
2070+ },
2071+ "target_id": {
2072+ "name": "target_id",
2073+ "type": "text",
2074+ "primaryKey": false,
2075+ "notNull": false
2076+ },
2077+ "created_at": {
2078+ "name": "created_at",
2079+ "type": "timestamp with time zone",
2080+ "primaryKey": false,
2081+ "notNull": true,
2082+ "default": "now()"
2083+ },
2084+ "processed_at": {
2085+ "name": "processed_at",
2086+ "type": "timestamp with time zone",
2087+ "primaryKey": false,
2088+ "notNull": false
2089+ }
2090+ },
2091+ "indexes": {
2092+ "normalized_records_status_idx": {
2093+ "name": "normalized_records_status_idx",
2094+ "columns": [
2095+ {
2096+ "expression": "status",
2097+ "isExpression": false,
2098+ "asc": true,
2099+ "nulls": "last"
2100+ },
2101+ {
2102+ "expression": "created_at",
2103+ "isExpression": false,
2104+ "asc": true,
2105+ "nulls": "last"
2106+ }
2107+ ],
2108+ "isUnique": false,
2109+ "concurrently": false,
2110+ "method": "btree",
2111+ "with": {}
2112+ },
2113+ "normalized_records_status_kind_idx": {
2114+ "name": "normalized_records_status_kind_idx",
2115+ "columns": [
2116+ {
2117+ "expression": "status",
2118+ "isExpression": false,
2119+ "asc": true,
2120+ "nulls": "last"
2121+ },
2122+ {
2123+ "expression": "kind",
2124+ "isExpression": false,
2125+ "asc": true,
2126+ "nulls": "last"
2127+ },
2128+ {
2129+ "expression": "created_at",
2130+ "isExpression": false,
2131+ "asc": true,
2132+ "nulls": "last"
2133+ }
2134+ ],
2135+ "isUnique": false,
2136+ "concurrently": false,
2137+ "method": "btree",
2138+ "with": {}
2139+ },
2140+ "normalized_records_asset_idx": {
2141+ "name": "normalized_records_asset_idx",
2142+ "columns": [
2143+ {
2144+ "expression": "asset_id",
2145+ "isExpression": false,
2146+ "asc": true,
2147+ "nulls": "last"
2148+ }
2149+ ],
2150+ "isUnique": false,
2151+ "concurrently": false,
2152+ "method": "btree",
2153+ "with": {}
2154+ },
2155+ "normalized_records_raw_uq": {
2156+ "name": "normalized_records_raw_uq",
2157+ "columns": [
2158+ {
2159+ "expression": "raw_record_id",
2160+ "isExpression": false,
2161+ "asc": true,
2162+ "nulls": "last"
2163+ },
2164+ {
2165+ "expression": "kind",
2166+ "isExpression": false,
2167+ "asc": true,
2168+ "nulls": "last"
2169+ },
2170+ {
2171+ "expression": "seq",
2172+ "isExpression": false,
2173+ "asc": true,
2174+ "nulls": "last"
2175+ }
2176+ ],
2177+ "isUnique": true,
2178+ "concurrently": false,
2179+ "method": "btree",
2180+ "with": {}
2181+ }
2182+ },
2183+ "foreignKeys": {},
2184+ "compositePrimaryKeys": {},
2185+ "uniqueConstraints": {},
2186+ "policies": {},
2187+ "checkConstraints": {},
2188+ "isRLSEnabled": false
2189+ },
2190+ "public.raw_records": {
2191+ "name": "raw_records",
2192+ "schema": "",
2193+ "columns": {
2194+ "id": {
2195+ "name": "id",
2196+ "type": "text",
2197+ "primaryKey": true,
2198+ "notNull": true
2199+ },
2200+ "connector_id": {
2201+ "name": "connector_id",
2202+ "type": "text",
2203+ "primaryKey": false,
2204+ "notNull": true
2205+ },
2206+ "source_id": {
2207+ "name": "source_id",
2208+ "type": "text",
2209+ "primaryKey": false,
2210+ "notNull": true
2211+ },
2212+ "run_id": {
2213+ "name": "run_id",
2214+ "type": "text",
2215+ "primaryKey": false,
2216+ "notNull": false
2217+ },
2218+ "engine": {
2219+ "name": "engine",
2220+ "type": "text",
2221+ "primaryKey": false,
2222+ "notNull": true
2223+ },
2224+ "url": {
2225+ "name": "url",
2226+ "type": "text",
2227+ "primaryKey": false,
2228+ "notNull": true
2229+ },
2230+ "external_id": {
2231+ "name": "external_id",
2232+ "type": "text",
2233+ "primaryKey": false,
2234+ "notNull": false
2235+ },
2236+ "kind": {
2237+ "name": "kind",
2238+ "type": "text",
2239+ "primaryKey": false,
2240+ "notNull": true
2241+ },
2242+ "fetched_at": {
2243+ "name": "fetched_at",
2244+ "type": "timestamp with time zone",
2245+ "primaryKey": false,
2246+ "notNull": true
2247+ },
2248+ "content_hash": {
2249+ "name": "content_hash",
2250+ "type": "text",
2251+ "primaryKey": false,
2252+ "notNull": true
2253+ },
2254+ "http_status": {
2255+ "name": "http_status",
2256+ "type": "integer",
2257+ "primaryKey": false,
2258+ "notNull": false
2259+ },
2260+ "payload": {
2261+ "name": "payload",
2262+ "type": "jsonb",
2263+ "primaryKey": false,
2264+ "notNull": true
2265+ },
2266+ "snapshot_ref": {
2267+ "name": "snapshot_ref",
2268+ "type": "text",
2269+ "primaryKey": false,
2270+ "notNull": false
2271+ },
2272+ "parser_version": {
2273+ "name": "parser_version",
2274+ "type": "text",
2275+ "primaryKey": false,
2276+ "notNull": true
2277+ },
2278+ "connector_version": {
2279+ "name": "connector_version",
2280+ "type": "text",
2281+ "primaryKey": false,
2282+ "notNull": true
2283+ },
2284+ "processed_at": {
2285+ "name": "processed_at",
2286+ "type": "timestamp with time zone",
2287+ "primaryKey": false,
2288+ "notNull": false
2289+ },
2290+ "process_error": {
2291+ "name": "process_error",
2292+ "type": "text",
2293+ "primaryKey": false,
2294+ "notNull": false
2295+ },
2296+ "created_at": {
2297+ "name": "created_at",
2298+ "type": "timestamp with time zone",
2299+ "primaryKey": false,
2300+ "notNull": true,
2301+ "default": "now()"
2302+ }
2303+ },
2304+ "indexes": {
2305+ "raw_records_connector_hash_uq": {
2306+ "name": "raw_records_connector_hash_uq",
2307+ "columns": [
2308+ {
2309+ "expression": "connector_id",
2310+ "isExpression": false,
2311+ "asc": true,
2312+ "nulls": "last"
2313+ },
2314+ {
2315+ "expression": "content_hash",
2316+ "isExpression": false,
2317+ "asc": true,
2318+ "nulls": "last"
2319+ }
2320+ ],
2321+ "isUnique": true,
2322+ "concurrently": false,
2323+ "method": "btree",
2324+ "with": {}
2325+ },
2326+ "raw_records_connector_fetched_idx": {
2327+ "name": "raw_records_connector_fetched_idx",
2328+ "columns": [
2329+ {
2330+ "expression": "connector_id",
2331+ "isExpression": false,
2332+ "asc": true,
2333+ "nulls": "last"
2334+ },
2335+ {
2336+ "expression": "fetched_at",
2337+ "isExpression": false,
2338+ "asc": true,
2339+ "nulls": "last"
2340+ }
2341+ ],
2342+ "isUnique": false,
2343+ "concurrently": false,
2344+ "method": "btree",
2345+ "with": {}
2346+ },
2347+ "raw_records_unprocessed_idx": {
2348+ "name": "raw_records_unprocessed_idx",
2349+ "columns": [
2350+ {
2351+ "expression": "processed_at",
2352+ "isExpression": false,
2353+ "asc": true,
2354+ "nulls": "last"
2355+ }
2356+ ],
2357+ "isUnique": false,
2358+ "concurrently": false,
2359+ "method": "btree",
2360+ "with": {}
2361+ },
2362+ "raw_records_external_idx": {
2363+ "name": "raw_records_external_idx",
2364+ "columns": [
2365+ {
2366+ "expression": "connector_id",
2367+ "isExpression": false,
2368+ "asc": true,
2369+ "nulls": "last"
2370+ },
2371+ {
2372+ "expression": "external_id",
2373+ "isExpression": false,
2374+ "asc": true,
2375+ "nulls": "last"
2376+ }
2377+ ],
2378+ "isUnique": false,
2379+ "concurrently": false,
2380+ "method": "btree",
2381+ "with": {}
2382+ }
2383+ },
2384+ "foreignKeys": {},
2385+ "compositePrimaryKeys": {},
2386+ "uniqueConstraints": {},
2387+ "policies": {},
2388+ "checkConstraints": {},
2389+ "isRLSEnabled": false
2390+ },
2391+ "public.asset_embeddings": {
2392+ "name": "asset_embeddings",
2393+ "schema": "",
2394+ "columns": {
2395+ "asset_id": {
2396+ "name": "asset_id",
2397+ "type": "text",
2398+ "primaryKey": true,
2399+ "notNull": true
2400+ },
2401+ "model": {
2402+ "name": "model",
2403+ "type": "text",
2404+ "primaryKey": false,
2405+ "notNull": true
2406+ },
2407+ "embedding": {
2408+ "name": "embedding",
2409+ "type": "vector(1536)",
2410+ "primaryKey": false,
2411+ "notNull": true
2412+ },
2413+ "updated_at": {
2414+ "name": "updated_at",
2415+ "type": "timestamp with time zone",
2416+ "primaryKey": false,
2417+ "notNull": true,
2418+ "default": "now()"
2419+ }
2420+ },
2421+ "indexes": {},
2422+ "foreignKeys": {},
2423+ "compositePrimaryKeys": {},
2424+ "uniqueConstraints": {},
2425+ "policies": {},
2426+ "checkConstraints": {},
2427+ "isRLSEnabled": false
2428+ },
2429+ "public.asset_stats": {
2430+ "name": "asset_stats",
2431+ "schema": "",
2432+ "columns": {
2433+ "asset_id": {
2434+ "name": "asset_id",
2435+ "type": "text",
2436+ "primaryKey": true,
2437+ "notNull": true
2438+ },
2439+ "riv_usd": {
2440+ "name": "riv_usd",
2441+ "type": "numeric(18, 4)",
2442+ "primaryKey": false,
2443+ "notNull": false
2444+ },
2445+ "riv_low_usd": {
2446+ "name": "riv_low_usd",
2447+ "type": "numeric(18, 4)",
2448+ "primaryKey": false,
2449+ "notNull": false
2450+ },
2451+ "riv_high_usd": {
2452+ "name": "riv_high_usd",
2453+ "type": "numeric(18, 4)",
2454+ "primaryKey": false,
2455+ "notNull": false
2456+ },
2457+ "riv_confidence": {
2458+ "name": "riv_confidence",
2459+ "type": "numeric(8, 6)",
2460+ "primaryKey": false,
2461+ "notNull": false
2462+ },
2463+ "riv_sample_size": {
2464+ "name": "riv_sample_size",
2465+ "type": "integer",
2466+ "primaryKey": false,
2467+ "notNull": true,
2468+ "default": 0
2469+ },
2470+ "riv_variant_id": {
2471+ "name": "riv_variant_id",
2472+ "type": "text",
2473+ "primaryKey": false,
2474+ "notNull": false
2475+ },
2476+ "latest_sale_usd": {
2477+ "name": "latest_sale_usd",
2478+ "type": "numeric(18, 4)",
2479+ "primaryKey": false,
2480+ "notNull": false
2481+ },
2482+ "latest_sale_at": {
2483+ "name": "latest_sale_at",
2484+ "type": "timestamp with time zone",
2485+ "primaryKey": false,
2486+ "notNull": false
2487+ },
2488+ "change_1d": {
2489+ "name": "change_1d",
2490+ "type": "numeric(8, 6)",
2491+ "primaryKey": false,
2492+ "notNull": false
2493+ },
2494+ "change_7d": {
2495+ "name": "change_7d",
2496+ "type": "numeric(8, 6)",
2497+ "primaryKey": false,
2498+ "notNull": false
2499+ },
2500+ "change_30d": {
2501+ "name": "change_30d",
2502+ "type": "numeric(8, 6)",
2503+ "primaryKey": false,
2504+ "notNull": false
2505+ },
2506+ "change_90d": {
2507+ "name": "change_90d",
2508+ "type": "numeric(8, 6)",
2509+ "primaryKey": false,
2510+ "notNull": false
2511+ },
2512+ "change_1y": {
2513+ "name": "change_1y",
2514+ "type": "numeric(8, 6)",
2515+ "primaryKey": false,
2516+ "notNull": false
2517+ },
2518+ "ath_usd": {
2519+ "name": "ath_usd",
2520+ "type": "numeric(18, 4)",
2521+ "primaryKey": false,
2522+ "notNull": false
2523+ },
2524+ "ath_at": {
2525+ "name": "ath_at",
2526+ "type": "timestamp with time zone",
2527+ "primaryKey": false,
2528+ "notNull": false
2529+ },
2530+ "atl_usd": {
2531+ "name": "atl_usd",
2532+ "type": "numeric(18, 4)",
2533+ "primaryKey": false,
2534+ "notNull": false
2535+ },
2536+ "atl_at": {
2537+ "name": "atl_at",
2538+ "type": "timestamp with time zone",
2539+ "primaryKey": false,
2540+ "notNull": false
2541+ },
2542+ "sales_count": {
2543+ "name": "sales_count",
2544+ "type": "integer",
2545+ "primaryKey": false,
2546+ "notNull": true,
2547+ "default": 0
2548+ },
2549+ "sales_30d": {
2550+ "name": "sales_30d",
2551+ "type": "integer",
2552+ "primaryKey": false,
2553+ "notNull": true,
2554+ "default": 0
2555+ },
2556+ "sales_1y": {
2557+ "name": "sales_1y",
2558+ "type": "integer",
2559+ "primaryKey": false,
2560+ "notNull": true,
2561+ "default": 0
2562+ },
2563+ "volume_30d_usd": {
2564+ "name": "volume_30d_usd",
2565+ "type": "numeric(18, 4)",
2566+ "primaryKey": false,
2567+ "notNull": false
2568+ },
2569+ "active_listings": {
2570+ "name": "active_listings",
2571+ "type": "integer",
2572+ "primaryKey": false,
2573+ "notNull": true,
2574+ "default": 0
2575+ },
2576+ "min_ask_usd": {
2577+ "name": "min_ask_usd",
2578+ "type": "numeric(18, 4)",
2579+ "primaryKey": false,
2580+ "notNull": false
2581+ },
2582+ "observations_count": {
2583+ "name": "observations_count",
2584+ "type": "integer",
2585+ "primaryKey": false,
2586+ "notNull": true,
2587+ "default": 0
2588+ },
2589+ "sources_count": {
2590+ "name": "sources_count",
2591+ "type": "integer",
2592+ "primaryKey": false,
2593+ "notNull": true,
2594+ "default": 0
2595+ },
2596+ "liquidity_score": {
2597+ "name": "liquidity_score",
2598+ "type": "real",
2599+ "primaryKey": false,
2600+ "notNull": false
2601+ },
2602+ "rarity_score": {
2603+ "name": "rarity_score",
2604+ "type": "real",
2605+ "primaryKey": false,
2606+ "notNull": false
2607+ },
2608+ "momentum_7d": {
2609+ "name": "momentum_7d",
2610+ "type": "real",
2611+ "primaryKey": false,
2612+ "notNull": false
2613+ },
2614+ "momentum_30d": {
2615+ "name": "momentum_30d",
2616+ "type": "real",
2617+ "primaryKey": false,
2618+ "notNull": false
2619+ },
2620+ "momentum_90d": {
2621+ "name": "momentum_90d",
2622+ "type": "real",
2623+ "primaryKey": false,
2624+ "notNull": false
2625+ },
2626+ "momentum_1y": {
2627+ "name": "momentum_1y",
2628+ "type": "real",
2629+ "primaryKey": false,
2630+ "notNull": false
2631+ },
2632+ "trending_score": {
2633+ "name": "trending_score",
2634+ "type": "real",
2635+ "primaryKey": false,
2636+ "notNull": false
2637+ },
2638+ "value_opportunity": {
2639+ "name": "value_opportunity",
2640+ "type": "real",
2641+ "primaryKey": false,
2642+ "notNull": false
2643+ },
2644+ "data_quality": {
2645+ "name": "data_quality",
2646+ "type": "real",
2647+ "primaryKey": false,
2648+ "notNull": false
2649+ },
2650+ "watchers": {
2651+ "name": "watchers",
2652+ "type": "integer",
2653+ "primaryKey": false,
2654+ "notNull": true,
2655+ "default": 0
2656+ },
2657+ "views_30d": {
2658+ "name": "views_30d",
2659+ "type": "integer",
2660+ "primaryKey": false,
2661+ "notNull": true,
2662+ "default": 0
2663+ },
2664+ "updated_at": {
2665+ "name": "updated_at",
2666+ "type": "timestamp with time zone",
2667+ "primaryKey": false,
2668+ "notNull": true,
2669+ "default": "now()"
2670+ }
2671+ },
2672+ "indexes": {
2673+ "asset_stats_riv_idx": {
2674+ "name": "asset_stats_riv_idx",
2675+ "columns": [
2676+ {
2677+ "expression": "riv_usd",
2678+ "isExpression": false,
2679+ "asc": true,
2680+ "nulls": "last"
2681+ }
2682+ ],
2683+ "isUnique": false,
2684+ "concurrently": false,
2685+ "method": "btree",
2686+ "with": {}
2687+ },
2688+ "asset_stats_trending_idx": {
2689+ "name": "asset_stats_trending_idx",
2690+ "columns": [
2691+ {
2692+ "expression": "trending_score",
2693+ "isExpression": false,
2694+ "asc": true,
2695+ "nulls": "last"
2696+ }
2697+ ],
2698+ "isUnique": false,
2699+ "concurrently": false,
2700+ "method": "btree",
2701+ "with": {}
2702+ },
2703+ "asset_stats_liquidity_idx": {
2704+ "name": "asset_stats_liquidity_idx",
2705+ "columns": [
2706+ {
2707+ "expression": "liquidity_score",
2708+ "isExpression": false,
2709+ "asc": true,
2710+ "nulls": "last"
2711+ }
2712+ ],
2713+ "isUnique": false,
2714+ "concurrently": false,
2715+ "method": "btree",
2716+ "with": {}
2717+ },
2718+ "asset_stats_change_7d_idx": {
2719+ "name": "asset_stats_change_7d_idx",
2720+ "columns": [
2721+ {
2722+ "expression": "change_7d",
2723+ "isExpression": false,
2724+ "asc": true,
2725+ "nulls": "last"
2726+ }
2727+ ],
2728+ "isUnique": false,
2729+ "concurrently": false,
2730+ "method": "btree",
2731+ "with": {}
2732+ },
2733+ "asset_stats_change_30d_idx": {
2734+ "name": "asset_stats_change_30d_idx",
2735+ "columns": [
2736+ {
2737+ "expression": "change_30d",
2738+ "isExpression": false,
2739+ "asc": true,
2740+ "nulls": "last"
2741+ }
2742+ ],
2743+ "isUnique": false,
2744+ "concurrently": false,
2745+ "method": "btree",
2746+ "with": {}
2747+ },
2748+ "asset_stats_change_1y_idx": {
2749+ "name": "asset_stats_change_1y_idx",
2750+ "columns": [
2751+ {
2752+ "expression": "change_1y",
2753+ "isExpression": false,
2754+ "asc": true,
2755+ "nulls": "last"
2756+ }
2757+ ],
2758+ "isUnique": false,
2759+ "concurrently": false,
2760+ "method": "btree",
2761+ "with": {}
2762+ },
2763+ "asset_stats_opportunity_idx": {
2764+ "name": "asset_stats_opportunity_idx",
2765+ "columns": [
2766+ {
2767+ "expression": "value_opportunity",
2768+ "isExpression": false,
2769+ "asc": true,
2770+ "nulls": "last"
2771+ }
2772+ ],
2773+ "isUnique": false,
2774+ "where": "value_opportunity is not null",
2775+ "concurrently": false,
2776+ "method": "btree",
2777+ "with": {}
2778+ },
2779+ "asset_stats_sales_30d_idx": {
2780+ "name": "asset_stats_sales_30d_idx",
2781+ "columns": [
2782+ {
2783+ "expression": "sales_30d",
2784+ "isExpression": false,
2785+ "asc": true,
2786+ "nulls": "last"
2787+ }
2788+ ],
2789+ "isUnique": false,
2790+ "concurrently": false,
2791+ "method": "btree",
2792+ "with": {}
2793+ }
2794+ },
2795+ "foreignKeys": {},
2796+ "compositePrimaryKeys": {},
2797+ "uniqueConstraints": {},
2798+ "policies": {},
2799+ "checkConstraints": {},
2800+ "isRLSEnabled": false
2801+ },
2802+ "public.asset_variants": {
2803+ "name": "asset_variants",
2804+ "schema": "",
2805+ "columns": {
2806+ "id": {
2807+ "name": "id",
2808+ "type": "text",
2809+ "primaryKey": true,
2810+ "notNull": true
2811+ },
2812+ "asset_id": {
2813+ "name": "asset_id",
2814+ "type": "text",
2815+ "primaryKey": false,
2816+ "notNull": true
2817+ },
2818+ "variant_key": {
2819+ "name": "variant_key",
2820+ "type": "text",
2821+ "primaryKey": false,
2822+ "notNull": true
2823+ },
2824+ "grader": {
2825+ "name": "grader",
2826+ "type": "text",
2827+ "primaryKey": false,
2828+ "notNull": false
2829+ },
2830+ "grade": {
2831+ "name": "grade",
2832+ "type": "text",
2833+ "primaryKey": false,
2834+ "notNull": false
2835+ },
2836+ "qualifier": {
2837+ "name": "qualifier",
2838+ "type": "text",
2839+ "primaryKey": false,
2840+ "notNull": false
2841+ },
2842+ "condition": {
2843+ "name": "condition",
2844+ "type": "text",
2845+ "primaryKey": false,
2846+ "notNull": false
2847+ },
2848+ "completeness": {
2849+ "name": "completeness",
2850+ "type": "text",
2851+ "primaryKey": false,
2852+ "notNull": false
2853+ },
2854+ "size_label": {
2855+ "name": "size_label",
2856+ "type": "text",
2857+ "primaryKey": false,
2858+ "notNull": false
2859+ },
2860+ "label": {
2861+ "name": "label",
2862+ "type": "text",
2863+ "primaryKey": false,
2864+ "notNull": true
2865+ },
2866+ "is_default": {
2867+ "name": "is_default",
2868+ "type": "boolean",
2869+ "primaryKey": false,
2870+ "notNull": true,
2871+ "default": false
2872+ },
2873+ "created_at": {
2874+ "name": "created_at",
2875+ "type": "timestamp with time zone",
2876+ "primaryKey": false,
2877+ "notNull": true,
2878+ "default": "now()"
2879+ }
2880+ },
2881+ "indexes": {
2882+ "asset_variants_uq": {
2883+ "name": "asset_variants_uq",
2884+ "columns": [
2885+ {
2886+ "expression": "asset_id",
2887+ "isExpression": false,
2888+ "asc": true,
2889+ "nulls": "last"
2890+ },
2891+ {
2892+ "expression": "variant_key",
2893+ "isExpression": false,
2894+ "asc": true,
2895+ "nulls": "last"
2896+ }
2897+ ],
2898+ "isUnique": true,
2899+ "concurrently": false,
2900+ "method": "btree",
2901+ "with": {}
2902+ },
2903+ "asset_variants_asset_idx": {
2904+ "name": "asset_variants_asset_idx",
2905+ "columns": [
2906+ {
2907+ "expression": "asset_id",
2908+ "isExpression": false,
2909+ "asc": true,
2910+ "nulls": "last"
2911+ }
2912+ ],
2913+ "isUnique": false,
2914+ "concurrently": false,
2915+ "method": "btree",
2916+ "with": {}
2917+ }
2918+ },
2919+ "foreignKeys": {},
2920+ "compositePrimaryKeys": {},
2921+ "uniqueConstraints": {},
2922+ "policies": {},
2923+ "checkConstraints": {},
2924+ "isRLSEnabled": false
2925+ },
2926+ "public.assets": {
2927+ "name": "assets",
2928+ "schema": "",
2929+ "columns": {
2930+ "id": {
2931+ "name": "id",
2932+ "type": "text",
2933+ "primaryKey": true,
2934+ "notNull": true
2935+ },
2936+ "slug": {
2937+ "name": "slug",
2938+ "type": "text",
2939+ "primaryKey": false,
2940+ "notNull": true
2941+ },
2942+ "canonical_key": {
2943+ "name": "canonical_key",
2944+ "type": "text",
2945+ "primaryKey": false,
2946+ "notNull": true
2947+ },
2948+ "category_slug": {
2949+ "name": "category_slug",
2950+ "type": "text",
2951+ "primaryKey": false,
2952+ "notNull": true
2953+ },
2954+ "subcategory_slug": {
2955+ "name": "subcategory_slug",
2956+ "type": "text",
2957+ "primaryKey": false,
2958+ "notNull": false
2959+ },
2960+ "family_slug": {
2961+ "name": "family_slug",
2962+ "type": "text",
2963+ "primaryKey": false,
2964+ "notNull": true
2965+ },
2966+ "franchise": {
2967+ "name": "franchise",
2968+ "type": "text",
2969+ "primaryKey": false,
2970+ "notNull": false
2971+ },
2972+ "brand": {
2973+ "name": "brand",
2974+ "type": "text",
2975+ "primaryKey": false,
2976+ "notNull": false
2977+ },
2978+ "series": {
2979+ "name": "series",
2980+ "type": "text",
2981+ "primaryKey": false,
2982+ "notNull": false
2983+ },
2984+ "set_slug": {
2985+ "name": "set_slug",
2986+ "type": "text",
2987+ "primaryKey": false,
2988+ "notNull": false
2989+ },
2990+ "set_name": {
2991+ "name": "set_name",
2992+ "type": "text",
2993+ "primaryKey": false,
2994+ "notNull": false
2995+ },
2996+ "set_code": {
2997+ "name": "set_code",
2998+ "type": "text",
2999+ "primaryKey": false,
3000+ "notNull": false
3001+ },
3002+ "name": {
3003+ "name": "name",
3004+ "type": "text",
3005+ "primaryKey": false,
3006+ "notNull": true
3007+ },
3008+ "title": {
3009+ "name": "title",
3010+ "type": "text",
3011+ "primaryKey": false,
3012+ "notNull": true
3013+ },
3014+ "model": {
3015+ "name": "model",
3016+ "type": "text",
3017+ "primaryKey": false,
3018+ "notNull": false
3019+ },
3020+ "reference": {
3021+ "name": "reference",
3022+ "type": "text",
3023+ "primaryKey": false,
3024+ "notNull": false
3025+ },
3026+ "number": {
3027+ "name": "number",
3028+ "type": "text",
3029+ "primaryKey": false,
3030+ "notNull": false
3031+ },
3032+ "year": {
3033+ "name": "year",
3034+ "type": "integer",
3035+ "primaryKey": false,
3036+ "notNull": false
3037+ },
3038+ "edition": {
3039+ "name": "edition",
3040+ "type": "text",
3041+ "primaryKey": false,
3042+ "notNull": false
3043+ },
3044+ "variant": {
3045+ "name": "variant",
3046+ "type": "text",
3047+ "primaryKey": false,
3048+ "notNull": false
3049+ },
3050+ "language": {
3051+ "name": "language",
3052+ "type": "text",
3053+ "primaryKey": false,
3054+ "notNull": false
3055+ },
3056+ "region": {
3057+ "name": "region",
3058+ "type": "text",
3059+ "primaryKey": false,
3060+ "notNull": false
3061+ },
3062+ "country": {
3063+ "name": "country",
3064+ "type": "text",
3065+ "primaryKey": false,
3066+ "notNull": false
3067+ },
3068+ "material": {
3069+ "name": "material",
3070+ "type": "text",
3071+ "primaryKey": false,
3072+ "notNull": false
3073+ },
3074+ "size": {
3075+ "name": "size",
3076+ "type": "text",
3077+ "primaryKey": false,
3078+ "notNull": false
3079+ },
3080+ "color": {
3081+ "name": "color",
3082+ "type": "text",
3083+ "primaryKey": false,
3084+ "notNull": false
3085+ },
3086+ "rarity": {
3087+ "name": "rarity",
3088+ "type": "text",
3089+ "primaryKey": false,
3090+ "notNull": false
3091+ },
3092+ "production_quantity": {
3093+ "name": "production_quantity",
3094+ "type": "integer",
3095+ "primaryKey": false,
3096+ "notNull": false
3097+ },
3098+ "original_msrp": {
3099+ "name": "original_msrp",
3100+ "type": "numeric(18, 4)",
3101+ "primaryKey": false,
3102+ "notNull": false
3103+ },
3104+ "original_msrp_currency": {
3105+ "name": "original_msrp_currency",
3106+ "type": "text",
3107+ "primaryKey": false,
3108+ "notNull": false
3109+ },
3110+ "release_date": {
3111+ "name": "release_date",
3112+ "type": "text",
3113+ "primaryKey": false,
3114+ "notNull": false
3115+ },
3116+ "description": {
3117+ "name": "description",
3118+ "type": "text",
3119+ "primaryKey": false,
3120+ "notNull": false
3121+ },
3122+ "hero_image_url": {
3123+ "name": "hero_image_url",
3124+ "type": "text",
3125+ "primaryKey": false,
3126+ "notNull": false
3127+ },
3128+ "identifiers": {
3129+ "name": "identifiers",
3130+ "type": "jsonb",
3131+ "primaryKey": false,
3132+ "notNull": true,
3133+ "default": "'{}'::jsonb"
3134+ },
3135+ "metadata": {
3136+ "name": "metadata",
3137+ "type": "jsonb",
3138+ "primaryKey": false,
3139+ "notNull": true,
3140+ "default": "'{}'::jsonb"
3141+ },
3142+ "merged_from": {
3143+ "name": "merged_from",
3144+ "type": "text[]",
3145+ "primaryKey": false,
3146+ "notNull": true,
3147+ "default": "'{}'::text[]"
3148+ },
3149+ "data_quality": {
3150+ "name": "data_quality",
3151+ "type": "real",
3152+ "primaryKey": false,
3153+ "notNull": true,
3154+ "default": 0
3155+ },
3156+ "verified": {
3157+ "name": "verified",
3158+ "type": "boolean",
3159+ "primaryKey": false,
3160+ "notNull": true,
3161+ "default": false
3162+ },
3163+ "search": {
3164+ "name": "search",
3165+ "type": "tsvector",
3166+ "primaryKey": false,
3167+ "notNull": false,
3168+ "generated": {
3169+ "as": "setweight(to_tsvector('simple', coalesce(name, '')), 'A') || setweight(to_tsvector('simple', coalesce(set_name, '') || ' ' || coalesce(number, '') || ' ' || coalesce(variant, '') || ' ' || coalesce(edition, '')), 'B') || setweight(to_tsvector('simple', coalesce(brand, '') || ' ' || coalesce(franchise, '') || ' ' || coalesce(reference, '') || ' ' || coalesce(model, '') || ' ' || coalesce(year::text, '') || ' ' || coalesce(category_slug, '')), 'C')",
3170+ "type": "stored"
3171+ }
3172+ },
3173+ "created_at": {
3174+ "name": "created_at",
3175+ "type": "timestamp with time zone",
3176+ "primaryKey": false,
3177+ "notNull": true,
3178+ "default": "now()"
3179+ },
3180+ "updated_at": {
3181+ "name": "updated_at",
3182+ "type": "timestamp with time zone",
3183+ "primaryKey": false,
3184+ "notNull": true,
3185+ "default": "now()"
3186+ }
3187+ },
3188+ "indexes": {
3189+ "assets_canonical_key_uq": {
3190+ "name": "assets_canonical_key_uq",
3191+ "columns": [
3192+ {
3193+ "expression": "canonical_key",
3194+ "isExpression": false,
3195+ "asc": true,
3196+ "nulls": "last"
3197+ }
3198+ ],
3199+ "isUnique": true,
3200+ "concurrently": false,
3201+ "method": "btree",
3202+ "with": {}
3203+ },
3204+ "assets_slug_uq": {
3205+ "name": "assets_slug_uq",
3206+ "columns": [
3207+ {
3208+ "expression": "slug",
3209+ "isExpression": false,
3210+ "asc": true,
3211+ "nulls": "last"
3212+ }
3213+ ],
3214+ "isUnique": true,
3215+ "concurrently": false,
3216+ "method": "btree",
3217+ "with": {}
3218+ },
3219+ "assets_category_idx": {
3220+ "name": "assets_category_idx",
3221+ "columns": [
3222+ {
3223+ "expression": "category_slug",
3224+ "isExpression": false,
3225+ "asc": true,
3226+ "nulls": "last"
3227+ }
3228+ ],
3229+ "isUnique": false,
3230+ "concurrently": false,
3231+ "method": "btree",
3232+ "with": {}
3233+ },
3234+ "assets_family_idx": {
3235+ "name": "assets_family_idx",
3236+ "columns": [
3237+ {
3238+ "expression": "family_slug",
3239+ "isExpression": false,
3240+ "asc": true,
3241+ "nulls": "last"
3242+ }
3243+ ],
3244+ "isUnique": false,
3245+ "concurrently": false,
3246+ "method": "btree",
3247+ "with": {}
3248+ },
3249+ "assets_set_idx": {
3250+ "name": "assets_set_idx",
3251+ "columns": [
3252+ {
3253+ "expression": "set_slug",
3254+ "isExpression": false,
3255+ "asc": true,
3256+ "nulls": "last"
3257+ }
3258+ ],
3259+ "isUnique": false,
3260+ "concurrently": false,
3261+ "method": "btree",
3262+ "with": {}
3263+ },
3264+ "assets_search_gin": {
3265+ "name": "assets_search_gin",
3266+ "columns": [
3267+ {
3268+ "expression": "search",
3269+ "isExpression": false,
3270+ "asc": true,
3271+ "nulls": "last"
3272+ }
3273+ ],
3274+ "isUnique": false,
3275+ "concurrently": false,
3276+ "method": "gin",
3277+ "with": {}
3278+ },
3279+ "assets_title_trgm": {
3280+ "name": "assets_title_trgm",
3281+ "columns": [
3282+ {
3283+ "expression": "\"title\" gin_trgm_ops",
3284+ "asc": true,
3285+ "isExpression": true,
3286+ "nulls": "last"
3287+ }
3288+ ],
3289+ "isUnique": false,
3290+ "concurrently": false,
3291+ "method": "gin",
3292+ "with": {}
3293+ },
3294+ "assets_identifiers_gin": {
3295+ "name": "assets_identifiers_gin",
3296+ "columns": [
3297+ {
3298+ "expression": "identifiers",
3299+ "isExpression": false,
3300+ "asc": true,
3301+ "nulls": "last"
3302+ }
3303+ ],
3304+ "isUnique": false,
3305+ "concurrently": false,
3306+ "method": "gin",
3307+ "with": {}
3308+ }
3309+ },
3310+ "foreignKeys": {},
3311+ "compositePrimaryKeys": {},
3312+ "uniqueConstraints": {},
3313+ "policies": {},
3314+ "checkConstraints": {},
3315+ "isRLSEnabled": false
3316+ },
3317+ "public.grade_premiums": {
3318+ "name": "grade_premiums",
3319+ "schema": "",
3320+ "columns": {
3321+ "id": {
3322+ "name": "id",
3323+ "type": "text",
3324+ "primaryKey": true,
3325+ "notNull": true
3326+ },
3327+ "category_slug": {
3328+ "name": "category_slug",
3329+ "type": "text",
3330+ "primaryKey": false,
3331+ "notNull": true
3332+ },
3333+ "grader": {
3334+ "name": "grader",
3335+ "type": "text",
3336+ "primaryKey": false,
3337+ "notNull": true
3338+ },
3339+ "grade": {
3340+ "name": "grade",
3341+ "type": "text",
3342+ "primaryKey": false,
3343+ "notNull": true
3344+ },
3345+ "market_multiplier": {
3346+ "name": "market_multiplier",
3347+ "type": "real",
3348+ "primaryKey": false,
3349+ "notNull": true
3350+ },
3351+ "sample_size": {
3352+ "name": "sample_size",
3353+ "type": "integer",
3354+ "primaryKey": false,
3355+ "notNull": true
3356+ },
3357+ "computed_at": {
3358+ "name": "computed_at",
3359+ "type": "timestamp with time zone",
3360+ "primaryKey": false,
3361+ "notNull": true
3362+ }
3363+ },
3364+ "indexes": {
3365+ "grade_premiums_uq": {
3366+ "name": "grade_premiums_uq",
3367+ "columns": [
3368+ {
3369+ "expression": "category_slug",
3370+ "isExpression": false,
3371+ "asc": true,
3372+ "nulls": "last"
3373+ },
3374+ {
3375+ "expression": "grader",
3376+ "isExpression": false,
3377+ "asc": true,
3378+ "nulls": "last"
3379+ },
3380+ {
3381+ "expression": "grade",
3382+ "isExpression": false,
3383+ "asc": true,
3384+ "nulls": "last"
3385+ }
3386+ ],
3387+ "isUnique": true,
3388+ "concurrently": false,
3389+ "method": "btree",
3390+ "with": {}
3391+ }
3392+ },
3393+ "foreignKeys": {},
3394+ "compositePrimaryKeys": {},
3395+ "uniqueConstraints": {},
3396+ "policies": {},
3397+ "checkConstraints": {},
3398+ "isRLSEnabled": false
3399+ },
3400+ "public.images": {
3401+ "name": "images",
3402+ "schema": "",
3403+ "columns": {
3404+ "id": {
3405+ "name": "id",
3406+ "type": "text",
3407+ "primaryKey": true,
3408+ "notNull": true
3409+ },
3410+ "asset_id": {
3411+ "name": "asset_id",
3412+ "type": "text",
3413+ "primaryKey": false,
3414+ "notNull": false
3415+ },
3416+ "listing_id": {
3417+ "name": "listing_id",
3418+ "type": "text",
3419+ "primaryKey": false,
3420+ "notNull": false
3421+ },
3422+ "sale_id": {
3423+ "name": "sale_id",
3424+ "type": "text",
3425+ "primaryKey": false,
3426+ "notNull": false
3427+ },
3428+ "source_id": {
3429+ "name": "source_id",
3430+ "type": "text",
3431+ "primaryKey": false,
3432+ "notNull": false
3433+ },
3434+ "url": {
3435+ "name": "url",
3436+ "type": "text",
3437+ "primaryKey": false,
3438+ "notNull": true
3439+ },
3440+ "role": {
3441+ "name": "role",
3442+ "type": "text",
3443+ "primaryKey": false,
3444+ "notNull": true,
3445+ "default": "'gallery'"
3446+ },
3447+ "width": {
3448+ "name": "width",
3449+ "type": "integer",
3450+ "primaryKey": false,
3451+ "notNull": false
3452+ },
3453+ "height": {
3454+ "name": "height",
3455+ "type": "integer",
3456+ "primaryKey": false,
3457+ "notNull": false
3458+ },
3459+ "phash": {
3460+ "name": "phash",
3461+ "type": "text",
3462+ "primaryKey": false,
3463+ "notNull": false
3464+ },
3465+ "embedding": {
3466+ "name": "embedding",
3467+ "type": "vector(512)",
3468+ "primaryKey": false,
3469+ "notNull": false
3470+ },
3471+ "attribution": {
3472+ "name": "attribution",
3473+ "type": "text",
3474+ "primaryKey": false,
3475+ "notNull": false
3476+ },
3477+ "status": {
3478+ "name": "status",
3479+ "type": "text",
3480+ "primaryKey": false,
3481+ "notNull": true,
3482+ "default": "'unchecked'"
3483+ },
3484+ "checked_at": {
3485+ "name": "checked_at",
3486+ "type": "timestamp with time zone",
3487+ "primaryKey": false,
3488+ "notNull": false
3489+ },
3490+ "bytes": {
3491+ "name": "bytes",
3492+ "type": "integer",
3493+ "primaryKey": false,
3494+ "notNull": false
3495+ },
3496+ "content_type": {
3497+ "name": "content_type",
3498+ "type": "text",
3499+ "primaryKey": false,
3500+ "notNull": false
3501+ },
3502+ "cache_key": {
3503+ "name": "cache_key",
3504+ "type": "text",
3505+ "primaryKey": false,
3506+ "notNull": false
3507+ },
3508+ "error": {
3509+ "name": "error",
3510+ "type": "text",
3511+ "primaryKey": false,
3512+ "notNull": false
3513+ },
3514+ "created_at": {
3515+ "name": "created_at",
3516+ "type": "timestamp with time zone",
3517+ "primaryKey": false,
3518+ "notNull": true,
3519+ "default": "now()"
3520+ }
3521+ },
3522+ "indexes": {
3523+ "images_asset_idx": {
3524+ "name": "images_asset_idx",
3525+ "columns": [
3526+ {
3527+ "expression": "asset_id",
3528+ "isExpression": false,
3529+ "asc": true,
3530+ "nulls": "last"
3531+ }
3532+ ],
3533+ "isUnique": false,
3534+ "concurrently": false,
3535+ "method": "btree",
3536+ "with": {}
3537+ },
3538+ "images_phash_idx": {
3539+ "name": "images_phash_idx",
3540+ "columns": [
3541+ {
3542+ "expression": "phash",
3543+ "isExpression": false,
3544+ "asc": true,
3545+ "nulls": "last"
3546+ }
3547+ ],
3548+ "isUnique": false,
3549+ "concurrently": false,
3550+ "method": "btree",
3551+ "with": {}
3552+ },
3553+ "images_url_uq": {
3554+ "name": "images_url_uq",
3555+ "columns": [
3556+ {
3557+ "expression": "url",
3558+ "isExpression": false,
3559+ "asc": true,
3560+ "nulls": "last"
3561+ }
3562+ ],
3563+ "isUnique": true,
3564+ "concurrently": false,
3565+ "method": "btree",
3566+ "with": {}
3567+ },
3568+ "images_cache_key_idx": {
3569+ "name": "images_cache_key_idx",
3570+ "columns": [
3571+ {
3572+ "expression": "cache_key",
3573+ "isExpression": false,
3574+ "asc": true,
3575+ "nulls": "last"
3576+ }
3577+ ],
3578+ "isUnique": false,
3579+ "concurrently": false,
3580+ "method": "btree",
3581+ "with": {}
3582+ },
3583+ "images_status_idx": {
3584+ "name": "images_status_idx",
3585+ "columns": [
3586+ {
3587+ "expression": "status",
3588+ "isExpression": false,
3589+ "asc": true,
3590+ "nulls": "last"
3591+ },
3592+ {
3593+ "expression": "checked_at",
3594+ "isExpression": false,
3595+ "asc": true,
3596+ "nulls": "last"
3597+ }
3598+ ],
3599+ "isUnique": false,
3600+ "concurrently": false,
3601+ "method": "btree",
3602+ "with": {}
3603+ }
3604+ },
3605+ "foreignKeys": {},
3606+ "compositePrimaryKeys": {},
3607+ "uniqueConstraints": {},
3608+ "policies": {},
3609+ "checkConstraints": {},
3610+ "isRLSEnabled": false
3611+ },
3612+ "public.population_reports": {
3613+ "name": "population_reports",
3614+ "schema": "",
3615+ "columns": {
3616+ "id": {
3617+ "name": "id",
3618+ "type": "text",
3619+ "primaryKey": true,
3620+ "notNull": true
3621+ },
3622+ "asset_id": {
3623+ "name": "asset_id",
3624+ "type": "text",
3625+ "primaryKey": false,
3626+ "notNull": true
3627+ },
3628+ "grader": {
3629+ "name": "grader",
3630+ "type": "text",
3631+ "primaryKey": false,
3632+ "notNull": true
3633+ },
3634+ "source_id": {
3635+ "name": "source_id",
3636+ "type": "text",
3637+ "primaryKey": false,
3638+ "notNull": true
3639+ },
3640+ "source_url": {
3641+ "name": "source_url",
3642+ "type": "text",
3643+ "primaryKey": false,
3644+ "notNull": false
3645+ },
3646+ "report_date": {
3647+ "name": "report_date",
3648+ "type": "text",
3649+ "primaryKey": false,
3650+ "notNull": true
3651+ },
3652+ "total": {
3653+ "name": "total",
3654+ "type": "integer",
3655+ "primaryKey": false,
3656+ "notNull": true
3657+ },
3658+ "by_grade": {
3659+ "name": "by_grade",
3660+ "type": "jsonb",
3661+ "primaryKey": false,
3662+ "notNull": true
3663+ },
3664+ "created_at": {
3665+ "name": "created_at",
3666+ "type": "timestamp with time zone",
3667+ "primaryKey": false,
3668+ "notNull": true,
3669+ "default": "now()"
3670+ }
3671+ },
3672+ "indexes": {
3673+ "population_reports_uq": {
3674+ "name": "population_reports_uq",
3675+ "columns": [
3676+ {
3677+ "expression": "asset_id",
3678+ "isExpression": false,
3679+ "asc": true,
3680+ "nulls": "last"
3681+ },
3682+ {
3683+ "expression": "grader",
3684+ "isExpression": false,
3685+ "asc": true,
3686+ "nulls": "last"
3687+ },
3688+ {
3689+ "expression": "report_date",
3690+ "isExpression": false,
3691+ "asc": true,
3692+ "nulls": "last"
3693+ }
3694+ ],
3695+ "isUnique": true,
3696+ "concurrently": false,
3697+ "method": "btree",
3698+ "with": {}
3699+ }
3700+ },
3701+ "foreignKeys": {},
3702+ "compositePrimaryKeys": {},
3703+ "uniqueConstraints": {},
3704+ "policies": {},
3705+ "checkConstraints": {},
3706+ "isRLSEnabled": false
3707+ },
3708+ "public.variant_stats": {
3709+ "name": "variant_stats",
3710+ "schema": "",
3711+ "columns": {
3712+ "variant_id": {
3713+ "name": "variant_id",
3714+ "type": "text",
3715+ "primaryKey": true,
3716+ "notNull": true
3717+ },
3718+ "asset_id": {
3719+ "name": "asset_id",
3720+ "type": "text",
3721+ "primaryKey": false,
3722+ "notNull": true
3723+ },
3724+ "riv_usd": {
3725+ "name": "riv_usd",
3726+ "type": "numeric(18, 4)",
3727+ "primaryKey": false,
3728+ "notNull": false
3729+ },
3730+ "riv_low_usd": {
3731+ "name": "riv_low_usd",
3732+ "type": "numeric(18, 4)",
3733+ "primaryKey": false,
3734+ "notNull": false
3735+ },
3736+ "riv_high_usd": {
3737+ "name": "riv_high_usd",
3738+ "type": "numeric(18, 4)",
3739+ "primaryKey": false,
3740+ "notNull": false
3741+ },
3742+ "riv_confidence": {
3743+ "name": "riv_confidence",
3744+ "type": "numeric(8, 6)",
3745+ "primaryKey": false,
3746+ "notNull": false
3747+ },
3748+ "riv_sample_size": {
3749+ "name": "riv_sample_size",
3750+ "type": "integer",
3751+ "primaryKey": false,
3752+ "notNull": true,
3753+ "default": 0
3754+ },
3755+ "latest_sale_usd": {
3756+ "name": "latest_sale_usd",
3757+ "type": "numeric(18, 4)",
3758+ "primaryKey": false,
3759+ "notNull": false
3760+ },
3761+ "latest_sale_at": {
3762+ "name": "latest_sale_at",
3763+ "type": "timestamp with time zone",
3764+ "primaryKey": false,
3765+ "notNull": false
3766+ },
3767+ "change_30d": {
3768+ "name": "change_30d",
3769+ "type": "numeric(8, 6)",
3770+ "primaryKey": false,
3771+ "notNull": false
3772+ },
3773+ "change_1y": {
3774+ "name": "change_1y",
3775+ "type": "numeric(8, 6)",
3776+ "primaryKey": false,
3777+ "notNull": false
3778+ },
3779+ "sales_count": {
3780+ "name": "sales_count",
3781+ "type": "integer",
3782+ "primaryKey": false,
3783+ "notNull": true,
3784+ "default": 0
3785+ },
3786+ "sales_30d": {
3787+ "name": "sales_30d",
3788+ "type": "integer",
3789+ "primaryKey": false,
3790+ "notNull": true,
3791+ "default": 0
3792+ },
3793+ "active_listings": {
3794+ "name": "active_listings",
3795+ "type": "integer",
3796+ "primaryKey": false,
3797+ "notNull": true,
3798+ "default": 0
3799+ },
3800+ "min_ask_usd": {
3801+ "name": "min_ask_usd",
3802+ "type": "numeric(18, 4)",
3803+ "primaryKey": false,
3804+ "notNull": false
3805+ },
3806+ "liquidity_score": {
3807+ "name": "liquidity_score",
3808+ "type": "real",
3809+ "primaryKey": false,
3810+ "notNull": false
3811+ },
3812+ "updated_at": {
3813+ "name": "updated_at",
3814+ "type": "timestamp with time zone",
3815+ "primaryKey": false,
3816+ "notNull": true,
3817+ "default": "now()"
3818+ }
3819+ },
3820+ "indexes": {
3821+ "variant_stats_asset_idx": {
3822+ "name": "variant_stats_asset_idx",
3823+ "columns": [
3824+ {
3825+ "expression": "asset_id",
3826+ "isExpression": false,
3827+ "asc": true,
3828+ "nulls": "last"
3829+ }
3830+ ],
3831+ "isUnique": false,
3832+ "concurrently": false,
3833+ "method": "btree",
3834+ "with": {}
3835+ }
3836+ },
3837+ "foreignKeys": {},
3838+ "compositePrimaryKeys": {},
3839+ "uniqueConstraints": {},
3840+ "policies": {},
3841+ "checkConstraints": {},
3842+ "isRLSEnabled": false
3843+ },
3844+ "public.auction_lots": {
3845+ "name": "auction_lots",
3846+ "schema": "",
3847+ "columns": {
3848+ "id": {
3849+ "name": "id",
3850+ "type": "text",
3851+ "primaryKey": true,
3852+ "notNull": true
3853+ },
3854+ "auction_id": {
3855+ "name": "auction_id",
3856+ "type": "text",
3857+ "primaryKey": false,
3858+ "notNull": true
3859+ },
3860+ "asset_id": {
3861+ "name": "asset_id",
3862+ "type": "text",
3863+ "primaryKey": false,
3864+ "notNull": false
3865+ },
3866+ "variant_id": {
3867+ "name": "variant_id",
3868+ "type": "text",
3869+ "primaryKey": false,
3870+ "notNull": false
3871+ },
3872+ "source_id": {
3873+ "name": "source_id",
3874+ "type": "text",
3875+ "primaryKey": false,
3876+ "notNull": true
3877+ },
3878+ "lot_number": {
3879+ "name": "lot_number",
3880+ "type": "text",
3881+ "primaryKey": false,
3882+ "notNull": false
3883+ },
3884+ "title": {
3885+ "name": "title",
3886+ "type": "text",
3887+ "primaryKey": false,
3888+ "notNull": true
3889+ },
3890+ "url": {
3891+ "name": "url",
3892+ "type": "text",
3893+ "primaryKey": false,
3894+ "notNull": true
3895+ },
3896+ "estimate_low": {
3897+ "name": "estimate_low",
3898+ "type": "numeric(18, 4)",
3899+ "primaryKey": false,
3900+ "notNull": false
3901+ },
3902+ "estimate_high": {
3903+ "name": "estimate_high",
3904+ "type": "numeric(18, 4)",
3905+ "primaryKey": false,
3906+ "notNull": false
3907+ },
3908+ "current_bid": {
3909+ "name": "current_bid",
3910+ "type": "numeric(18, 4)",
3911+ "primaryKey": false,
3912+ "notNull": false
3913+ },
3914+ "hammer_price": {
3915+ "name": "hammer_price",
3916+ "type": "numeric(18, 4)",
3917+ "primaryKey": false,
3918+ "notNull": false
3919+ },
3920+ "currency": {
3921+ "name": "currency",
3922+ "type": "text",
3923+ "primaryKey": false,
3924+ "notNull": false
3925+ },
3926+ "bid_count": {
3927+ "name": "bid_count",
3928+ "type": "integer",
3929+ "primaryKey": false,
3930+ "notNull": false
3931+ },
3932+ "starts_at": {
3933+ "name": "starts_at",
3934+ "type": "timestamp with time zone",
3935+ "primaryKey": false,
3936+ "notNull": false
3937+ },
3938+ "ends_at": {
3939+ "name": "ends_at",
3940+ "type": "timestamp with time zone",
3941+ "primaryKey": false,
3942+ "notNull": false
3943+ },
3944+ "status": {
3945+ "name": "status",
3946+ "type": "text",
3947+ "primaryKey": false,
3948+ "notNull": true,
3949+ "default": "'upcoming'"
3950+ },
3951+ "image_urls": {
3952+ "name": "image_urls",
3953+ "type": "jsonb",
3954+ "primaryKey": false,
3955+ "notNull": true,
3956+ "default": "'[]'::jsonb"
3957+ },
3958+ "grader": {
3959+ "name": "grader",
3960+ "type": "text",
3961+ "primaryKey": false,
3962+ "notNull": false
3963+ },
3964+ "grade": {
3965+ "name": "grade",
3966+ "type": "text",
3967+ "primaryKey": false,
3968+ "notNull": false
3969+ },
3970+ "created_at": {
3971+ "name": "created_at",
3972+ "type": "timestamp with time zone",
3973+ "primaryKey": false,
3974+ "notNull": true,
3975+ "default": "now()"
3976+ },
3977+ "updated_at": {
3978+ "name": "updated_at",
3979+ "type": "timestamp with time zone",
3980+ "primaryKey": false,
3981+ "notNull": true,
3982+ "default": "now()"
3983+ }
3984+ },
3985+ "indexes": {
3986+ "auction_lots_url_uq": {
3987+ "name": "auction_lots_url_uq",
3988+ "columns": [
3989+ {
3990+ "expression": "url",
3991+ "isExpression": false,
3992+ "asc": true,
3993+ "nulls": "last"
3994+ }
3995+ ],
3996+ "isUnique": true,
3997+ "concurrently": false,
3998+ "method": "btree",
3999+ "with": {}
4000+ },
4001+ "auction_lots_auction_idx": {
4002+ "name": "auction_lots_auction_idx",
4003+ "columns": [
4004+ {
4005+ "expression": "auction_id",
4006+ "isExpression": false,
4007+ "asc": true,
4008+ "nulls": "last"
4009+ }
4010+ ],
4011+ "isUnique": false,
4012+ "concurrently": false,
4013+ "method": "btree",
4014+ "with": {}
4015+ },
4016+ "auction_lots_asset_idx": {
4017+ "name": "auction_lots_asset_idx",
4018+ "columns": [
4019+ {
4020+ "expression": "asset_id",
4021+ "isExpression": false,
4022+ "asc": true,
4023+ "nulls": "last"
4024+ }
4025+ ],
4026+ "isUnique": false,
4027+ "concurrently": false,
4028+ "method": "btree",
4029+ "with": {}
4030+ },
4031+ "auction_lots_ends_idx": {
4032+ "name": "auction_lots_ends_idx",
4033+ "columns": [
4034+ {
4035+ "expression": "ends_at",
4036+ "isExpression": false,
4037+ "asc": true,
4038+ "nulls": "last"
4039+ }
4040+ ],
4041+ "isUnique": false,
4042+ "concurrently": false,
4043+ "method": "btree",
4044+ "with": {}
4045+ }
4046+ },
4047+ "foreignKeys": {},
4048+ "compositePrimaryKeys": {},
4049+ "uniqueConstraints": {},
4050+ "policies": {},
4051+ "checkConstraints": {},
4052+ "isRLSEnabled": false
4053+ },
4054+ "public.auctions": {
4055+ "name": "auctions",
4056+ "schema": "",
4057+ "columns": {
4058+ "id": {
4059+ "name": "id",
4060+ "type": "text",
4061+ "primaryKey": true,
4062+ "notNull": true
4063+ },
4064+ "source_id": {
4065+ "name": "source_id",
4066+ "type": "text",
4067+ "primaryKey": false,
4068+ "notNull": true
4069+ },
4070+ "auction_house": {
4071+ "name": "auction_house",
4072+ "type": "text",
4073+ "primaryKey": false,
4074+ "notNull": true
4075+ },
4076+ "name": {
4077+ "name": "name",
4078+ "type": "text",
4079+ "primaryKey": false,
4080+ "notNull": true
4081+ },
4082+ "url": {
4083+ "name": "url",
4084+ "type": "text",
4085+ "primaryKey": false,
4086+ "notNull": true
4087+ },
4088+ "starts_at": {
4089+ "name": "starts_at",
4090+ "type": "timestamp with time zone",
4091+ "primaryKey": false,
4092+ "notNull": false
4093+ },
4094+ "ends_at": {
4095+ "name": "ends_at",
4096+ "type": "timestamp with time zone",
4097+ "primaryKey": false,
4098+ "notNull": false
4099+ },
4100+ "location": {
4101+ "name": "location",
4102+ "type": "text",
4103+ "primaryKey": false,
4104+ "notNull": false
4105+ },
4106+ "category_slugs": {
4107+ "name": "category_slugs",
4108+ "type": "text[]",
4109+ "primaryKey": false,
4110+ "notNull": true,
4111+ "default": "'{}'::text[]"
4112+ },
4113+ "lot_count": {
4114+ "name": "lot_count",
4115+ "type": "integer",
4116+ "primaryKey": false,
4117+ "notNull": false
4118+ },
4119+ "status": {
4120+ "name": "status",
4121+ "type": "text",
4122+ "primaryKey": false,
4123+ "notNull": true,
4124+ "default": "'upcoming'"
4125+ },
4126+ "currency": {
4127+ "name": "currency",
4128+ "type": "text",
4129+ "primaryKey": false,
4130+ "notNull": false
4131+ },
4132+ "created_at": {
4133+ "name": "created_at",
4134+ "type": "timestamp with time zone",
4135+ "primaryKey": false,
4136+ "notNull": true,
4137+ "default": "now()"
4138+ },
4139+ "updated_at": {
4140+ "name": "updated_at",
4141+ "type": "timestamp with time zone",
4142+ "primaryKey": false,
4143+ "notNull": true,
4144+ "default": "now()"
4145+ }
4146+ },
4147+ "indexes": {
4148+ "auctions_url_uq": {
4149+ "name": "auctions_url_uq",
4150+ "columns": [
4151+ {
4152+ "expression": "url",
4153+ "isExpression": false,
4154+ "asc": true,
4155+ "nulls": "last"
4156+ }
4157+ ],
4158+ "isUnique": true,
4159+ "concurrently": false,
4160+ "method": "btree",
4161+ "with": {}
4162+ },
4163+ "auctions_ends_idx": {
4164+ "name": "auctions_ends_idx",
4165+ "columns": [
4166+ {
4167+ "expression": "ends_at",
4168+ "isExpression": false,
4169+ "asc": true,
4170+ "nulls": "last"
4171+ }
4172+ ],
4173+ "isUnique": false,
4174+ "concurrently": false,
4175+ "method": "btree",
4176+ "with": {}
4177+ }
4178+ },
4179+ "foreignKeys": {},
4180+ "compositePrimaryKeys": {},
4181+ "uniqueConstraints": {},
4182+ "policies": {},
4183+ "checkConstraints": {},
4184+ "isRLSEnabled": false
4185+ },
4186+ "public.cross_listing_groups": {
4187+ "name": "cross_listing_groups",
4188+ "schema": "",
4189+ "columns": {
4190+ "id": {
4191+ "name": "id",
4192+ "type": "text",
4193+ "primaryKey": true,
4194+ "notNull": true
4195+ },
4196+ "asset_id": {
4197+ "name": "asset_id",
4198+ "type": "text",
4199+ "primaryKey": false,
4200+ "notNull": false
4201+ },
4202+ "signals": {
4203+ "name": "signals",
4204+ "type": "jsonb",
4205+ "primaryKey": false,
4206+ "notNull": true,
4207+ "default": "'{}'::jsonb"
4208+ },
4209+ "created_at": {
4210+ "name": "created_at",
4211+ "type": "timestamp with time zone",
4212+ "primaryKey": false,
4213+ "notNull": true,
4214+ "default": "now()"
4215+ }
4216+ },
4217+ "indexes": {},
4218+ "foreignKeys": {},
4219+ "compositePrimaryKeys": {},
4220+ "uniqueConstraints": {},
4221+ "policies": {},
4222+ "checkConstraints": {},
4223+ "isRLSEnabled": false
4224+ },
4225+ "public.fx_rates": {
4226+ "name": "fx_rates",
4227+ "schema": "",
4228+ "columns": {
4229+ "date": {
4230+ "name": "date",
4231+ "type": "date",
4232+ "primaryKey": false,
4233+ "notNull": true
4234+ },
4235+ "base": {
4236+ "name": "base",
4237+ "type": "text",
4238+ "primaryKey": false,
4239+ "notNull": true
4240+ },
4241+ "quote": {
4242+ "name": "quote",
4243+ "type": "text",
4244+ "primaryKey": false,
4245+ "notNull": true
4246+ },
4247+ "rate": {
4248+ "name": "rate",
4249+ "type": "real",
4250+ "primaryKey": false,
4251+ "notNull": true
4252+ },
4253+ "source": {
4254+ "name": "source",
4255+ "type": "text",
4256+ "primaryKey": false,
4257+ "notNull": true,
4258+ "default": "'ecb'"
4259+ }
4260+ },
4261+ "indexes": {
4262+ "fx_rates_uq": {
4263+ "name": "fx_rates_uq",
4264+ "columns": [
4265+ {
4266+ "expression": "date",
4267+ "isExpression": false,
4268+ "asc": true,
4269+ "nulls": "last"
4270+ },
4271+ {
4272+ "expression": "base",
4273+ "isExpression": false,
4274+ "asc": true,
4275+ "nulls": "last"
4276+ },
4277+ {
4278+ "expression": "quote",
4279+ "isExpression": false,
4280+ "asc": true,
4281+ "nulls": "last"
4282+ }
4283+ ],
4284+ "isUnique": true,
4285+ "concurrently": false,
4286+ "method": "btree",
4287+ "with": {}
4288+ }
4289+ },
4290+ "foreignKeys": {},
4291+ "compositePrimaryKeys": {},
4292+ "uniqueConstraints": {},
4293+ "policies": {},
4294+ "checkConstraints": {},
4295+ "isRLSEnabled": false
4296+ },
4297+ "public.listing_events": {
4298+ "name": "listing_events",
4299+ "schema": "",
4300+ "columns": {
4301+ "id": {
4302+ "name": "id",
4303+ "type": "text",
4304+ "primaryKey": true,
4305+ "notNull": true
4306+ },
4307+ "listing_id": {
4308+ "name": "listing_id",
4309+ "type": "text",
4310+ "primaryKey": false,
4311+ "notNull": true
4312+ },
4313+ "event_type": {
4314+ "name": "event_type",
4315+ "type": "text",
4316+ "primaryKey": false,
4317+ "notNull": true
4318+ },
4319+ "old_price": {
4320+ "name": "old_price",
4321+ "type": "numeric(18, 4)",
4322+ "primaryKey": false,
4323+ "notNull": false
4324+ },
4325+ "new_price": {
4326+ "name": "new_price",
4327+ "type": "numeric(18, 4)",
4328+ "primaryKey": false,
4329+ "notNull": false
4330+ },
4331+ "currency": {
4332+ "name": "currency",
4333+ "type": "text",
4334+ "primaryKey": false,
4335+ "notNull": false
4336+ },
4337+ "occurred_at": {
4338+ "name": "occurred_at",
4339+ "type": "timestamp with time zone",
4340+ "primaryKey": false,
4341+ "notNull": true
4342+ }
4343+ },
4344+ "indexes": {
4345+ "listing_events_listing_idx": {
4346+ "name": "listing_events_listing_idx",
4347+ "columns": [
4348+ {
4349+ "expression": "listing_id",
4350+ "isExpression": false,
4351+ "asc": true,
4352+ "nulls": "last"
4353+ },
4354+ {
4355+ "expression": "occurred_at",
4356+ "isExpression": false,
4357+ "asc": true,
4358+ "nulls": "last"
4359+ }
4360+ ],
4361+ "isUnique": false,
4362+ "concurrently": false,
4363+ "method": "btree",
4364+ "with": {}
4365+ }
4366+ },
4367+ "foreignKeys": {},
4368+ "compositePrimaryKeys": {},
4369+ "uniqueConstraints": {},
4370+ "policies": {},
4371+ "checkConstraints": {},
4372+ "isRLSEnabled": false
4373+ },
4374+ "public.listings": {
4375+ "name": "listings",
4376+ "schema": "",
4377+ "columns": {
4378+ "id": {
4379+ "name": "id",
4380+ "type": "text",
4381+ "primaryKey": true,
4382+ "notNull": true
4383+ },
4384+ "asset_id": {
4385+ "name": "asset_id",
4386+ "type": "text",
4387+ "primaryKey": false,
4388+ "notNull": true
4389+ },
4390+ "variant_id": {
4391+ "name": "variant_id",
4392+ "type": "text",
4393+ "primaryKey": false,
4394+ "notNull": false
4395+ },
4396+ "source_id": {
4397+ "name": "source_id",
4398+ "type": "text",
4399+ "primaryKey": false,
4400+ "notNull": true
4401+ },
4402+ "connector_id": {
4403+ "name": "connector_id",
4404+ "type": "text",
4405+ "primaryKey": false,
4406+ "notNull": true
4407+ },
4408+ "raw_record_id": {
4409+ "name": "raw_record_id",
4410+ "type": "text",
4411+ "primaryKey": false,
4412+ "notNull": false
4413+ },
4414+ "source_url": {
4415+ "name": "source_url",
4416+ "type": "text",
4417+ "primaryKey": false,
4418+ "notNull": true
4419+ },
4420+ "external_id": {
4421+ "name": "external_id",
4422+ "type": "text",
4423+ "primaryKey": false,
4424+ "notNull": true
4425+ },
4426+ "listing_type": {
4427+ "name": "listing_type",
4428+ "type": "text",
4429+ "primaryKey": false,
4430+ "notNull": true,
4431+ "default": "'unknown'"
4432+ },
4433+ "price": {
4434+ "name": "price",
4435+ "type": "numeric(18, 4)",
4436+ "primaryKey": false,
4437+ "notNull": false
4438+ },
4439+ "currency": {
4440+ "name": "currency",
4441+ "type": "text",
4442+ "primaryKey": false,
4443+ "notNull": false
4444+ },
4445+ "price_usd": {
4446+ "name": "price_usd",
4447+ "type": "numeric(18, 4)",
4448+ "primaryKey": false,
4449+ "notNull": false
4450+ },
4451+ "seller": {
4452+ "name": "seller",
4453+ "type": "text",
4454+ "primaryKey": false,
4455+ "notNull": false
4456+ },
4457+ "seller_reputation": {
4458+ "name": "seller_reputation",
4459+ "type": "text",
4460+ "primaryKey": false,
4461+ "notNull": false
4462+ },
4463+ "location": {
4464+ "name": "location",
4465+ "type": "text",
4466+ "primaryKey": false,
4467+ "notNull": false
4468+ },
4469+ "shipping_cost": {
4470+ "name": "shipping_cost",
4471+ "type": "numeric(18, 4)",
4472+ "primaryKey": false,
4473+ "notNull": false
4474+ },
4475+ "quantity": {
4476+ "name": "quantity",
4477+ "type": "integer",
4478+ "primaryKey": false,
4479+ "notNull": false
4480+ },
4481+ "condition": {
4482+ "name": "condition",
4483+ "type": "text",
4484+ "primaryKey": false,
4485+ "notNull": false
4486+ },
4487+ "grader": {
4488+ "name": "grader",
4489+ "type": "text",
4490+ "primaryKey": false,
4491+ "notNull": false
4492+ },
4493+ "grade": {
4494+ "name": "grade",
4495+ "type": "text",
4496+ "primaryKey": false,
4497+ "notNull": false
4498+ },
4499+ "certification_number": {
4500+ "name": "certification_number",
4501+ "type": "text",
4502+ "primaryKey": false,
4503+ "notNull": false
4504+ },
4505+ "image_urls": {
4506+ "name": "image_urls",
4507+ "type": "jsonb",
4508+ "primaryKey": false,
4509+ "notNull": true,
4510+ "default": "'[]'::jsonb"
4511+ },
4512+ "raw_title": {
4513+ "name": "raw_title",
4514+ "type": "text",
4515+ "primaryKey": false,
4516+ "notNull": true
4517+ },
4518+ "description": {
4519+ "name": "description",
4520+ "type": "text",
4521+ "primaryKey": false,
4522+ "notNull": false
4523+ },
4524+ "listed_at": {
4525+ "name": "listed_at",
4526+ "type": "timestamp with time zone",
4527+ "primaryKey": false,
4528+ "notNull": false
4529+ },
4530+ "ends_at": {
4531+ "name": "ends_at",
4532+ "type": "timestamp with time zone",
4533+ "primaryKey": false,
4534+ "notNull": false
4535+ },
4536+ "availability": {
4537+ "name": "availability",
4538+ "type": "text",
4539+ "primaryKey": false,
4540+ "notNull": true,
4541+ "default": "'available'"
4542+ },
4543+ "bid_count": {
4544+ "name": "bid_count",
4545+ "type": "integer",
4546+ "primaryKey": false,
4547+ "notNull": false
4548+ },
4549+ "first_seen_at": {
4550+ "name": "first_seen_at",
4551+ "type": "timestamp with time zone",
4552+ "primaryKey": false,
4553+ "notNull": true
4554+ },
4555+ "last_seen_at": {
4556+ "name": "last_seen_at",
4557+ "type": "timestamp with time zone",
4558+ "primaryKey": false,
4559+ "notNull": true
4560+ },
4561+ "price_changed_at": {
4562+ "name": "price_changed_at",
4563+ "type": "timestamp with time zone",
4564+ "primaryKey": false,
4565+ "notNull": false
4566+ },
4567+ "cross_listing_group_id": {
4568+ "name": "cross_listing_group_id",
4569+ "type": "text",
4570+ "primaryKey": false,
4571+ "notNull": false
4572+ },
4573+ "confidence": {
4574+ "name": "confidence",
4575+ "type": "numeric(8, 6)",
4576+ "primaryKey": false,
4577+ "notNull": true,
4578+ "default": 0.8
4579+ },
4580+ "data_quality": {
4581+ "name": "data_quality",
4582+ "type": "real",
4583+ "primaryKey": false,
4584+ "notNull": true,
4585+ "default": 0
4586+ },
4587+ "flags": {
4588+ "name": "flags",
4589+ "type": "text[]",
4590+ "primaryKey": false,
4591+ "notNull": true,
4592+ "default": "'{}'::text[]"
4593+ },
4594+ "discount_to_riv": {
4595+ "name": "discount_to_riv",
4596+ "type": "numeric(8, 6)",
4597+ "primaryKey": false,
4598+ "notNull": false
4599+ },
4600+ "created_at": {
4601+ "name": "created_at",
4602+ "type": "timestamp with time zone",
4603+ "primaryKey": false,
4604+ "notNull": true,
4605+ "default": "now()"
4606+ },
4607+ "updated_at": {
4608+ "name": "updated_at",
4609+ "type": "timestamp with time zone",
4610+ "primaryKey": false,
4611+ "notNull": true,
4612+ "default": "now()"
4613+ }
4614+ },
4615+ "indexes": {
4616+ "listings_source_external_uq": {
4617+ "name": "listings_source_external_uq",
4618+ "columns": [
4619+ {
4620+ "expression": "source_id",
4621+ "isExpression": false,
4622+ "asc": true,
4623+ "nulls": "last"
4624+ },
4625+ {
4626+ "expression": "external_id",
4627+ "isExpression": false,
4628+ "asc": true,
4629+ "nulls": "last"
4630+ }
4631+ ],
4632+ "isUnique": true,
4633+ "concurrently": false,
4634+ "method": "btree",
4635+ "with": {}
4636+ },
4637+ "listings_asset_avail_idx": {
4638+ "name": "listings_asset_avail_idx",
4639+ "columns": [
4640+ {
4641+ "expression": "asset_id",
4642+ "isExpression": false,
4643+ "asc": true,
4644+ "nulls": "last"
4645+ },
4646+ {
4647+ "expression": "availability",
4648+ "isExpression": false,
4649+ "asc": true,
4650+ "nulls": "last"
4651+ }
4652+ ],
4653+ "isUnique": false,
4654+ "concurrently": false,
4655+ "method": "btree",
4656+ "with": {}
4657+ },
4658+ "listings_avail_price_idx": {
4659+ "name": "listings_avail_price_idx",
4660+ "columns": [
4661+ {
4662+ "expression": "availability",
4663+ "isExpression": false,
4664+ "asc": true,
4665+ "nulls": "last"
4666+ },
4667+ {
4668+ "expression": "price_usd",
4669+ "isExpression": false,
4670+ "asc": true,
4671+ "nulls": "last"
4672+ }
4673+ ],
4674+ "isUnique": false,
4675+ "concurrently": false,
4676+ "method": "btree",
4677+ "with": {}
4678+ },
4679+ "listings_ends_idx": {
4680+ "name": "listings_ends_idx",
4681+ "columns": [
4682+ {
4683+ "expression": "ends_at",
4684+ "isExpression": false,
4685+ "asc": true,
4686+ "nulls": "last"
4687+ }
4688+ ],
4689+ "isUnique": false,
4690+ "concurrently": false,
4691+ "method": "btree",
4692+ "with": {}
4693+ },
4694+ "listings_last_seen_idx": {
4695+ "name": "listings_last_seen_idx",
4696+ "columns": [
4697+ {
4698+ "expression": "last_seen_at",
4699+ "isExpression": false,
4700+ "asc": true,
4701+ "nulls": "last"
4702+ }
4703+ ],
4704+ "isUnique": false,
4705+ "concurrently": false,
4706+ "method": "btree",
4707+ "with": {}
4708+ },
4709+ "listings_variant_avail_idx": {
4710+ "name": "listings_variant_avail_idx",
4711+ "columns": [
4712+ {
4713+ "expression": "variant_id",
4714+ "isExpression": false,
4715+ "asc": true,
4716+ "nulls": "last"
4717+ },
4718+ {
4719+ "expression": "availability",
4720+ "isExpression": false,
4721+ "asc": true,
4722+ "nulls": "last"
4723+ }
4724+ ],
4725+ "isUnique": false,
4726+ "concurrently": false,
4727+ "method": "btree",
4728+ "with": {}
4729+ },
4730+ "listings_discount_idx": {
4731+ "name": "listings_discount_idx",
4732+ "columns": [
4733+ {
4734+ "expression": "discount_to_riv",
4735+ "isExpression": false,
4736+ "asc": true,
4737+ "nulls": "last"
4738+ }
4739+ ],
4740+ "isUnique": false,
4741+ "where": "discount_to_riv is not null and availability = 'available'",
4742+ "concurrently": false,
4743+ "method": "btree",
4744+ "with": {}
4745+ }
4746+ },
4747+ "foreignKeys": {},
4748+ "compositePrimaryKeys": {},
4749+ "uniqueConstraints": {},
4750+ "policies": {},
4751+ "checkConstraints": {},
4752+ "isRLSEnabled": false
4753+ },
4754+ "public.news": {
4755+ "name": "news",
4756+ "schema": "",
4757+ "columns": {
4758+ "id": {
4759+ "name": "id",
4760+ "type": "text",
4761+ "primaryKey": true,
4762+ "notNull": true
4763+ },
4764+ "source_id": {
4765+ "name": "source_id",
4766+ "type": "text",
4767+ "primaryKey": false,
4768+ "notNull": true
4769+ },
4770+ "url": {
4771+ "name": "url",
4772+ "type": "text",
4773+ "primaryKey": false,
4774+ "notNull": true
4775+ },
4776+ "title": {
4777+ "name": "title",
4778+ "type": "text",
4779+ "primaryKey": false,
4780+ "notNull": true
4781+ },
4782+ "summary": {
4783+ "name": "summary",
4784+ "type": "text",
4785+ "primaryKey": false,
4786+ "notNull": false
4787+ },
4788+ "ai_summary": {
4789+ "name": "ai_summary",
4790+ "type": "text",
4791+ "primaryKey": false,
4792+ "notNull": false
4793+ },
4794+ "published_at": {
4795+ "name": "published_at",
4796+ "type": "timestamp with time zone",
4797+ "primaryKey": false,
4798+ "notNull": false
4799+ },
4800+ "category_slugs": {
4801+ "name": "category_slugs",
4802+ "type": "text[]",
4803+ "primaryKey": false,
4804+ "notNull": true,
4805+ "default": "'{}'::text[]"
4806+ },
4807+ "news_type": {
4808+ "name": "news_type",
4809+ "type": "text",
4810+ "primaryKey": false,
4811+ "notNull": false
4812+ },
4813+ "image_url": {
4814+ "name": "image_url",
4815+ "type": "text",
4816+ "primaryKey": false,
4817+ "notNull": false
4818+ },
4819+ "fetched_at": {
4820+ "name": "fetched_at",
4821+ "type": "timestamp with time zone",
4822+ "primaryKey": false,
4823+ "notNull": true
4824+ }
4825+ },
4826+ "indexes": {
4827+ "news_url_uq": {
4828+ "name": "news_url_uq",
4829+ "columns": [
4830+ {
4831+ "expression": "url",
4832+ "isExpression": false,
4833+ "asc": true,
4834+ "nulls": "last"
4835+ }
4836+ ],
4837+ "isUnique": true,
4838+ "concurrently": false,
4839+ "method": "btree",
4840+ "with": {}
4841+ },
4842+ "news_published_idx": {
4843+ "name": "news_published_idx",
4844+ "columns": [
4845+ {
4846+ "expression": "published_at",
4847+ "isExpression": false,
4848+ "asc": true,
4849+ "nulls": "last"
4850+ }
4851+ ],
4852+ "isUnique": false,
4853+ "concurrently": false,
4854+ "method": "btree",
4855+ "with": {}
4856+ }
4857+ },
4858+ "foreignKeys": {},
4859+ "compositePrimaryKeys": {},
4860+ "uniqueConstraints": {},
4861+ "policies": {},
4862+ "checkConstraints": {},
4863+ "isRLSEnabled": false
4864+ },
4865+ "public.price_observations": {
4866+ "name": "price_observations",
4867+ "schema": "",
4868+ "columns": {
4869+ "id": {
4870+ "name": "id",
4871+ "type": "text",
4872+ "primaryKey": true,
4873+ "notNull": true
4874+ },
4875+ "asset_id": {
4876+ "name": "asset_id",
4877+ "type": "text",
4878+ "primaryKey": false,
4879+ "notNull": true
4880+ },
4881+ "variant_id": {
4882+ "name": "variant_id",
4883+ "type": "text",
4884+ "primaryKey": false,
4885+ "notNull": false
4886+ },
4887+ "source_id": {
4888+ "name": "source_id",
4889+ "type": "text",
4890+ "primaryKey": false,
4891+ "notNull": true
4892+ },
4893+ "connector_id": {
4894+ "name": "connector_id",
4895+ "type": "text",
4896+ "primaryKey": false,
4897+ "notNull": true
4898+ },
4899+ "raw_record_id": {
4900+ "name": "raw_record_id",
4901+ "type": "text",
4902+ "primaryKey": false,
4903+ "notNull": false
4904+ },
4905+ "source_url": {
4906+ "name": "source_url",
4907+ "type": "text",
4908+ "primaryKey": false,
4909+ "notNull": true
4910+ },
4911+ "price_kind": {
4912+ "name": "price_kind",
4913+ "type": "text",
4914+ "primaryKey": false,
4915+ "notNull": true
4916+ },
4917+ "price": {
4918+ "name": "price",
4919+ "type": "numeric(18, 4)",
4920+ "primaryKey": false,
4921+ "notNull": true
4922+ },
4923+ "currency": {
4924+ "name": "currency",
4925+ "type": "text",
4926+ "primaryKey": false,
4927+ "notNull": true
4928+ },
4929+ "price_usd": {
4930+ "name": "price_usd",
4931+ "type": "numeric(18, 4)",
4932+ "primaryKey": false,
4933+ "notNull": true
4934+ },
4935+ "observation_date": {
4936+ "name": "observation_date",
4937+ "type": "date",
4938+ "primaryKey": false,
4939+ "notNull": true
4940+ },
4941+ "sample_size": {
4942+ "name": "sample_size",
4943+ "type": "integer",
4944+ "primaryKey": false,
4945+ "notNull": false
4946+ },
4947+ "dedupe_key": {
4948+ "name": "dedupe_key",
4949+ "type": "text",
4950+ "primaryKey": false,
4951+ "notNull": true
4952+ },
4953+ "created_at": {
4954+ "name": "created_at",
4955+ "type": "timestamp with time zone",
4956+ "primaryKey": false,
4957+ "notNull": true,
4958+ "default": "now()"
4959+ }
4960+ },
4961+ "indexes": {
4962+ "price_observations_dedupe_uq": {
4963+ "name": "price_observations_dedupe_uq",
4964+ "columns": [
4965+ {
4966+ "expression": "dedupe_key",
4967+ "isExpression": false,
4968+ "asc": true,
4969+ "nulls": "last"
4970+ }
4971+ ],
4972+ "isUnique": true,
4973+ "concurrently": false,
4974+ "method": "btree",
4975+ "with": {}
4976+ },
4977+ "price_observations_asset_date_idx": {
4978+ "name": "price_observations_asset_date_idx",
4979+ "columns": [
4980+ {
4981+ "expression": "asset_id",
4982+ "isExpression": false,
4983+ "asc": true,
4984+ "nulls": "last"
4985+ },
4986+ {
4987+ "expression": "observation_date",
4988+ "isExpression": false,
4989+ "asc": true,
4990+ "nulls": "last"
4991+ }
4992+ ],
4993+ "isUnique": false,
4994+ "concurrently": false,
4995+ "method": "btree",
4996+ "with": {}
4997+ }
4998+ },
4999+ "foreignKeys": {},
5000+ "compositePrimaryKeys": {},
5001+ "uniqueConstraints": {},
5002+ "policies": {},
5003+ "checkConstraints": {},
5004+ "isRLSEnabled": false
5005+ },
5006+ "public.sales": {
5007+ "name": "sales",
5008+ "schema": "",
5009+ "columns": {
5010+ "id": {
5011+ "name": "id",
5012+ "type": "text",
5013+ "primaryKey": true,
5014+ "notNull": true
5015+ },
5016+ "asset_id": {
5017+ "name": "asset_id",
5018+ "type": "text",
5019+ "primaryKey": false,
5020+ "notNull": true
5021+ },
5022+ "variant_id": {
5023+ "name": "variant_id",
5024+ "type": "text",
5025+ "primaryKey": false,
5026+ "notNull": false
5027+ },
5028+ "source_id": {
5029+ "name": "source_id",
5030+ "type": "text",
5031+ "primaryKey": false,
5032+ "notNull": true
5033+ },
5034+ "connector_id": {
5035+ "name": "connector_id",
5036+ "type": "text",
5037+ "primaryKey": false,
5038+ "notNull": true
5039+ },
5040+ "raw_record_id": {
5041+ "name": "raw_record_id",
5042+ "type": "text",
5043+ "primaryKey": false,
5044+ "notNull": false
5045+ },
5046+ "normalized_record_id": {
5047+ "name": "normalized_record_id",
5048+ "type": "text",
5049+ "primaryKey": false,
5050+ "notNull": false
5051+ },
5052+ "source_url": {
5053+ "name": "source_url",
5054+ "type": "text",
5055+ "primaryKey": false,
5056+ "notNull": true
5057+ },
5058+ "external_id": {
5059+ "name": "external_id",
5060+ "type": "text",
5061+ "primaryKey": false,
5062+ "notNull": false
5063+ },
5064+ "sale_type": {
5065+ "name": "sale_type",
5066+ "type": "text",
5067+ "primaryKey": false,
5068+ "notNull": true,
5069+ "default": "'unknown'"
5070+ },
5071+ "sale_date": {
5072+ "name": "sale_date",
5073+ "type": "timestamp with time zone",
5074+ "primaryKey": false,
5075+ "notNull": true
5076+ },
5077+ "price": {
5078+ "name": "price",
5079+ "type": "numeric(18, 4)",
5080+ "primaryKey": false,
5081+ "notNull": true
5082+ },
5083+ "currency": {
5084+ "name": "currency",
5085+ "type": "text",
5086+ "primaryKey": false,
5087+ "notNull": true
5088+ },
5089+ "price_usd": {
5090+ "name": "price_usd",
5091+ "type": "numeric(18, 4)",
5092+ "primaryKey": false,
5093+ "notNull": true
5094+ },
5095+ "fx_rate": {
5096+ "name": "fx_rate",
5097+ "type": "real",
5098+ "primaryKey": false,
5099+ "notNull": false
5100+ },
5101+ "fx_date": {
5102+ "name": "fx_date",
5103+ "type": "date",
5104+ "primaryKey": false,
5105+ "notNull": false
5106+ },
5107+ "buyer_premium_included": {
5108+ "name": "buyer_premium_included",
5109+ "type": "boolean",
5110+ "primaryKey": false,
5111+ "notNull": false
5112+ },
5113+ "quantity": {
5114+ "name": "quantity",
5115+ "type": "integer",
5116+ "primaryKey": false,
5117+ "notNull": true,
5118+ "default": 1
5119+ },
5120+ "is_bundle": {
5121+ "name": "is_bundle",
5122+ "type": "boolean",
5123+ "primaryKey": false,
5124+ "notNull": true,
5125+ "default": false
5126+ },
5127+ "condition": {
5128+ "name": "condition",
5129+ "type": "text",
5130+ "primaryKey": false,
5131+ "notNull": false
5132+ },
5133+ "grader": {
5134+ "name": "grader",
5135+ "type": "text",
5136+ "primaryKey": false,
5137+ "notNull": false
5138+ },
5139+ "grade": {
5140+ "name": "grade",
5141+ "type": "text",
5142+ "primaryKey": false,
5143+ "notNull": false
5144+ },
5145+ "certification_number": {
5146+ "name": "certification_number",
5147+ "type": "text",
5148+ "primaryKey": false,
5149+ "notNull": false
5150+ },
5151+ "location": {
5152+ "name": "location",
5153+ "type": "text",
5154+ "primaryKey": false,
5155+ "notNull": false
5156+ },
5157+ "auction_house": {
5158+ "name": "auction_house",
5159+ "type": "text",
5160+ "primaryKey": false,
5161+ "notNull": false
5162+ },
5163+ "lot_number": {
5164+ "name": "lot_number",
5165+ "type": "text",
5166+ "primaryKey": false,
5167+ "notNull": false
5168+ },
5169+ "image_urls": {
5170+ "name": "image_urls",
5171+ "type": "jsonb",
5172+ "primaryKey": false,
5173+ "notNull": true,
5174+ "default": "'[]'::jsonb"
5175+ },
5176+ "raw_title": {
5177+ "name": "raw_title",
5178+ "type": "text",
5179+ "primaryKey": false,
5180+ "notNull": true
5181+ },
5182+ "confidence": {
5183+ "name": "confidence",
5184+ "type": "numeric(8, 6)",
5185+ "primaryKey": false,
5186+ "notNull": true,
5187+ "default": 0.8
5188+ },
5189+ "data_quality": {
5190+ "name": "data_quality",
5191+ "type": "real",
5192+ "primaryKey": false,
5193+ "notNull": true,
5194+ "default": 0
5195+ },
5196+ "status": {
5197+ "name": "status",
5198+ "type": "text",
5199+ "primaryKey": false,
5200+ "notNull": true,
5201+ "default": "'valid'"
5202+ },
5203+ "flags": {
5204+ "name": "flags",
5205+ "type": "text[]",
5206+ "primaryKey": false,
5207+ "notNull": true,
5208+ "default": "'{}'::text[]"
5209+ },
5210+ "dedupe_key": {
5211+ "name": "dedupe_key",
5212+ "type": "text",
5213+ "primaryKey": false,
5214+ "notNull": true
5215+ },
5216+ "created_at": {
5217+ "name": "created_at",
5218+ "type": "timestamp with time zone",
5219+ "primaryKey": false,
5220+ "notNull": true,
5221+ "default": "now()"
5222+ }
5223+ },
5224+ "indexes": {
5225+ "sales_dedupe_uq": {
5226+ "name": "sales_dedupe_uq",
5227+ "columns": [
5228+ {
5229+ "expression": "dedupe_key",
5230+ "isExpression": false,
5231+ "asc": true,
5232+ "nulls": "last"
5233+ }
5234+ ],
5235+ "isUnique": true,
5236+ "concurrently": false,
5237+ "method": "btree",
5238+ "with": {}
5239+ },
5240+ "sales_asset_date_idx": {
5241+ "name": "sales_asset_date_idx",
5242+ "columns": [
5243+ {
5244+ "expression": "asset_id",
5245+ "isExpression": false,
5246+ "asc": true,
5247+ "nulls": "last"
5248+ },
5249+ {
5250+ "expression": "sale_date",
5251+ "isExpression": false,
5252+ "asc": true,
5253+ "nulls": "last"
5254+ }
5255+ ],
5256+ "isUnique": false,
5257+ "concurrently": false,
5258+ "method": "btree",
5259+ "with": {}
5260+ },
5261+ "sales_variant_date_idx": {
5262+ "name": "sales_variant_date_idx",
5263+ "columns": [
5264+ {
5265+ "expression": "variant_id",
5266+ "isExpression": false,
5267+ "asc": true,
5268+ "nulls": "last"
5269+ },
5270+ {
5271+ "expression": "sale_date",
5272+ "isExpression": false,
5273+ "asc": true,
5274+ "nulls": "last"
5275+ }
5276+ ],
5277+ "isUnique": false,
5278+ "concurrently": false,
5279+ "method": "btree",
5280+ "with": {}
5281+ },
5282+ "sales_source_idx": {
5283+ "name": "sales_source_idx",
5284+ "columns": [
5285+ {
5286+ "expression": "source_id",
5287+ "isExpression": false,
5288+ "asc": true,
5289+ "nulls": "last"
5290+ },
5291+ {
5292+ "expression": "sale_date",
5293+ "isExpression": false,
5294+ "asc": true,
5295+ "nulls": "last"
5296+ }
5297+ ],
5298+ "isUnique": false,
5299+ "concurrently": false,
5300+ "method": "btree",
5301+ "with": {}
5302+ },
5303+ "sales_date_idx": {
5304+ "name": "sales_date_idx",
5305+ "columns": [
5306+ {
5307+ "expression": "sale_date",
5308+ "isExpression": false,
5309+ "asc": true,
5310+ "nulls": "last"
5311+ }
5312+ ],
5313+ "isUnique": false,
5314+ "concurrently": false,
5315+ "method": "btree",
5316+ "with": {}
5317+ },
5318+ "sales_price_idx": {
5319+ "name": "sales_price_idx",
5320+ "columns": [
5321+ {
5322+ "expression": "price_usd",
5323+ "isExpression": false,
5324+ "asc": true,
5325+ "nulls": "last"
5326+ }
5327+ ],
5328+ "isUnique": false,
5329+ "concurrently": false,
5330+ "method": "btree",
5331+ "with": {}
5332+ },
5333+ "sales_valid_date_idx": {
5334+ "name": "sales_valid_date_idx",
5335+ "columns": [
5336+ {
5337+ "expression": "sale_date",
5338+ "isExpression": false,
5339+ "asc": true,
5340+ "nulls": "last"
5341+ }
5342+ ],
5343+ "isUnique": false,
5344+ "where": "status = 'valid'",
5345+ "concurrently": false,
5346+ "method": "btree",
5347+ "with": {}
5348+ }
5349+ },
5350+ "foreignKeys": {},
5351+ "compositePrimaryKeys": {},
5352+ "uniqueConstraints": {},
5353+ "policies": {},
5354+ "checkConstraints": {},
5355+ "isRLSEnabled": false
5356+ },
5357+ "public.benchmarks": {
5358+ "name": "benchmarks",
5359+ "schema": "",
5360+ "columns": {
5361+ "ticker": {
5362+ "name": "ticker",
5363+ "type": "text",
5364+ "primaryKey": false,
5365+ "notNull": true
5366+ },
5367+ "date": {
5368+ "name": "date",
5369+ "type": "date",
5370+ "primaryKey": false,
5371+ "notNull": true
5372+ },
5373+ "value": {
5374+ "name": "value",
5375+ "type": "real",
5376+ "primaryKey": false,
5377+ "notNull": true
5378+ },
5379+ "source": {
5380+ "name": "source",
5381+ "type": "text",
5382+ "primaryKey": false,
5383+ "notNull": true
5384+ }
5385+ },
5386+ "indexes": {},
5387+ "foreignKeys": {},
5388+ "compositePrimaryKeys": {
5389+ "benchmarks_ticker_date_pk": {
5390+ "name": "benchmarks_ticker_date_pk",
5391+ "columns": [
5392+ "ticker",
5393+ "date"
5394+ ]
5395+ }
5396+ },
5397+ "uniqueConstraints": {},
5398+ "policies": {},
5399+ "checkConstraints": {},
5400+ "isRLSEnabled": false
5401+ },
5402+ "public.category_snapshots": {
5403+ "name": "category_snapshots",
5404+ "schema": "",
5405+ "columns": {
5406+ "category_slug": {
5407+ "name": "category_slug",
5408+ "type": "text",
5409+ "primaryKey": false,
5410+ "notNull": true
5411+ },
5412+ "date": {
5413+ "name": "date",
5414+ "type": "date",
5415+ "primaryKey": false,
5416+ "notNull": true
5417+ },
5418+ "index_value": {
5419+ "name": "index_value",
5420+ "type": "real",
5421+ "primaryKey": false,
5422+ "notNull": false
5423+ },
5424+ "tracked_assets": {
5425+ "name": "tracked_assets",
5426+ "type": "integer",
5427+ "primaryKey": false,
5428+ "notNull": true,
5429+ "default": 0
5430+ },
5431+ "assets_with_valuation": {
5432+ "name": "assets_with_valuation",
5433+ "type": "integer",
5434+ "primaryKey": false,
5435+ "notNull": true,
5436+ "default": 0
5437+ },
5438+ "sales": {
5439+ "name": "sales",
5440+ "type": "integer",
5441+ "primaryKey": false,
5442+ "notNull": true,
5443+ "default": 0
5444+ },
5445+ "volume_usd": {
5446+ "name": "volume_usd",
5447+ "type": "numeric(18, 4)",
5448+ "primaryKey": false,
5449+ "notNull": false
5450+ },
5451+ "median_sale_usd": {
5452+ "name": "median_sale_usd",
5453+ "type": "numeric(18, 4)",
5454+ "primaryKey": false,
5455+ "notNull": false
5456+ },
5457+ "active_listings": {
5458+ "name": "active_listings",
5459+ "type": "integer",
5460+ "primaryKey": false,
5461+ "notNull": true,
5462+ "default": 0
5463+ },
5464+ "market_cap_est_usd": {
5465+ "name": "market_cap_est_usd",
5466+ "type": "numeric(18, 4)",
5467+ "primaryKey": false,
5468+ "notNull": false
5469+ },
5470+ "liquidity_score": {
5471+ "name": "liquidity_score",
5472+ "type": "real",
5473+ "primaryKey": false,
5474+ "notNull": false
5475+ },
5476+ "change_1d": {
5477+ "name": "change_1d",
5478+ "type": "numeric(8, 6)",
5479+ "primaryKey": false,
5480+ "notNull": false
5481+ },
5482+ "change_7d": {
5483+ "name": "change_7d",
5484+ "type": "numeric(8, 6)",
5485+ "primaryKey": false,
5486+ "notNull": false
5487+ },
5488+ "change_30d": {
5489+ "name": "change_30d",
5490+ "type": "numeric(8, 6)",
5491+ "primaryKey": false,
5492+ "notNull": false
5493+ },
5494+ "change_1y": {
5495+ "name": "change_1y",
5496+ "type": "numeric(8, 6)",
5497+ "primaryKey": false,
5498+ "notNull": false
5499+ }
5500+ },
5501+ "indexes": {},
5502+ "foreignKeys": {},
5503+ "compositePrimaryKeys": {
5504+ "category_snapshots_category_slug_date_pk": {
5505+ "name": "category_snapshots_category_slug_date_pk",
5506+ "columns": [
5507+ "category_slug",
5508+ "date"
5509+ ]
5510+ }
5511+ },
5512+ "uniqueConstraints": {},
5513+ "policies": {},
5514+ "checkConstraints": {},
5515+ "isRLSEnabled": false
5516+ },
5517+ "public.correlations": {
5518+ "name": "correlations",
5519+ "schema": "",
5520+ "columns": {
5521+ "a": {
5522+ "name": "a",
5523+ "type": "text",
5524+ "primaryKey": false,
5525+ "notNull": true
5526+ },
5527+ "b": {
5528+ "name": "b",
5529+ "type": "text",
5530+ "primaryKey": false,
5531+ "notNull": true
5532+ },
5533+ "window_days": {
5534+ "name": "window_days",
5535+ "type": "integer",
5536+ "primaryKey": false,
5537+ "notNull": true
5538+ },
5539+ "coefficient": {
5540+ "name": "coefficient",
5541+ "type": "real",
5542+ "primaryKey": false,
5543+ "notNull": true
5544+ },
5545+ "observations": {
5546+ "name": "observations",
5547+ "type": "integer",
5548+ "primaryKey": false,
5549+ "notNull": true
5550+ },
5551+ "computed_at": {
5552+ "name": "computed_at",
5553+ "type": "timestamp with time zone",
5554+ "primaryKey": false,
5555+ "notNull": true
5556+ }
5557+ },
5558+ "indexes": {},
5559+ "foreignKeys": {},
5560+ "compositePrimaryKeys": {
5561+ "correlations_a_b_window_days_pk": {
5562+ "name": "correlations_a_b_window_days_pk",
5563+ "columns": [
5564+ "a",
5565+ "b",
5566+ "window_days"
5567+ ]
5568+ }
5569+ },
5570+ "uniqueConstraints": {},
5571+ "policies": {},
5572+ "checkConstraints": {},
5573+ "isRLSEnabled": false
5574+ },
5575+ "public.index_constituents": {
5576+ "name": "index_constituents",
5577+ "schema": "",
5578+ "columns": {
5579+ "index_id": {
5580+ "name": "index_id",
5581+ "type": "text",
5582+ "primaryKey": false,
5583+ "notNull": true
5584+ },
5585+ "asset_id": {
5586+ "name": "asset_id",
5587+ "type": "text",
5588+ "primaryKey": false,
5589+ "notNull": true
5590+ },
5591+ "variant_id": {
5592+ "name": "variant_id",
5593+ "type": "text",
5594+ "primaryKey": false,
5595+ "notNull": true,
5596+ "default": "''"
5597+ },
5598+ "weight": {
5599+ "name": "weight",
5600+ "type": "real",
5601+ "primaryKey": false,
5602+ "notNull": true,
5603+ "default": 1
5604+ },
5605+ "added_at": {
5606+ "name": "added_at",
5607+ "type": "date",
5608+ "primaryKey": false,
5609+ "notNull": true
5610+ },
5611+ "removed_at": {
5612+ "name": "removed_at",
5613+ "type": "date",
5614+ "primaryKey": false,
5615+ "notNull": false
5616+ },
5617+ "reason": {
5618+ "name": "reason",
5619+ "type": "text",
5620+ "primaryKey": false,
5621+ "notNull": false
5622+ }
5623+ },
5624+ "indexes": {
5625+ "index_constituents_asset_idx": {
5626+ "name": "index_constituents_asset_idx",
5627+ "columns": [
5628+ {
5629+ "expression": "asset_id",
5630+ "isExpression": false,
5631+ "asc": true,
5632+ "nulls": "last"
5633+ }
5634+ ],
5635+ "isUnique": false,
5636+ "concurrently": false,
5637+ "method": "btree",
5638+ "with": {}
5639+ }
5640+ },
5641+ "foreignKeys": {},
5642+ "compositePrimaryKeys": {
5643+ "index_constituents_index_id_asset_id_variant_id_added_at_pk": {
5644+ "name": "index_constituents_index_id_asset_id_variant_id_added_at_pk",
5645+ "columns": [
5646+ "index_id",
5647+ "asset_id",
5648+ "variant_id",
5649+ "added_at"
5650+ ]
5651+ }
5652+ },
5653+ "uniqueConstraints": {},
5654+ "policies": {},
5655+ "checkConstraints": {},
5656+ "isRLSEnabled": false
5657+ },
5658+ "public.index_values": {
5659+ "name": "index_values",
5660+ "schema": "",
5661+ "columns": {
5662+ "index_id": {
5663+ "name": "index_id",
5664+ "type": "text",
5665+ "primaryKey": false,
5666+ "notNull": true
5667+ },
5668+ "date": {
5669+ "name": "date",
5670+ "type": "date",
5671+ "primaryKey": false,
5672+ "notNull": true
5673+ },
5674+ "value": {
5675+ "name": "value",
5676+ "type": "real",
5677+ "primaryKey": false,
5678+ "notNull": true
5679+ },
5680+ "constituents_count": {
5681+ "name": "constituents_count",
5682+ "type": "integer",
5683+ "primaryKey": false,
5684+ "notNull": true,
5685+ "default": 0
5686+ },
5687+ "transactions": {
5688+ "name": "transactions",
5689+ "type": "integer",
5690+ "primaryKey": false,
5691+ "notNull": true,
5692+ "default": 0
5693+ },
5694+ "volume_usd": {
5695+ "name": "volume_usd",
5696+ "type": "numeric(18, 4)",
5697+ "primaryKey": false,
5698+ "notNull": false
5699+ },
5700+ "median_sale_usd": {
5701+ "name": "median_sale_usd",
5702+ "type": "numeric(18, 4)",
5703+ "primaryKey": false,
5704+ "notNull": false
5705+ },
5706+ "avg_sale_usd": {
5707+ "name": "avg_sale_usd",
5708+ "type": "numeric(18, 4)",
5709+ "primaryKey": false,
5710+ "notNull": false
5711+ },
5712+ "market_cap_est_usd": {
5713+ "name": "market_cap_est_usd",
5714+ "type": "numeric(18, 4)",
5715+ "primaryKey": false,
5716+ "notNull": false
5717+ },
5718+ "market_cap_confidence": {
5719+ "name": "market_cap_confidence",
5720+ "type": "text",
5721+ "primaryKey": false,
5722+ "notNull": false
5723+ },
5724+ "liquidity_score": {
5725+ "name": "liquidity_score",
5726+ "type": "real",
5727+ "primaryKey": false,
5728+ "notNull": false
5729+ },
5730+ "momentum": {
5731+ "name": "momentum",
5732+ "type": "real",
5733+ "primaryKey": false,
5734+ "notNull": false
5735+ },
5736+ "breadth": {
5737+ "name": "breadth",
5738+ "type": "integer",
5739+ "primaryKey": false,
5740+ "notNull": false
5741+ },
5742+ "tracked_assets": {
5743+ "name": "tracked_assets",
5744+ "type": "integer",
5745+ "primaryKey": false,
5746+ "notNull": false
5747+ },
5748+ "coverage": {
5749+ "name": "coverage",
5750+ "type": "real",
5751+ "primaryKey": false,
5752+ "notNull": false
5753+ }
5754+ },
5755+ "indexes": {},
5756+ "foreignKeys": {},
5757+ "compositePrimaryKeys": {
5758+ "index_values_index_id_date_pk": {
5759+ "name": "index_values_index_id_date_pk",
5760+ "columns": [
5761+ "index_id",
5762+ "date"
5763+ ]
5764+ }
5765+ },
5766+ "uniqueConstraints": {},
5767+ "policies": {},
5768+ "checkConstraints": {},
5769+ "isRLSEnabled": false
5770+ },
5771+ "public.indices": {
5772+ "name": "indices",
5773+ "schema": "",
5774+ "columns": {
5775+ "id": {
5776+ "name": "id",
5777+ "type": "text",
5778+ "primaryKey": true,
5779+ "notNull": true
5780+ },
5781+ "ticker": {
5782+ "name": "ticker",
5783+ "type": "text",
5784+ "primaryKey": false,
5785+ "notNull": true
5786+ },
5787+ "name": {
5788+ "name": "name",
5789+ "type": "text",
5790+ "primaryKey": false,
5791+ "notNull": true
5792+ },
5793+ "description": {
5794+ "name": "description",
5795+ "type": "text",
5796+ "primaryKey": false,
5797+ "notNull": false
5798+ },
5799+ "category_slugs": {
5800+ "name": "category_slugs",
5801+ "type": "text[]",
5802+ "primaryKey": false,
5803+ "notNull": true,
5804+ "default": "'{}'::text[]"
5805+ },
5806+ "family_slugs": {
5807+ "name": "family_slugs",
5808+ "type": "text[]",
5809+ "primaryKey": false,
5810+ "notNull": true,
5811+ "default": "'{}'::text[]"
5812+ },
5813+ "parent_ticker": {
5814+ "name": "parent_ticker",
5815+ "type": "text",
5816+ "primaryKey": false,
5817+ "notNull": false
5818+ },
5819+ "methodology": {
5820+ "name": "methodology",
5821+ "type": "text",
5822+ "primaryKey": false,
5823+ "notNull": true,
5824+ "default": "'chain_linked_equal_weight_v1'"
5825+ },
5826+ "weighting": {
5827+ "name": "weighting",
5828+ "type": "text",
5829+ "primaryKey": false,
5830+ "notNull": true,
5831+ "default": "'equal'"
5832+ },
5833+ "base_date": {
5834+ "name": "base_date",
5835+ "type": "date",
5836+ "primaryKey": false,
5837+ "notNull": true
5838+ },
5839+ "base_value": {
5840+ "name": "base_value",
5841+ "type": "real",
5842+ "primaryKey": false,
5843+ "notNull": true,
5844+ "default": 1000
5845+ },
5846+ "min_constituents": {
5847+ "name": "min_constituents",
5848+ "type": "integer",
5849+ "primaryKey": false,
5850+ "notNull": true,
5851+ "default": 10
5852+ },
5853+ "active": {
5854+ "name": "active",
5855+ "type": "boolean",
5856+ "primaryKey": false,
5857+ "notNull": true,
5858+ "default": true
5859+ },
5860+ "is_flagship": {
5861+ "name": "is_flagship",
5862+ "type": "boolean",
5863+ "primaryKey": false,
5864+ "notNull": true,
5865+ "default": false
5866+ },
5867+ "color": {
5868+ "name": "color",
5869+ "type": "text",
5870+ "primaryKey": false,
5871+ "notNull": false
5872+ },
5873+ "created_at": {
5874+ "name": "created_at",
5875+ "type": "timestamp with time zone",
5876+ "primaryKey": false,
5877+ "notNull": true,
5878+ "default": "now()"
5879+ }
5880+ },
5881+ "indexes": {},
5882+ "foreignKeys": {},
5883+ "compositePrimaryKeys": {},
5884+ "uniqueConstraints": {
5885+ "indices_ticker_unique": {
5886+ "name": "indices_ticker_unique",
5887+ "nullsNotDistinct": false,
5888+ "columns": [
5889+ "ticker"
5890+ ]
5891+ }
5892+ },
5893+ "policies": {},
5894+ "checkConstraints": {},
5895+ "isRLSEnabled": false
5896+ },
5897+ "public.price_snapshots": {
5898+ "name": "price_snapshots",
5899+ "schema": "",
5900+ "columns": {
5901+ "asset_id": {
5902+ "name": "asset_id",
5903+ "type": "text",
5904+ "primaryKey": false,
5905+ "notNull": true
5906+ },
5907+ "variant_id": {
5908+ "name": "variant_id",
5909+ "type": "text",
5910+ "primaryKey": false,
5911+ "notNull": true,
5912+ "default": "''"
5913+ },
5914+ "date": {
5915+ "name": "date",
5916+ "type": "date",
5917+ "primaryKey": false,
5918+ "notNull": true
5919+ },
5920+ "riv_usd": {
5921+ "name": "riv_usd",
5922+ "type": "numeric(18, 4)",
5923+ "primaryKey": false,
5924+ "notNull": false
5925+ },
5926+ "latest_sale_usd": {
5927+ "name": "latest_sale_usd",
5928+ "type": "numeric(18, 4)",
5929+ "primaryKey": false,
5930+ "notNull": false
5931+ },
5932+ "median_usd": {
5933+ "name": "median_usd",
5934+ "type": "numeric(18, 4)",
5935+ "primaryKey": false,
5936+ "notNull": false
5937+ },
5938+ "sales_count": {
5939+ "name": "sales_count",
5940+ "type": "integer",
5941+ "primaryKey": false,
5942+ "notNull": true,
5943+ "default": 0
5944+ },
5945+ "volume_usd": {
5946+ "name": "volume_usd",
5947+ "type": "numeric(18, 4)",
5948+ "primaryKey": false,
5949+ "notNull": false
5950+ },
5951+ "listings_count": {
5952+ "name": "listings_count",
5953+ "type": "integer",
5954+ "primaryKey": false,
5955+ "notNull": true,
5956+ "default": 0
5957+ },
5958+ "min_ask_usd": {
5959+ "name": "min_ask_usd",
5960+ "type": "numeric(18, 4)",
5961+ "primaryKey": false,
5962+ "notNull": false
5963+ },
5964+ "observation_usd": {
5965+ "name": "observation_usd",
5966+ "type": "numeric(18, 4)",
5967+ "primaryKey": false,
5968+ "notNull": false
5969+ }
5970+ },
5971+ "indexes": {
5972+ "price_snapshots_date_idx": {
5973+ "name": "price_snapshots_date_idx",
5974+ "columns": [
5975+ {
5976+ "expression": "date",
5977+ "isExpression": false,
5978+ "asc": true,
5979+ "nulls": "last"
5980+ }
5981+ ],
5982+ "isUnique": false,
5983+ "concurrently": false,
5984+ "method": "btree",
5985+ "with": {}
5986+ }
5987+ },
5988+ "foreignKeys": {},
5989+ "compositePrimaryKeys": {
5990+ "price_snapshots_asset_id_variant_id_date_pk": {
5991+ "name": "price_snapshots_asset_id_variant_id_date_pk",
5992+ "columns": [
5993+ "asset_id",
5994+ "variant_id",
5995+ "date"
5996+ ]
5997+ }
5998+ },
5999+ "uniqueConstraints": {},
6000+ "policies": {},
6001+ "checkConstraints": {},
6002+ "isRLSEnabled": false
6003+ },
6004+ "public.radar_findings": {
6005+ "name": "radar_findings",
6006+ "schema": "",
6007+ "columns": {
6008+ "id": {
6009+ "name": "id",
6010+ "type": "text",
6011+ "primaryKey": true,
6012+ "notNull": true
6013+ },
6014+ "asset_id": {
6015+ "name": "asset_id",
6016+ "type": "text",
6017+ "primaryKey": false,
6018+ "notNull": true
6019+ },
6020+ "kind": {
6021+ "name": "kind",
6022+ "type": "text",
6023+ "primaryKey": false,
6024+ "notNull": true
6025+ },
6026+ "score": {
6027+ "name": "score",
6028+ "type": "real",
6029+ "primaryKey": false,
6030+ "notNull": true
6031+ },
6032+ "evidence": {
6033+ "name": "evidence",
6034+ "type": "jsonb",
6035+ "primaryKey": false,
6036+ "notNull": true,
6037+ "default": "'{}'::jsonb"
6038+ },
6039+ "entity_type": {
6040+ "name": "entity_type",
6041+ "type": "text",
6042+ "primaryKey": false,
6043+ "notNull": false
6044+ },
6045+ "entity_id": {
6046+ "name": "entity_id",
6047+ "type": "text",
6048+ "primaryKey": false,
6049+ "notNull": false
6050+ },
6051+ "detected_at": {
6052+ "name": "detected_at",
6053+ "type": "timestamp with time zone",
6054+ "primaryKey": false,
6055+ "notNull": true
6056+ },
6057+ "expires_at": {
6058+ "name": "expires_at",
6059+ "type": "timestamp with time zone",
6060+ "primaryKey": false,
6061+ "notNull": false
6062+ }
6063+ },
6064+ "indexes": {
6065+ "radar_kind_idx": {
6066+ "name": "radar_kind_idx",
6067+ "columns": [
6068+ {
6069+ "expression": "kind",
6070+ "isExpression": false,
6071+ "asc": true,
6072+ "nulls": "last"
6073+ },
6074+ {
6075+ "expression": "detected_at",
6076+ "isExpression": false,
6077+ "asc": true,
6078+ "nulls": "last"
6079+ }
6080+ ],
6081+ "isUnique": false,
6082+ "concurrently": false,
6083+ "method": "btree",
6084+ "with": {}
6085+ },
6086+ "radar_entity_uq": {
6087+ "name": "radar_entity_uq",
6088+ "columns": [
6089+ {
6090+ "expression": "kind",
6091+ "isExpression": false,
6092+ "asc": true,
6093+ "nulls": "last"
6094+ },
6095+ {
6096+ "expression": "entity_type",
6097+ "isExpression": false,
6098+ "asc": true,
6099+ "nulls": "last"
6100+ },
6101+ {
6102+ "expression": "entity_id",
6103+ "isExpression": false,
6104+ "asc": true,
6105+ "nulls": "last"
6106+ }
6107+ ],
6108+ "isUnique": true,
6109+ "concurrently": false,
6110+ "method": "btree",
6111+ "with": {}
6112+ }
6113+ },
6114+ "foreignKeys": {},
6115+ "compositePrimaryKeys": {},
6116+ "uniqueConstraints": {},
6117+ "policies": {},
6118+ "checkConstraints": {},
6119+ "isRLSEnabled": false
6120+ },
6121+ "public.valuations": {
6122+ "name": "valuations",
6123+ "schema": "",
6124+ "columns": {
6125+ "id": {
6126+ "name": "id",
6127+ "type": "text",
6128+ "primaryKey": true,
6129+ "notNull": true
6130+ },
6131+ "asset_id": {
6132+ "name": "asset_id",
6133+ "type": "text",
6134+ "primaryKey": false,
6135+ "notNull": true
6136+ },
6137+ "variant_id": {
6138+ "name": "variant_id",
6139+ "type": "text",
6140+ "primaryKey": false,
6141+ "notNull": false
6142+ },
6143+ "computed_at": {
6144+ "name": "computed_at",
6145+ "type": "timestamp with time zone",
6146+ "primaryKey": false,
6147+ "notNull": true
6148+ },
6149+ "riv_usd": {
6150+ "name": "riv_usd",
6151+ "type": "numeric(18, 4)",
6152+ "primaryKey": false,
6153+ "notNull": false
6154+ },
6155+ "low_usd": {
6156+ "name": "low_usd",
6157+ "type": "numeric(18, 4)",
6158+ "primaryKey": false,
6159+ "notNull": false
6160+ },
6161+ "high_usd": {
6162+ "name": "high_usd",
6163+ "type": "numeric(18, 4)",
6164+ "primaryKey": false,
6165+ "notNull": false
6166+ },
6167+ "confidence": {
6168+ "name": "confidence",
6169+ "type": "numeric(8, 6)",
6170+ "primaryKey": false,
6171+ "notNull": true,
6172+ "default": 0
6173+ },
6174+ "confidence_label": {
6175+ "name": "confidence_label",
6176+ "type": "text",
6177+ "primaryKey": false,
6178+ "notNull": true,
6179+ "default": "'insufficient'"
6180+ },
6181+ "sample_size": {
6182+ "name": "sample_size",
6183+ "type": "integer",
6184+ "primaryKey": false,
6185+ "notNull": true,
6186+ "default": 0
6187+ },
6188+ "window_days": {
6189+ "name": "window_days",
6190+ "type": "integer",
6191+ "primaryKey": false,
6192+ "notNull": true,
6193+ "default": 365
6194+ },
6195+ "methods": {
6196+ "name": "methods",
6197+ "type": "jsonb",
6198+ "primaryKey": false,
6199+ "notNull": true,
6200+ "default": "'{}'::jsonb"
6201+ },
6202+ "sales_used": {
6203+ "name": "sales_used",
6204+ "type": "text[]",
6205+ "primaryKey": false,
6206+ "notNull": true,
6207+ "default": "'{}'::text[]"
6208+ },
6209+ "observations_used": {
6210+ "name": "observations_used",
6211+ "type": "integer",
6212+ "primaryKey": false,
6213+ "notNull": true,
6214+ "default": 0
6215+ },
6216+ "method": {
6217+ "name": "method",
6218+ "type": "text",
6219+ "primaryKey": false,
6220+ "notNull": true,
6221+ "default": "'ensemble_v1'"
6222+ },
6223+ "notes": {
6224+ "name": "notes",
6225+ "type": "text[]",
6226+ "primaryKey": false,
6227+ "notNull": true,
6228+ "default": "'{}'::text[]"
6229+ }
6230+ },
6231+ "indexes": {
6232+ "valuations_asset_idx": {
6233+ "name": "valuations_asset_idx",
6234+ "columns": [
6235+ {
6236+ "expression": "asset_id",
6237+ "isExpression": false,
6238+ "asc": true,
6239+ "nulls": "last"
6240+ },
6241+ {
6242+ "expression": "computed_at",
6243+ "isExpression": false,
6244+ "asc": true,
6245+ "nulls": "last"
6246+ }
6247+ ],
6248+ "isUnique": false,
6249+ "concurrently": false,
6250+ "method": "btree",
6251+ "with": {}
6252+ },
6253+ "valuations_variant_idx": {
6254+ "name": "valuations_variant_idx",
6255+ "columns": [
6256+ {
6257+ "expression": "variant_id",
6258+ "isExpression": false,
6259+ "asc": true,
6260+ "nulls": "last"
6261+ },
6262+ {
6263+ "expression": "computed_at",
6264+ "isExpression": false,
6265+ "asc": true,
6266+ "nulls": "last"
6267+ }
6268+ ],
6269+ "isUnique": false,
6270+ "concurrently": false,
6271+ "method": "btree",
6272+ "with": {}
6273+ }
6274+ },
6275+ "foreignKeys": {},
6276+ "compositePrimaryKeys": {},
6277+ "uniqueConstraints": {},
6278+ "policies": {},
6279+ "checkConstraints": {},
6280+ "isRLSEnabled": false
6281+ },
6282+ "public.alert_events": {
6283+ "name": "alert_events",
6284+ "schema": "",
6285+ "columns": {
6286+ "id": {
6287+ "name": "id",
6288+ "type": "text",
6289+ "primaryKey": true,
6290+ "notNull": true
6291+ },
6292+ "alert_id": {
6293+ "name": "alert_id",
6294+ "type": "text",
6295+ "primaryKey": false,
6296+ "notNull": true
6297+ },
6298+ "user_id": {
6299+ "name": "user_id",
6300+ "type": "text",
6301+ "primaryKey": false,
6302+ "notNull": true
6303+ },
6304+ "message": {
6305+ "name": "message",
6306+ "type": "text",
6307+ "primaryKey": false,
6308+ "notNull": true
6309+ },
6310+ "payload": {
6311+ "name": "payload",
6312+ "type": "jsonb",
6313+ "primaryKey": false,
6314+ "notNull": true,
6315+ "default": "'{}'::jsonb"
6316+ },
6317+ "read_at": {
6318+ "name": "read_at",
6319+ "type": "timestamp with time zone",
6320+ "primaryKey": false,
6321+ "notNull": false
6322+ },
6323+ "created_at": {
6324+ "name": "created_at",
6325+ "type": "timestamp with time zone",
6326+ "primaryKey": false,
6327+ "notNull": true,
6328+ "default": "now()"
6329+ }
6330+ },
6331+ "indexes": {
6332+ "alert_events_user_idx": {
6333+ "name": "alert_events_user_idx",
6334+ "columns": [
6335+ {
6336+ "expression": "user_id",
6337+ "isExpression": false,
6338+ "asc": true,
6339+ "nulls": "last"
6340+ },
6341+ {
6342+ "expression": "created_at",
6343+ "isExpression": false,
6344+ "asc": true,
6345+ "nulls": "last"
6346+ }
6347+ ],
6348+ "isUnique": false,
6349+ "concurrently": false,
6350+ "method": "btree",
6351+ "with": {}
6352+ }
6353+ },
6354+ "foreignKeys": {},
6355+ "compositePrimaryKeys": {},
6356+ "uniqueConstraints": {},
6357+ "policies": {},
6358+ "checkConstraints": {},
6359+ "isRLSEnabled": false
6360+ },
6361+ "public.alerts": {
6362+ "name": "alerts",
6363+ "schema": "",
6364+ "columns": {
6365+ "id": {
6366+ "name": "id",
6367+ "type": "text",
6368+ "primaryKey": true,
6369+ "notNull": true
6370+ },
6371+ "user_id": {
6372+ "name": "user_id",
6373+ "type": "text",
6374+ "primaryKey": false,
6375+ "notNull": true
6376+ },
6377+ "alert_type": {
6378+ "name": "alert_type",
6379+ "type": "text",
6380+ "primaryKey": false,
6381+ "notNull": true
6382+ },
6383+ "target_type": {
6384+ "name": "target_type",
6385+ "type": "text",
6386+ "primaryKey": false,
6387+ "notNull": true
6388+ },
6389+ "target_id": {
6390+ "name": "target_id",
6391+ "type": "text",
6392+ "primaryKey": false,
6393+ "notNull": true
6394+ },
6395+ "threshold": {
6396+ "name": "threshold",
6397+ "type": "numeric(18, 4)",
6398+ "primaryKey": false,
6399+ "notNull": false
6400+ },
6401+ "currency": {
6402+ "name": "currency",
6403+ "type": "text",
6404+ "primaryKey": false,
6405+ "notNull": false
6406+ },
6407+ "params": {
6408+ "name": "params",
6409+ "type": "jsonb",
6410+ "primaryKey": false,
6411+ "notNull": true,
6412+ "default": "'{}'::jsonb"
6413+ },
6414+ "channel": {
6415+ "name": "channel",
6416+ "type": "text",
6417+ "primaryKey": false,
6418+ "notNull": true,
6419+ "default": "'inapp'"
6420+ },
6421+ "active": {
6422+ "name": "active",
6423+ "type": "boolean",
6424+ "primaryKey": false,
6425+ "notNull": true,
6426+ "default": true
6427+ },
6428+ "name": {
6429+ "name": "name",
6430+ "type": "text",
6431+ "primaryKey": false,
6432+ "notNull": false
6433+ },
6434+ "cooldown_minutes": {
6435+ "name": "cooldown_minutes",
6436+ "type": "integer",
6437+ "primaryKey": false,
6438+ "notNull": true,
6439+ "default": 1440
6440+ },
6441+ "last_triggered_at": {
6442+ "name": "last_triggered_at",
6443+ "type": "timestamp with time zone",
6444+ "primaryKey": false,
6445+ "notNull": false
6446+ },
6447+ "trigger_count": {
6448+ "name": "trigger_count",
6449+ "type": "integer",
6450+ "primaryKey": false,
6451+ "notNull": true,
6452+ "default": 0
6453+ },
6454+ "created_at": {
6455+ "name": "created_at",
6456+ "type": "timestamp with time zone",
6457+ "primaryKey": false,
6458+ "notNull": true,
6459+ "default": "now()"
6460+ }
6461+ },
6462+ "indexes": {
6463+ "alerts_user_idx": {
6464+ "name": "alerts_user_idx",
6465+ "columns": [
6466+ {
6467+ "expression": "user_id",
6468+ "isExpression": false,
6469+ "asc": true,
6470+ "nulls": "last"
6471+ }
6472+ ],
6473+ "isUnique": false,
6474+ "concurrently": false,
6475+ "method": "btree",
6476+ "with": {}
6477+ },
6478+ "alerts_target_idx": {
6479+ "name": "alerts_target_idx",
6480+ "columns": [
6481+ {
6482+ "expression": "target_type",
6483+ "isExpression": false,
6484+ "asc": true,
6485+ "nulls": "last"
6486+ },
6487+ {
6488+ "expression": "target_id",
6489+ "isExpression": false,
6490+ "asc": true,
6491+ "nulls": "last"
6492+ },
6493+ {
6494+ "expression": "active",
6495+ "isExpression": false,
6496+ "asc": true,
6497+ "nulls": "last"
6498+ }
6499+ ],
6500+ "isUnique": false,
6501+ "concurrently": false,
6502+ "method": "btree",
6503+ "with": {}
6504+ }
6505+ },
6506+ "foreignKeys": {},
6507+ "compositePrimaryKeys": {},
6508+ "uniqueConstraints": {},
6509+ "policies": {},
6510+ "checkConstraints": {},
6511+ "isRLSEnabled": false
6512+ },
6513+ "public.api_keys": {
6514+ "name": "api_keys",
6515+ "schema": "",
6516+ "columns": {
6517+ "id": {
6518+ "name": "id",
6519+ "type": "text",
6520+ "primaryKey": true,
6521+ "notNull": true
6522+ },
6523+ "user_id": {
6524+ "name": "user_id",
6525+ "type": "text",
6526+ "primaryKey": false,
6527+ "notNull": false
6528+ },
6529+ "name": {
6530+ "name": "name",
6531+ "type": "text",
6532+ "primaryKey": false,
6533+ "notNull": true
6534+ },
6535+ "prefix": {
6536+ "name": "prefix",
6537+ "type": "text",
6538+ "primaryKey": false,
6539+ "notNull": true
6540+ },
6541+ "key_hash": {
6542+ "name": "key_hash",
6543+ "type": "text",
6544+ "primaryKey": false,
6545+ "notNull": true
6546+ },
6547+ "tier": {
6548+ "name": "tier",
6549+ "type": "text",
6550+ "primaryKey": false,
6551+ "notNull": true,
6552+ "default": "'free'"
6553+ },
6554+ "rate_limit_per_minute": {
6555+ "name": "rate_limit_per_minute",
6556+ "type": "integer",
6557+ "primaryKey": false,
6558+ "notNull": true,
6559+ "default": 60
6560+ },
6561+ "daily_quota": {
6562+ "name": "daily_quota",
6563+ "type": "integer",
6564+ "primaryKey": false,
6565+ "notNull": true,
6566+ "default": 1000
6567+ },
6568+ "created_at": {
6569+ "name": "created_at",
6570+ "type": "timestamp with time zone",
6571+ "primaryKey": false,
6572+ "notNull": true,
6573+ "default": "now()"
6574+ },
6575+ "last_used_at": {
6576+ "name": "last_used_at",
6577+ "type": "timestamp with time zone",
6578+ "primaryKey": false,
6579+ "notNull": false
6580+ },
6581+ "revoked_at": {
6582+ "name": "revoked_at",
6583+ "type": "timestamp with time zone",
6584+ "primaryKey": false,
6585+ "notNull": false
6586+ }
6587+ },
6588+ "indexes": {
6589+ "api_keys_user_idx": {
6590+ "name": "api_keys_user_idx",
6591+ "columns": [
6592+ {
6593+ "expression": "user_id",
6594+ "isExpression": false,
6595+ "asc": true,
6596+ "nulls": "last"
6597+ }
6598+ ],
6599+ "isUnique": false,
6600+ "concurrently": false,
6601+ "method": "btree",
6602+ "with": {}
6603+ }
6604+ },
6605+ "foreignKeys": {},
6606+ "compositePrimaryKeys": {},
6607+ "uniqueConstraints": {
6608+ "api_keys_key_hash_unique": {
6609+ "name": "api_keys_key_hash_unique",
6610+ "nullsNotDistinct": false,
6611+ "columns": [
6612+ "key_hash"
6613+ ]
6614+ }
6615+ },
6616+ "policies": {},
6617+ "checkConstraints": {},
6618+ "isRLSEnabled": false
6619+ },
6620+ "public.api_usage": {
6621+ "name": "api_usage",
6622+ "schema": "",
6623+ "columns": {
6624+ "key_id": {
6625+ "name": "key_id",
6626+ "type": "text",
6627+ "primaryKey": false,
6628+ "notNull": true
6629+ },
6630+ "date": {
6631+ "name": "date",
6632+ "type": "date",
6633+ "primaryKey": false,
6634+ "notNull": true
6635+ },
6636+ "endpoint": {
6637+ "name": "endpoint",
6638+ "type": "text",
6639+ "primaryKey": false,
6640+ "notNull": true
6641+ },
6642+ "count": {
6643+ "name": "count",
6644+ "type": "integer",
6645+ "primaryKey": false,
6646+ "notNull": true,
6647+ "default": 0
6648+ },
6649+ "latency_ms_avg": {
6650+ "name": "latency_ms_avg",
6651+ "type": "real",
6652+ "primaryKey": false,
6653+ "notNull": false
6654+ }
6655+ },
6656+ "indexes": {},
6657+ "foreignKeys": {},
6658+ "compositePrimaryKeys": {
6659+ "api_usage_key_id_date_endpoint_pk": {
6660+ "name": "api_usage_key_id_date_endpoint_pk",
6661+ "columns": [
6662+ "key_id",
6663+ "date",
6664+ "endpoint"
6665+ ]
6666+ }
6667+ },
6668+ "uniqueConstraints": {},
6669+ "policies": {},
6670+ "checkConstraints": {},
6671+ "isRLSEnabled": false
6672+ },
6673+ "public.asset_views": {
6674+ "name": "asset_views",
6675+ "schema": "",
6676+ "columns": {
6677+ "asset_id": {
6678+ "name": "asset_id",
6679+ "type": "text",
6680+ "primaryKey": false,
6681+ "notNull": true
6682+ },
6683+ "date": {
6684+ "name": "date",
6685+ "type": "date",
6686+ "primaryKey": false,
6687+ "notNull": true
6688+ },
6689+ "views": {
6690+ "name": "views",
6691+ "type": "integer",
6692+ "primaryKey": false,
6693+ "notNull": true,
6694+ "default": 0
6695+ }
6696+ },
6697+ "indexes": {},
6698+ "foreignKeys": {},
6699+ "compositePrimaryKeys": {
6700+ "asset_views_asset_id_date_pk": {
6701+ "name": "asset_views_asset_id_date_pk",
6702+ "columns": [
6703+ "asset_id",
6704+ "date"
6705+ ]
6706+ }
6707+ },
6708+ "uniqueConstraints": {},
6709+ "policies": {},
6710+ "checkConstraints": {},
6711+ "isRLSEnabled": false
6712+ },
6713+ "public.collection_items": {
6714+ "name": "collection_items",
6715+ "schema": "",
6716+ "columns": {
6717+ "id": {
6718+ "name": "id",
6719+ "type": "text",
6720+ "primaryKey": true,
6721+ "notNull": true
6722+ },
6723+ "collection_id": {
6724+ "name": "collection_id",
6725+ "type": "text",
6726+ "primaryKey": false,
6727+ "notNull": true
6728+ },
6729+ "asset_id": {
6730+ "name": "asset_id",
6731+ "type": "text",
6732+ "primaryKey": false,
6733+ "notNull": true
6734+ },
6735+ "variant_id": {
6736+ "name": "variant_id",
6737+ "type": "text",
6738+ "primaryKey": false,
6739+ "notNull": false
6740+ },
6741+ "quantity": {
6742+ "name": "quantity",
6743+ "type": "integer",
6744+ "primaryKey": false,
6745+ "notNull": true,
6746+ "default": 1
6747+ },
6748+ "acquired_at": {
6749+ "name": "acquired_at",
6750+ "type": "date",
6751+ "primaryKey": false,
6752+ "notNull": false
6753+ },
6754+ "purchase_price": {
6755+ "name": "purchase_price",
6756+ "type": "numeric(18, 4)",
6757+ "primaryKey": false,
6758+ "notNull": false
6759+ },
6760+ "purchase_currency": {
6761+ "name": "purchase_currency",
6762+ "type": "text",
6763+ "primaryKey": false,
6764+ "notNull": false
6765+ },
6766+ "purchase_price_usd": {
6767+ "name": "purchase_price_usd",
6768+ "type": "numeric(18, 4)",
6769+ "primaryKey": false,
6770+ "notNull": false
6771+ },
6772+ "source": {
6773+ "name": "source",
6774+ "type": "text",
6775+ "primaryKey": false,
6776+ "notNull": false
6777+ },
6778+ "grader": {
6779+ "name": "grader",
6780+ "type": "text",
6781+ "primaryKey": false,
6782+ "notNull": false
6783+ },
6784+ "grade": {
6785+ "name": "grade",
6786+ "type": "text",
6787+ "primaryKey": false,
6788+ "notNull": false
6789+ },
6790+ "certification_number": {
6791+ "name": "certification_number",
6792+ "type": "text",
6793+ "primaryKey": false,
6794+ "notNull": false
6795+ },
6796+ "serial": {
6797+ "name": "serial",
6798+ "type": "text",
6799+ "primaryKey": false,
6800+ "notNull": false
6801+ },
6802+ "photos": {
6803+ "name": "photos",
6804+ "type": "jsonb",
6805+ "primaryKey": false,
6806+ "notNull": true,
6807+ "default": "'[]'::jsonb"
6808+ },
6809+ "notes": {
6810+ "name": "notes",
6811+ "type": "text",
6812+ "primaryKey": false,
6813+ "notNull": false
6814+ },
6815+ "tags": {
6816+ "name": "tags",
6817+ "type": "jsonb",
6818+ "primaryKey": false,
6819+ "notNull": true,
6820+ "default": "'[]'::jsonb"
6821+ },
6822+ "condition": {
6823+ "name": "condition",
6824+ "type": "text",
6825+ "primaryKey": false,
6826+ "notNull": false
6827+ },
6828+ "manual_value_usd": {
6829+ "name": "manual_value_usd",
6830+ "type": "numeric(18, 4)",
6831+ "primaryKey": false,
6832+ "notNull": false
6833+ },
6834+ "sold_at": {
6835+ "name": "sold_at",
6836+ "type": "date",
6837+ "primaryKey": false,
6838+ "notNull": false
6839+ },
6840+ "sold_price_usd": {
6841+ "name": "sold_price_usd",
6842+ "type": "numeric(18, 4)",
6843+ "primaryKey": false,
6844+ "notNull": false
6845+ },
6846+ "created_at": {
6847+ "name": "created_at",
6848+ "type": "timestamp with time zone",
6849+ "primaryKey": false,
6850+ "notNull": true,
6851+ "default": "now()"
6852+ },
6853+ "updated_at": {
6854+ "name": "updated_at",
6855+ "type": "timestamp with time zone",
6856+ "primaryKey": false,
6857+ "notNull": true,
6858+ "default": "now()"
6859+ }
6860+ },
6861+ "indexes": {
6862+ "collection_items_collection_idx": {
6863+ "name": "collection_items_collection_idx",
6864+ "columns": [
6865+ {
6866+ "expression": "collection_id",
6867+ "isExpression": false,
6868+ "asc": true,
6869+ "nulls": "last"
6870+ }
6871+ ],
6872+ "isUnique": false,
6873+ "concurrently": false,
6874+ "method": "btree",
6875+ "with": {}
6876+ },
6877+ "collection_items_asset_idx": {
6878+ "name": "collection_items_asset_idx",
6879+ "columns": [
6880+ {
6881+ "expression": "asset_id",
6882+ "isExpression": false,
6883+ "asc": true,
6884+ "nulls": "last"
6885+ }
6886+ ],
6887+ "isUnique": false,
6888+ "concurrently": false,
6889+ "method": "btree",
6890+ "with": {}
6891+ }
6892+ },
6893+ "foreignKeys": {},
6894+ "compositePrimaryKeys": {},
6895+ "uniqueConstraints": {},
6896+ "policies": {},
6897+ "checkConstraints": {},
6898+ "isRLSEnabled": false
6899+ },
6900+ "public.collection_snapshots": {
6901+ "name": "collection_snapshots",
6902+ "schema": "",
6903+ "columns": {
6904+ "collection_id": {
6905+ "name": "collection_id",
6906+ "type": "text",
6907+ "primaryKey": false,
6908+ "notNull": true
6909+ },
6910+ "date": {
6911+ "name": "date",
6912+ "type": "date",
6913+ "primaryKey": false,
6914+ "notNull": true
6915+ },
6916+ "value_usd": {
6917+ "name": "value_usd",
6918+ "type": "numeric(18, 4)",
6919+ "primaryKey": false,
6920+ "notNull": true
6921+ },
6922+ "cost_basis_usd": {
6923+ "name": "cost_basis_usd",
6924+ "type": "numeric(18, 4)",
6925+ "primaryKey": false,
6926+ "notNull": true
6927+ },
6928+ "items": {
6929+ "name": "items",
6930+ "type": "integer",
6931+ "primaryKey": false,
6932+ "notNull": true
6933+ }
6934+ },
6935+ "indexes": {},
6936+ "foreignKeys": {},
6937+ "compositePrimaryKeys": {
6938+ "collection_snapshots_collection_id_date_pk": {
6939+ "name": "collection_snapshots_collection_id_date_pk",
6940+ "columns": [
6941+ "collection_id",
6942+ "date"
6943+ ]
6944+ }
6945+ },
6946+ "uniqueConstraints": {},
6947+ "policies": {},
6948+ "checkConstraints": {},
6949+ "isRLSEnabled": false
6950+ },
6951+ "public.collections": {
6952+ "name": "collections",
6953+ "schema": "",
6954+ "columns": {
6955+ "id": {
6956+ "name": "id",
6957+ "type": "text",
6958+ "primaryKey": true,
6959+ "notNull": true
6960+ },
6961+ "user_id": {
6962+ "name": "user_id",
6963+ "type": "text",
6964+ "primaryKey": false,
6965+ "notNull": true
6966+ },
6967+ "name": {
6968+ "name": "name",
6969+ "type": "text",
6970+ "primaryKey": false,
6971+ "notNull": true
6972+ },
6973+ "description": {
6974+ "name": "description",
6975+ "type": "text",
6976+ "primaryKey": false,
6977+ "notNull": false
6978+ },
6979+ "is_public": {
6980+ "name": "is_public",
6981+ "type": "boolean",
6982+ "primaryKey": false,
6983+ "notNull": true,
6984+ "default": false
6985+ },
6986+ "public_slug": {
6987+ "name": "public_slug",
6988+ "type": "text",
6989+ "primaryKey": false,
6990+ "notNull": false
6991+ },
6992+ "kind": {
6993+ "name": "kind",
6994+ "type": "text",
6995+ "primaryKey": false,
6996+ "notNull": true,
6997+ "default": "'collection'"
6998+ },
6999+ "budget_usd": {
7000+ "name": "budget_usd",
7001+ "type": "numeric(18, 4)",
7002+ "primaryKey": false,
7003+ "notNull": false
7004+ },
7005+ "color": {
7006+ "name": "color",
7007+ "type": "text",
7008+ "primaryKey": false,
7009+ "notNull": false
7010+ },
7011+ "created_at": {
7012+ "name": "created_at",
7013+ "type": "timestamp with time zone",
7014+ "primaryKey": false,
7015+ "notNull": true,
7016+ "default": "now()"
7017+ },
7018+ "updated_at": {
7019+ "name": "updated_at",
7020+ "type": "timestamp with time zone",
7021+ "primaryKey": false,
7022+ "notNull": true,
7023+ "default": "now()"
7024+ }
7025+ },
7026+ "indexes": {
7027+ "collections_user_idx": {
7028+ "name": "collections_user_idx",
7029+ "columns": [
7030+ {
7031+ "expression": "user_id",
7032+ "isExpression": false,
7033+ "asc": true,
7034+ "nulls": "last"
7035+ }
7036+ ],
7037+ "isUnique": false,
7038+ "concurrently": false,
7039+ "method": "btree",
7040+ "with": {}
7041+ },
7042+ "collections_public_slug_uq": {
7043+ "name": "collections_public_slug_uq",
7044+ "columns": [
7045+ {
7046+ "expression": "public_slug",
7047+ "isExpression": false,
7048+ "asc": true,
7049+ "nulls": "last"
7050+ }
7051+ ],
7052+ "isUnique": true,
7053+ "concurrently": false,
7054+ "method": "btree",
7055+ "with": {}
7056+ }
7057+ },
7058+ "foreignKeys": {},
7059+ "compositePrimaryKeys": {},
7060+ "uniqueConstraints": {},
7061+ "policies": {},
7062+ "checkConstraints": {},
7063+ "isRLSEnabled": false
7064+ },
7065+ "public.search_log": {
7066+ "name": "search_log",
7067+ "schema": "",
7068+ "columns": {
7069+ "id": {
7070+ "name": "id",
7071+ "type": "text",
7072+ "primaryKey": true,
7073+ "notNull": true
7074+ },
7075+ "query": {
7076+ "name": "query",
7077+ "type": "text",
7078+ "primaryKey": false,
7079+ "notNull": true
7080+ },
7081+ "normalized": {
7082+ "name": "normalized",
7083+ "type": "text",
7084+ "primaryKey": false,
7085+ "notNull": true
7086+ },
7087+ "results": {
7088+ "name": "results",
7089+ "type": "integer",
7090+ "primaryKey": false,
7091+ "notNull": true
7092+ },
7093+ "user_id": {
7094+ "name": "user_id",
7095+ "type": "text",
7096+ "primaryKey": false,
7097+ "notNull": false
7098+ },
7099+ "created_at": {
7100+ "name": "created_at",
7101+ "type": "timestamp with time zone",
7102+ "primaryKey": false,
7103+ "notNull": true,
7104+ "default": "now()"
7105+ }
7106+ },
7107+ "indexes": {
7108+ "search_log_created_idx": {
7109+ "name": "search_log_created_idx",
7110+ "columns": [
7111+ {
7112+ "expression": "created_at",
7113+ "isExpression": false,
7114+ "asc": true,
7115+ "nulls": "last"
7116+ }
7117+ ],
7118+ "isUnique": false,
7119+ "concurrently": false,
7120+ "method": "btree",
7121+ "with": {}
7122+ },
7123+ "search_log_normalized_idx": {
7124+ "name": "search_log_normalized_idx",
7125+ "columns": [
7126+ {
7127+ "expression": "normalized",
7128+ "isExpression": false,
7129+ "asc": true,
7130+ "nulls": "last"
7131+ }
7132+ ],
7133+ "isUnique": false,
7134+ "concurrently": false,
7135+ "method": "btree",
7136+ "with": {}
7137+ }
7138+ },
7139+ "foreignKeys": {},
7140+ "compositePrimaryKeys": {},
7141+ "uniqueConstraints": {},
7142+ "policies": {},
7143+ "checkConstraints": {},
7144+ "isRLSEnabled": false
7145+ },
7146+ "public.sessions": {
7147+ "name": "sessions",
7148+ "schema": "",
7149+ "columns": {
7150+ "id": {
7151+ "name": "id",
7152+ "type": "text",
7153+ "primaryKey": true,
7154+ "notNull": true
7155+ },
7156+ "user_id": {
7157+ "name": "user_id",
7158+ "type": "text",
7159+ "primaryKey": false,
7160+ "notNull": true
7161+ },
7162+ "expires_at": {
7163+ "name": "expires_at",
7164+ "type": "timestamp with time zone",
7165+ "primaryKey": false,
7166+ "notNull": true
7167+ },
7168+ "user_agent": {
7169+ "name": "user_agent",
7170+ "type": "text",
7171+ "primaryKey": false,
7172+ "notNull": false
7173+ },
7174+ "ip": {
7175+ "name": "ip",
7176+ "type": "text",
7177+ "primaryKey": false,
7178+ "notNull": false
7179+ },
7180+ "last_seen_at": {
7181+ "name": "last_seen_at",
7182+ "type": "timestamp with time zone",
7183+ "primaryKey": false,
7184+ "notNull": false
7185+ },
7186+ "revoked_at": {
7187+ "name": "revoked_at",
7188+ "type": "timestamp with time zone",
7189+ "primaryKey": false,
7190+ "notNull": false
7191+ },
7192+ "created_at": {
7193+ "name": "created_at",
7194+ "type": "timestamp with time zone",
7195+ "primaryKey": false,
7196+ "notNull": true,
7197+ "default": "now()"
7198+ }
7199+ },
7200+ "indexes": {
7201+ "sessions_user_idx": {
7202+ "name": "sessions_user_idx",
7203+ "columns": [
7204+ {
7205+ "expression": "user_id",
7206+ "isExpression": false,
7207+ "asc": true,
7208+ "nulls": "last"
7209+ }
7210+ ],
7211+ "isUnique": false,
7212+ "concurrently": false,
7213+ "method": "btree",
7214+ "with": {}
7215+ }
7216+ },
7217+ "foreignKeys": {},
7218+ "compositePrimaryKeys": {},
7219+ "uniqueConstraints": {},
7220+ "policies": {},
7221+ "checkConstraints": {},
7222+ "isRLSEnabled": false
7223+ },
7224+ "public.users": {
7225+ "name": "users",
7226+ "schema": "",
7227+ "columns": {
7228+ "id": {
7229+ "name": "id",
7230+ "type": "text",
7231+ "primaryKey": true,
7232+ "notNull": true
7233+ },
7234+ "email": {
7235+ "name": "email",
7236+ "type": "text",
7237+ "primaryKey": false,
7238+ "notNull": true
7239+ },
7240+ "email_verified_at": {
7241+ "name": "email_verified_at",
7242+ "type": "timestamp with time zone",
7243+ "primaryKey": false,
7244+ "notNull": false
7245+ },
7246+ "password_hash": {
7247+ "name": "password_hash",
7248+ "type": "text",
7249+ "primaryKey": false,
7250+ "notNull": false
7251+ },
7252+ "name": {
7253+ "name": "name",
7254+ "type": "text",
7255+ "primaryKey": false,
7256+ "notNull": false
7257+ },
7258+ "role": {
7259+ "name": "role",
7260+ "type": "text",
7261+ "primaryKey": false,
7262+ "notNull": true,
7263+ "default": "'user'"
7264+ },
7265+ "display_currency": {
7266+ "name": "display_currency",
7267+ "type": "text",
7268+ "primaryKey": false,
7269+ "notNull": true,
7270+ "default": "'USD'"
7271+ },
7272+ "providers": {
7273+ "name": "providers",
7274+ "type": "jsonb",
7275+ "primaryKey": false,
7276+ "notNull": true,
7277+ "default": "'[]'::jsonb"
7278+ },
7279+ "preferences": {
7280+ "name": "preferences",
7281+ "type": "jsonb",
7282+ "primaryKey": false,
7283+ "notNull": true,
7284+ "default": "'{}'::jsonb"
7285+ },
7286+ "handle": {
7287+ "name": "handle",
7288+ "type": "text",
7289+ "primaryKey": false,
7290+ "notNull": false
7291+ },
7292+ "avatar_url": {
7293+ "name": "avatar_url",
7294+ "type": "text",
7295+ "primaryKey": false,
7296+ "notNull": false
7297+ },
7298+ "bio": {
7299+ "name": "bio",
7300+ "type": "text",
7301+ "primaryKey": false,
7302+ "notNull": false
7303+ },
7304+ "mfa_enabled": {
7305+ "name": "mfa_enabled",
7306+ "type": "boolean",
7307+ "primaryKey": false,
7308+ "notNull": true,
7309+ "default": false
7310+ },
7311+ "totp_secret_enc": {
7312+ "name": "totp_secret_enc",
7313+ "type": "text",
7314+ "primaryKey": false,
7315+ "notNull": false
7316+ },
7317+ "always_ask_code": {
7318+ "name": "always_ask_code",
7319+ "type": "boolean",
7320+ "primaryKey": false,
7321+ "notNull": true,
7322+ "default": true
7323+ },
7324+ "password_changed_at": {
7325+ "name": "password_changed_at",
7326+ "type": "timestamp with time zone",
7327+ "primaryKey": false,
7328+ "notNull": false
7329+ },
7330+ "pending_email": {
7331+ "name": "pending_email",
7332+ "type": "text",
7333+ "primaryKey": false,
7334+ "notNull": false
7335+ },
7336+ "deleted_at": {
7337+ "name": "deleted_at",
7338+ "type": "timestamp with time zone",
7339+ "primaryKey": false,
7340+ "notNull": false
7341+ },
7342+ "purge_after": {
7343+ "name": "purge_after",
7344+ "type": "timestamp with time zone",
7345+ "primaryKey": false,
7346+ "notNull": false
7347+ },
7348+ "created_at": {
7349+ "name": "created_at",
7350+ "type": "timestamp with time zone",
7351+ "primaryKey": false,
7352+ "notNull": true,
7353+ "default": "now()"
7354+ },
7355+ "last_login_at": {
7356+ "name": "last_login_at",
7357+ "type": "timestamp with time zone",
7358+ "primaryKey": false,
7359+ "notNull": false
7360+ }
7361+ },
7362+ "indexes": {},
7363+ "foreignKeys": {},
7364+ "compositePrimaryKeys": {},
7365+ "uniqueConstraints": {
7366+ "users_email_unique": {
7367+ "name": "users_email_unique",
7368+ "nullsNotDistinct": false,
7369+ "columns": [
7370+ "email"
7371+ ]
7372+ },
7373+ "users_handle_unique": {
7374+ "name": "users_handle_unique",
7375+ "nullsNotDistinct": false,
7376+ "columns": [
7377+ "handle"
7378+ ]
7379+ }
7380+ },
7381+ "policies": {},
7382+ "checkConstraints": {},
7383+ "isRLSEnabled": false
7384+ },
7385+ "public.watchlist_items": {
7386+ "name": "watchlist_items",
7387+ "schema": "",
7388+ "columns": {
7389+ "id": {
7390+ "name": "id",
7391+ "type": "text",
7392+ "primaryKey": true,
7393+ "notNull": true
7394+ },
7395+ "watchlist_id": {
7396+ "name": "watchlist_id",
7397+ "type": "text",
7398+ "primaryKey": false,
7399+ "notNull": true
7400+ },
7401+ "target_type": {
7402+ "name": "target_type",
7403+ "type": "text",
7404+ "primaryKey": false,
7405+ "notNull": true
7406+ },
7407+ "target_id": {
7408+ "name": "target_id",
7409+ "type": "text",
7410+ "primaryKey": false,
7411+ "notNull": true
7412+ },
7413+ "label": {
7414+ "name": "label",
7415+ "type": "text",
7416+ "primaryKey": false,
7417+ "notNull": false
7418+ },
7419+ "note": {
7420+ "name": "note",
7421+ "type": "text",
7422+ "primaryKey": false,
7423+ "notNull": false
7424+ },
7425+ "target_price_usd": {
7426+ "name": "target_price_usd",
7427+ "type": "numeric(18, 4)",
7428+ "primaryKey": false,
7429+ "notNull": false
7430+ },
7431+ "baseline_usd": {
7432+ "name": "baseline_usd",
7433+ "type": "numeric(18, 4)",
7434+ "primaryKey": false,
7435+ "notNull": false
7436+ },
7437+ "created_at": {
7438+ "name": "created_at",
7439+ "type": "timestamp with time zone",
7440+ "primaryKey": false,
7441+ "notNull": true,
7442+ "default": "now()"
7443+ }
7444+ },
7445+ "indexes": {
7446+ "watchlist_items_uq": {
7447+ "name": "watchlist_items_uq",
7448+ "columns": [
7449+ {
7450+ "expression": "watchlist_id",
7451+ "isExpression": false,
7452+ "asc": true,
7453+ "nulls": "last"
7454+ },
7455+ {
7456+ "expression": "target_type",
7457+ "isExpression": false,
7458+ "asc": true,
7459+ "nulls": "last"
7460+ },
7461+ {
7462+ "expression": "target_id",
7463+ "isExpression": false,
7464+ "asc": true,
7465+ "nulls": "last"
7466+ }
7467+ ],
7468+ "isUnique": true,
7469+ "concurrently": false,
7470+ "method": "btree",
7471+ "with": {}
7472+ }
7473+ },
7474+ "foreignKeys": {},
7475+ "compositePrimaryKeys": {},
7476+ "uniqueConstraints": {},
7477+ "policies": {},
7478+ "checkConstraints": {},
7479+ "isRLSEnabled": false
7480+ },
7481+ "public.watchlists": {
7482+ "name": "watchlists",
7483+ "schema": "",
7484+ "columns": {
7485+ "id": {
7486+ "name": "id",
7487+ "type": "text",
7488+ "primaryKey": true,
7489+ "notNull": true
7490+ },
7491+ "user_id": {
7492+ "name": "user_id",
7493+ "type": "text",
7494+ "primaryKey": false,
7495+ "notNull": true
7496+ },
7497+ "name": {
7498+ "name": "name",
7499+ "type": "text",
7500+ "primaryKey": false,
7501+ "notNull": true,
7502+ "default": "'Watchlist'"
7503+ },
7504+ "created_at": {
7505+ "name": "created_at",
7506+ "type": "timestamp with time zone",
7507+ "primaryKey": false,
7508+ "notNull": true,
7509+ "default": "now()"
7510+ }
7511+ },
7512+ "indexes": {
7513+ "watchlists_user_idx": {
7514+ "name": "watchlists_user_idx",
7515+ "columns": [
7516+ {
7517+ "expression": "user_id",
7518+ "isExpression": false,
7519+ "asc": true,
7520+ "nulls": "last"
7521+ }
7522+ ],
7523+ "isUnique": false,
7524+ "concurrently": false,
7525+ "method": "btree",
7526+ "with": {}
7527+ }
7528+ },
7529+ "foreignKeys": {},
7530+ "compositePrimaryKeys": {},
7531+ "uniqueConstraints": {},
7532+ "policies": {},
7533+ "checkConstraints": {},
7534+ "isRLSEnabled": false
7535+ },
7536+ "public.ai_quotas": {
7537+ "name": "ai_quotas",
7538+ "schema": "",
7539+ "columns": {
7540+ "key": {
7541+ "name": "key",
7542+ "type": "text",
7543+ "primaryKey": false,
7544+ "notNull": true
7545+ },
7546+ "feature": {
7547+ "name": "feature",
7548+ "type": "text",
7549+ "primaryKey": false,
7550+ "notNull": true
7551+ },
7552+ "date": {
7553+ "name": "date",
7554+ "type": "text",
7555+ "primaryKey": false,
7556+ "notNull": true
7557+ },
7558+ "count": {
7559+ "name": "count",
7560+ "type": "integer",
7561+ "primaryKey": false,
7562+ "notNull": true,
7563+ "default": 0
7564+ }
7565+ },
7566+ "indexes": {
7567+ "ai_quotas_uq": {
7568+ "name": "ai_quotas_uq",
7569+ "columns": [
7570+ {
7571+ "expression": "key",
7572+ "isExpression": false,
7573+ "asc": true,
7574+ "nulls": "last"
7575+ },
7576+ {
7577+ "expression": "feature",
7578+ "isExpression": false,
7579+ "asc": true,
7580+ "nulls": "last"
7581+ },
7582+ {
7583+ "expression": "date",
7584+ "isExpression": false,
7585+ "asc": true,
7586+ "nulls": "last"
7587+ }
7588+ ],
7589+ "isUnique": true,
7590+ "concurrently": false,
7591+ "method": "btree",
7592+ "with": {}
7593+ }
7594+ },
7595+ "foreignKeys": {},
7596+ "compositePrimaryKeys": {},
7597+ "uniqueConstraints": {},
7598+ "policies": {},
7599+ "checkConstraints": {},
7600+ "isRLSEnabled": false
7601+ },
7602+ "public.research_messages": {
7603+ "name": "research_messages",
7604+ "schema": "",
7605+ "columns": {
7606+ "id": {
7607+ "name": "id",
7608+ "type": "text",
7609+ "primaryKey": true,
7610+ "notNull": true
7611+ },
7612+ "session_id": {
7613+ "name": "session_id",
7614+ "type": "text",
7615+ "primaryKey": false,
7616+ "notNull": true
7617+ },
7618+ "role": {
7619+ "name": "role",
7620+ "type": "text",
7621+ "primaryKey": false,
7622+ "notNull": true
7623+ },
7624+ "content": {
7625+ "name": "content",
7626+ "type": "text",
7627+ "primaryKey": false,
7628+ "notNull": true
7629+ },
7630+ "tool_calls": {
7631+ "name": "tool_calls",
7632+ "type": "jsonb",
7633+ "primaryKey": false,
7634+ "notNull": true,
7635+ "default": "'[]'::jsonb"
7636+ },
7637+ "usage": {
7638+ "name": "usage",
7639+ "type": "jsonb",
7640+ "primaryKey": false,
7641+ "notNull": true,
7642+ "default": "'{}'::jsonb"
7643+ },
7644+ "usd_est": {
7645+ "name": "usd_est",
7646+ "type": "real",
7647+ "primaryKey": false,
7648+ "notNull": true,
7649+ "default": 0
7650+ },
7651+ "model": {
7652+ "name": "model",
7653+ "type": "text",
7654+ "primaryKey": false,
7655+ "notNull": false
7656+ },
7657+ "created_at": {
7658+ "name": "created_at",
7659+ "type": "timestamp with time zone",
7660+ "primaryKey": false,
7661+ "notNull": true,
7662+ "default": "now()"
7663+ }
7664+ },
7665+ "indexes": {
7666+ "research_messages_session_idx": {
7667+ "name": "research_messages_session_idx",
7668+ "columns": [
7669+ {
7670+ "expression": "session_id",
7671+ "isExpression": false,
7672+ "asc": true,
7673+ "nulls": "last"
7674+ },
7675+ {
7676+ "expression": "created_at",
7677+ "isExpression": false,
7678+ "asc": true,
7679+ "nulls": "last"
7680+ }
7681+ ],
7682+ "isUnique": false,
7683+ "concurrently": false,
7684+ "method": "btree",
7685+ "with": {}
7686+ }
7687+ },
7688+ "foreignKeys": {},
7689+ "compositePrimaryKeys": {},
7690+ "uniqueConstraints": {},
7691+ "policies": {},
7692+ "checkConstraints": {},
7693+ "isRLSEnabled": false
7694+ },
7695+ "public.research_sessions": {
7696+ "name": "research_sessions",
7697+ "schema": "",
7698+ "columns": {
7699+ "id": {
7700+ "name": "id",
7701+ "type": "text",
7702+ "primaryKey": true,
7703+ "notNull": true
7704+ },
7705+ "user_id": {
7706+ "name": "user_id",
7707+ "type": "text",
7708+ "primaryKey": false,
7709+ "notNull": false
7710+ },
7711+ "anon_id": {
7712+ "name": "anon_id",
7713+ "type": "text",
7714+ "primaryKey": false,
7715+ "notNull": false
7716+ },
7717+ "title": {
7718+ "name": "title",
7719+ "type": "text",
7720+ "primaryKey": false,
7721+ "notNull": false
7722+ },
7723+ "model": {
7724+ "name": "model",
7725+ "type": "text",
7726+ "primaryKey": false,
7727+ "notNull": false
7728+ },
7729+ "message_count": {
7730+ "name": "message_count",
7731+ "type": "integer",
7732+ "primaryKey": false,
7733+ "notNull": true,
7734+ "default": 0
7735+ },
7736+ "usd_est": {
7737+ "name": "usd_est",
7738+ "type": "real",
7739+ "primaryKey": false,
7740+ "notNull": true,
7741+ "default": 0
7742+ },
7743+ "archived": {
7744+ "name": "archived",
7745+ "type": "boolean",
7746+ "primaryKey": false,
7747+ "notNull": true,
7748+ "default": false
7749+ },
7750+ "created_at": {
7751+ "name": "created_at",
7752+ "type": "timestamp with time zone",
7753+ "primaryKey": false,
7754+ "notNull": true,
7755+ "default": "now()"
7756+ },
7757+ "updated_at": {
7758+ "name": "updated_at",
7759+ "type": "timestamp with time zone",
7760+ "primaryKey": false,
7761+ "notNull": true,
7762+ "default": "now()"
7763+ }
7764+ },
7765+ "indexes": {
7766+ "research_sessions_anon_idx": {
7767+ "name": "research_sessions_anon_idx",
7768+ "columns": [
7769+ {
7770+ "expression": "anon_id",
7771+ "isExpression": false,
7772+ "asc": true,
7773+ "nulls": "last"
7774+ },
7775+ {
7776+ "expression": "updated_at",
7777+ "isExpression": false,
7778+ "asc": true,
7779+ "nulls": "last"
7780+ }
7781+ ],
7782+ "isUnique": false,
7783+ "concurrently": false,
7784+ "method": "btree",
7785+ "with": {}
7786+ },
7787+ "research_sessions_user_idx": {
7788+ "name": "research_sessions_user_idx",
7789+ "columns": [
7790+ {
7791+ "expression": "user_id",
7792+ "isExpression": false,
7793+ "asc": true,
7794+ "nulls": "last"
7795+ },
7796+ {
7797+ "expression": "updated_at",
7798+ "isExpression": false,
7799+ "asc": true,
7800+ "nulls": "last"
7801+ }
7802+ ],
7803+ "isUnique": false,
7804+ "concurrently": false,
7805+ "method": "btree",
7806+ "with": {}
7807+ }
7808+ },
7809+ "foreignKeys": {},
7810+ "compositePrimaryKeys": {},
7811+ "uniqueConstraints": {},
7812+ "policies": {},
7813+ "checkConstraints": {},
7814+ "isRLSEnabled": false
7815+ },
7816+ "public.scanner_sessions": {
7817+ "name": "scanner_sessions",
7818+ "schema": "",
7819+ "columns": {
7820+ "id": {
7821+ "name": "id",
7822+ "type": "text",
7823+ "primaryKey": true,
7824+ "notNull": true
7825+ },
7826+ "user_id": {
7827+ "name": "user_id",
7828+ "type": "text",
7829+ "primaryKey": false,
7830+ "notNull": false
7831+ },
7832+ "anon_id": {
7833+ "name": "anon_id",
7834+ "type": "text",
7835+ "primaryKey": false,
7836+ "notNull": false
7837+ },
7838+ "ip_hash": {
7839+ "name": "ip_hash",
7840+ "type": "text",
7841+ "primaryKey": false,
7842+ "notNull": false
7843+ },
7844+ "mode": {
7845+ "name": "mode",
7846+ "type": "text",
7847+ "primaryKey": false,
7848+ "notNull": true
7849+ },
7850+ "input_url": {
7851+ "name": "input_url",
7852+ "type": "text",
7853+ "primaryKey": false,
7854+ "notNull": false
7855+ },
7856+ "input_text": {
7857+ "name": "input_text",
7858+ "type": "text",
7859+ "primaryKey": false,
7860+ "notNull": false
7861+ },
7862+ "image_count": {
7863+ "name": "image_count",
7864+ "type": "integer",
7865+ "primaryKey": false,
7866+ "notNull": true,
7867+ "default": 0
7868+ },
7869+ "thumbnails": {
7870+ "name": "thumbnails",
7871+ "type": "jsonb",
7872+ "primaryKey": false,
7873+ "notNull": true,
7874+ "default": "'[]'::jsonb"
7875+ },
7876+ "guess": {
7877+ "name": "guess",
7878+ "type": "jsonb",
7879+ "primaryKey": false,
7880+ "notNull": true,
7881+ "default": "'{}'::jsonb"
7882+ },
7883+ "guess_confidence": {
7884+ "name": "guess_confidence",
7885+ "type": "real",
7886+ "primaryKey": false,
7887+ "notNull": false
7888+ },
7889+ "candidates": {
7890+ "name": "candidates",
7891+ "type": "jsonb",
7892+ "primaryKey": false,
7893+ "notNull": true,
7894+ "default": "'[]'::jsonb"
7895+ },
7896+ "chosen_asset_id": {
7897+ "name": "chosen_asset_id",
7898+ "type": "text",
7899+ "primaryKey": false,
7900+ "notNull": false
7901+ },
7902+ "listing": {
7903+ "name": "listing",
7904+ "type": "jsonb",
7905+ "primaryKey": false,
7906+ "notNull": true,
7907+ "default": "'{}'::jsonb"
7908+ },
7909+ "model": {
7910+ "name": "model",
7911+ "type": "text",
7912+ "primaryKey": false,
7913+ "notNull": false
7914+ },
7915+ "usd_est": {
7916+ "name": "usd_est",
7917+ "type": "real",
7918+ "primaryKey": false,
7919+ "notNull": true,
7920+ "default": 0
7921+ },
7922+ "duration_ms": {
7923+ "name": "duration_ms",
7924+ "type": "integer",
7925+ "primaryKey": false,
7926+ "notNull": false
7927+ },
7928+ "error": {
7929+ "name": "error",
7930+ "type": "text",
7931+ "primaryKey": false,
7932+ "notNull": false
7933+ },
7934+ "created_at": {
7935+ "name": "created_at",
7936+ "type": "timestamp with time zone",
7937+ "primaryKey": false,
7938+ "notNull": true,
7939+ "default": "now()"
7940+ },
7941+ "chosen_at": {
7942+ "name": "chosen_at",
7943+ "type": "timestamp with time zone",
7944+ "primaryKey": false,
7945+ "notNull": false
7946+ }
7947+ },
7948+ "indexes": {
7949+ "scanner_sessions_created_idx": {
7950+ "name": "scanner_sessions_created_idx",
7951+ "columns": [
7952+ {
7953+ "expression": "created_at",
7954+ "isExpression": false,
7955+ "asc": true,
7956+ "nulls": "last"
7957+ }
7958+ ],
7959+ "isUnique": false,
7960+ "concurrently": false,
7961+ "method": "btree",
7962+ "with": {}
7963+ },
7964+ "scanner_sessions_ip_idx": {
7965+ "name": "scanner_sessions_ip_idx",
7966+ "columns": [
7967+ {
7968+ "expression": "ip_hash",
7969+ "isExpression": false,
7970+ "asc": true,
7971+ "nulls": "last"
7972+ },
7973+ {
7974+ "expression": "created_at",
7975+ "isExpression": false,
7976+ "asc": true,
7977+ "nulls": "last"
7978+ }
7979+ ],
7980+ "isUnique": false,
7981+ "concurrently": false,
7982+ "method": "btree",
7983+ "with": {}
7984+ },
7985+ "scanner_sessions_user_idx": {
7986+ "name": "scanner_sessions_user_idx",
7987+ "columns": [
7988+ {
7989+ "expression": "user_id",
7990+ "isExpression": false,
7991+ "asc": true,
7992+ "nulls": "last"
7993+ }
7994+ ],
7995+ "isUnique": false,
7996+ "concurrently": false,
7997+ "method": "btree",
7998+ "with": {}
7999+ }
8000+ },
8001+ "foreignKeys": {},
8002+ "compositePrimaryKeys": {},
8003+ "uniqueConstraints": {},
8004+ "policies": {},
8005+ "checkConstraints": {},
8006+ "isRLSEnabled": false
8007+ },
8008+ "public.auth_codes": {
8009+ "name": "auth_codes",
8010+ "schema": "",
8011+ "columns": {
8012+ "id": {
8013+ "name": "id",
8014+ "type": "text",
8015+ "primaryKey": true,
8016+ "notNull": true
8017+ },
8018+ "user_id": {
8019+ "name": "user_id",
8020+ "type": "text",
8021+ "primaryKey": false,
8022+ "notNull": false
8023+ },
8024+ "email": {
8025+ "name": "email",
8026+ "type": "text",
8027+ "primaryKey": false,
8028+ "notNull": true
8029+ },
8030+ "purpose": {
8031+ "name": "purpose",
8032+ "type": "text",
8033+ "primaryKey": false,
8034+ "notNull": true
8035+ },
8036+ "code_hash": {
8037+ "name": "code_hash",
8038+ "type": "text",
8039+ "primaryKey": false,
8040+ "notNull": true
8041+ },
8042+ "expires_at": {
8043+ "name": "expires_at",
8044+ "type": "timestamp with time zone",
8045+ "primaryKey": false,
8046+ "notNull": true
8047+ },
8048+ "attempts": {
8049+ "name": "attempts",
8050+ "type": "integer",
8051+ "primaryKey": false,
8052+ "notNull": true,
8053+ "default": 0
8054+ },
8055+ "consumed_at": {
8056+ "name": "consumed_at",
8057+ "type": "timestamp with time zone",
8058+ "primaryKey": false,
8059+ "notNull": false
8060+ },
8061+ "payload": {
8062+ "name": "payload",
8063+ "type": "jsonb",
8064+ "primaryKey": false,
8065+ "notNull": true,
8066+ "default": "'{}'::jsonb"
8067+ },
8068+ "created_at": {
8069+ "name": "created_at",
8070+ "type": "timestamp with time zone",
8071+ "primaryKey": false,
8072+ "notNull": true,
8073+ "default": "now()"
8074+ }
8075+ },
8076+ "indexes": {
8077+ "auth_codes_lookup_idx": {
8078+ "name": "auth_codes_lookup_idx",
8079+ "columns": [
8080+ {
8081+ "expression": "email",
8082+ "isExpression": false,
8083+ "asc": true,
8084+ "nulls": "last"
8085+ },
8086+ {
8087+ "expression": "purpose",
8088+ "isExpression": false,
8089+ "asc": true,
8090+ "nulls": "last"
8091+ },
8092+ {
8093+ "expression": "created_at",
8094+ "isExpression": false,
8095+ "asc": true,
8096+ "nulls": "last"
8097+ }
8098+ ],
8099+ "isUnique": false,
8100+ "concurrently": false,
8101+ "method": "btree",
8102+ "with": {}
8103+ }
8104+ },
8105+ "foreignKeys": {},
8106+ "compositePrimaryKeys": {},
8107+ "uniqueConstraints": {},
8108+ "policies": {},
8109+ "checkConstraints": {},
8110+ "isRLSEnabled": false
8111+ },
8112+ "public.login_events": {
8113+ "name": "login_events",
8114+ "schema": "",
8115+ "columns": {
8116+ "id": {
8117+ "name": "id",
8118+ "type": "text",
8119+ "primaryKey": true,
8120+ "notNull": true
8121+ },
8122+ "user_id": {
8123+ "name": "user_id",
8124+ "type": "text",
8125+ "primaryKey": false,
8126+ "notNull": false
8127+ },
8128+ "email": {
8129+ "name": "email",
8130+ "type": "text",
8131+ "primaryKey": false,
8132+ "notNull": false
8133+ },
8134+ "ip": {
8135+ "name": "ip",
8136+ "type": "text",
8137+ "primaryKey": false,
8138+ "notNull": false
8139+ },
8140+ "user_agent": {
8141+ "name": "user_agent",
8142+ "type": "text",
8143+ "primaryKey": false,
8144+ "notNull": false
8145+ },
8146+ "outcome": {
8147+ "name": "outcome",
8148+ "type": "text",
8149+ "primaryKey": false,
8150+ "notNull": true
8151+ },
8152+ "method": {
8153+ "name": "method",
8154+ "type": "text",
8155+ "primaryKey": false,
8156+ "notNull": false
8157+ },
8158+ "created_at": {
8159+ "name": "created_at",
8160+ "type": "timestamp with time zone",
8161+ "primaryKey": false,
8162+ "notNull": true,
8163+ "default": "now()"
8164+ }
8165+ },
8166+ "indexes": {
8167+ "login_events_user_idx": {
8168+ "name": "login_events_user_idx",
8169+ "columns": [
8170+ {
8171+ "expression": "user_id",
8172+ "isExpression": false,
8173+ "asc": true,
8174+ "nulls": "last"
8175+ },
8176+ {
8177+ "expression": "created_at",
8178+ "isExpression": false,
8179+ "asc": true,
8180+ "nulls": "last"
8181+ }
8182+ ],
8183+ "isUnique": false,
8184+ "concurrently": false,
8185+ "method": "btree",
8186+ "with": {}
8187+ }
8188+ },
8189+ "foreignKeys": {},
8190+ "compositePrimaryKeys": {},
8191+ "uniqueConstraints": {},
8192+ "policies": {},
8193+ "checkConstraints": {},
8194+ "isRLSEnabled": false
8195+ },
8196+ "public.notifications": {
8197+ "name": "notifications",
8198+ "schema": "",
8199+ "columns": {
8200+ "id": {
8201+ "name": "id",
8202+ "type": "text",
8203+ "primaryKey": true,
8204+ "notNull": true
8205+ },
8206+ "user_id": {
8207+ "name": "user_id",
8208+ "type": "text",
8209+ "primaryKey": false,
8210+ "notNull": true
8211+ },
8212+ "kind": {
8213+ "name": "kind",
8214+ "type": "text",
8215+ "primaryKey": false,
8216+ "notNull": true
8217+ },
8218+ "title": {
8219+ "name": "title",
8220+ "type": "text",
8221+ "primaryKey": false,
8222+ "notNull": true
8223+ },
8224+ "body": {
8225+ "name": "body",
8226+ "type": "text",
8227+ "primaryKey": false,
8228+ "notNull": false
8229+ },
8230+ "href": {
8231+ "name": "href",
8232+ "type": "text",
8233+ "primaryKey": false,
8234+ "notNull": false
8235+ },
8236+ "payload": {
8237+ "name": "payload",
8238+ "type": "jsonb",
8239+ "primaryKey": false,
8240+ "notNull": true,
8241+ "default": "'{}'::jsonb"
8242+ },
8243+ "read_at": {
8244+ "name": "read_at",
8245+ "type": "timestamp with time zone",
8246+ "primaryKey": false,
8247+ "notNull": false
8248+ },
8249+ "emailed_at": {
8250+ "name": "emailed_at",
8251+ "type": "timestamp with time zone",
8252+ "primaryKey": false,
8253+ "notNull": false
8254+ },
8255+ "created_at": {
8256+ "name": "created_at",
8257+ "type": "timestamp with time zone",
8258+ "primaryKey": false,
8259+ "notNull": true,
8260+ "default": "now()"
8261+ }
8262+ },
8263+ "indexes": {
8264+ "notifications_user_idx": {
8265+ "name": "notifications_user_idx",
8266+ "columns": [
8267+ {
8268+ "expression": "user_id",
8269+ "isExpression": false,
8270+ "asc": true,
8271+ "nulls": "last"
8272+ },
8273+ {
8274+ "expression": "created_at",
8275+ "isExpression": false,
8276+ "asc": true,
8277+ "nulls": "last"
8278+ }
8279+ ],
8280+ "isUnique": false,
8281+ "concurrently": false,
8282+ "method": "btree",
8283+ "with": {}
8284+ },
8285+ "notifications_unread_idx": {
8286+ "name": "notifications_unread_idx",
8287+ "columns": [
8288+ {
8289+ "expression": "user_id",
8290+ "isExpression": false,
8291+ "asc": true,
8292+ "nulls": "last"
8293+ },
8294+ {
8295+ "expression": "read_at",
8296+ "isExpression": false,
8297+ "asc": true,
8298+ "nulls": "last"
8299+ }
8300+ ],
8301+ "isUnique": false,
8302+ "concurrently": false,
8303+ "method": "btree",
8304+ "with": {}
8305+ }
8306+ },
8307+ "foreignKeys": {},
8308+ "compositePrimaryKeys": {},
8309+ "uniqueConstraints": {},
8310+ "policies": {},
8311+ "checkConstraints": {},
8312+ "isRLSEnabled": false
8313+ },
8314+ "public.price_targets": {
8315+ "name": "price_targets",
8316+ "schema": "",
8317+ "columns": {
8318+ "id": {
8319+ "name": "id",
8320+ "type": "text",
8321+ "primaryKey": true,
8322+ "notNull": true
8323+ },
8324+ "user_id": {
8325+ "name": "user_id",
8326+ "type": "text",
8327+ "primaryKey": false,
8328+ "notNull": true
8329+ },
8330+ "asset_id": {
8331+ "name": "asset_id",
8332+ "type": "text",
8333+ "primaryKey": false,
8334+ "notNull": true
8335+ },
8336+ "variant_id": {
8337+ "name": "variant_id",
8338+ "type": "text",
8339+ "primaryKey": false,
8340+ "notNull": false
8341+ },
8342+ "direction": {
8343+ "name": "direction",
8344+ "type": "text",
8345+ "primaryKey": false,
8346+ "notNull": true,
8347+ "default": "'above'"
8348+ },
8349+ "target_usd": {
8350+ "name": "target_usd",
8351+ "type": "numeric(18, 4)",
8352+ "primaryKey": false,
8353+ "notNull": true
8354+ },
8355+ "baseline_usd": {
8356+ "name": "baseline_usd",
8357+ "type": "numeric(18, 4)",
8358+ "primaryKey": false,
8359+ "notNull": false
8360+ },
8361+ "note": {
8362+ "name": "note",
8363+ "type": "text",
8364+ "primaryKey": false,
8365+ "notNull": false
8366+ },
8367+ "hit_at": {
8368+ "name": "hit_at",
8369+ "type": "timestamp with time zone",
8370+ "primaryKey": false,
8371+ "notNull": false
8372+ },
8373+ "notified_at": {
8374+ "name": "notified_at",
8375+ "type": "timestamp with time zone",
8376+ "primaryKey": false,
8377+ "notNull": false
8378+ },
8379+ "created_at": {
8380+ "name": "created_at",
8381+ "type": "timestamp with time zone",
8382+ "primaryKey": false,
8383+ "notNull": true,
8384+ "default": "now()"
8385+ }
8386+ },
8387+ "indexes": {
8388+ "price_targets_user_idx": {
8389+ "name": "price_targets_user_idx",
8390+ "columns": [
8391+ {
8392+ "expression": "user_id",
8393+ "isExpression": false,
8394+ "asc": true,
8395+ "nulls": "last"
8396+ }
8397+ ],
8398+ "isUnique": false,
8399+ "concurrently": false,
8400+ "method": "btree",
8401+ "with": {}
8402+ },
8403+ "price_targets_asset_idx": {
8404+ "name": "price_targets_asset_idx",
8405+ "columns": [
8406+ {
8407+ "expression": "asset_id",
8408+ "isExpression": false,
8409+ "asc": true,
8410+ "nulls": "last"
8411+ },
8412+ {
8413+ "expression": "hit_at",
8414+ "isExpression": false,
8415+ "asc": true,
8416+ "nulls": "last"
8417+ }
8418+ ],
8419+ "isUnique": false,
8420+ "concurrently": false,
8421+ "method": "btree",
8422+ "with": {}
8423+ }
8424+ },
8425+ "foreignKeys": {},
8426+ "compositePrimaryKeys": {},
8427+ "uniqueConstraints": {},
8428+ "policies": {},
8429+ "checkConstraints": {},
8430+ "isRLSEnabled": false
8431+ },
8432+ "public.rate_limits": {
8433+ "name": "rate_limits",
8434+ "schema": "",
8435+ "columns": {
8436+ "key": {
8437+ "name": "key",
8438+ "type": "text",
8439+ "primaryKey": true,
8440+ "notNull": true
8441+ },
8442+ "count": {
8443+ "name": "count",
8444+ "type": "integer",
8445+ "primaryKey": false,
8446+ "notNull": true,
8447+ "default": 0
8448+ },
8449+ "reset_at": {
8450+ "name": "reset_at",
8451+ "type": "timestamp with time zone",
8452+ "primaryKey": false,
8453+ "notNull": true
8454+ }
8455+ },
8456+ "indexes": {},
8457+ "foreignKeys": {},
8458+ "compositePrimaryKeys": {},
8459+ "uniqueConstraints": {},
8460+ "policies": {},
8461+ "checkConstraints": {},
8462+ "isRLSEnabled": false
8463+ },
8464+ "public.recovery_codes": {
8465+ "name": "recovery_codes",
8466+ "schema": "",
8467+ "columns": {
8468+ "id": {
8469+ "name": "id",
8470+ "type": "text",
8471+ "primaryKey": true,
8472+ "notNull": true
8473+ },
8474+ "user_id": {
8475+ "name": "user_id",
8476+ "type": "text",
8477+ "primaryKey": false,
8478+ "notNull": true
8479+ },
8480+ "code_hash": {
8481+ "name": "code_hash",
8482+ "type": "text",
8483+ "primaryKey": false,
8484+ "notNull": true
8485+ },
8486+ "used_at": {
8487+ "name": "used_at",
8488+ "type": "timestamp with time zone",
8489+ "primaryKey": false,
8490+ "notNull": false
8491+ },
8492+ "created_at": {
8493+ "name": "created_at",
8494+ "type": "timestamp with time zone",
8495+ "primaryKey": false,
8496+ "notNull": true,
8497+ "default": "now()"
8498+ }
8499+ },
8500+ "indexes": {
8501+ "recovery_codes_user_idx": {
8502+ "name": "recovery_codes_user_idx",
8503+ "columns": [
8504+ {
8505+ "expression": "user_id",
8506+ "isExpression": false,
8507+ "asc": true,
8508+ "nulls": "last"
8509+ }
8510+ ],
8511+ "isUnique": false,
8512+ "concurrently": false,
8513+ "method": "btree",
8514+ "with": {}
8515+ }
8516+ },
8517+ "foreignKeys": {},
8518+ "compositePrimaryKeys": {},
8519+ "uniqueConstraints": {},
8520+ "policies": {},
8521+ "checkConstraints": {},
8522+ "isRLSEnabled": false
8523+ },
8524+ "public.saved_searches": {
8525+ "name": "saved_searches",
8526+ "schema": "",
8527+ "columns": {
8528+ "id": {
8529+ "name": "id",
8530+ "type": "text",
8531+ "primaryKey": true,
8532+ "notNull": true
8533+ },
8534+ "user_id": {
8535+ "name": "user_id",
8536+ "type": "text",
8537+ "primaryKey": false,
8538+ "notNull": true
8539+ },
8540+ "name": {
8541+ "name": "name",
8542+ "type": "text",
8543+ "primaryKey": false,
8544+ "notNull": true
8545+ },
8546+ "url": {
8547+ "name": "url",
8548+ "type": "text",
8549+ "primaryKey": false,
8550+ "notNull": true
8551+ },
8552+ "params": {
8553+ "name": "params",
8554+ "type": "jsonb",
8555+ "primaryKey": false,
8556+ "notNull": true,
8557+ "default": "'{}'::jsonb"
8558+ },
8559+ "notify": {
8560+ "name": "notify",
8561+ "type": "boolean",
8562+ "primaryKey": false,
8563+ "notNull": true,
8564+ "default": false
8565+ },
8566+ "last_run_at": {
8567+ "name": "last_run_at",
8568+ "type": "timestamp with time zone",
8569+ "primaryKey": false,
8570+ "notNull": false
8571+ },
8572+ "last_count": {
8573+ "name": "last_count",
8574+ "type": "integer",
8575+ "primaryKey": false,
8576+ "notNull": false
8577+ },
8578+ "created_at": {
8579+ "name": "created_at",
8580+ "type": "timestamp with time zone",
8581+ "primaryKey": false,
8582+ "notNull": true,
8583+ "default": "now()"
8584+ }
8585+ },
8586+ "indexes": {
8587+ "saved_searches_user_idx": {
8588+ "name": "saved_searches_user_idx",
8589+ "columns": [
8590+ {
8591+ "expression": "user_id",
8592+ "isExpression": false,
8593+ "asc": true,
8594+ "nulls": "last"
8595+ }
8596+ ],
8597+ "isUnique": false,
8598+ "concurrently": false,
8599+ "method": "btree",
8600+ "with": {}
8601+ }
8602+ },
8603+ "foreignKeys": {},
8604+ "compositePrimaryKeys": {},
8605+ "uniqueConstraints": {},
8606+ "policies": {},
8607+ "checkConstraints": {},
8608+ "isRLSEnabled": false
8609+ },
8610+ "public.trusted_devices": {
8611+ "name": "trusted_devices",
8612+ "schema": "",
8613+ "columns": {
8614+ "id": {
8615+ "name": "id",
8616+ "type": "text",
8617+ "primaryKey": true,
8618+ "notNull": true
8619+ },
8620+ "user_id": {
8621+ "name": "user_id",
8622+ "type": "text",
8623+ "primaryKey": false,
8624+ "notNull": true
8625+ },
8626+ "token_hash": {
8627+ "name": "token_hash",
8628+ "type": "text",
8629+ "primaryKey": false,
8630+ "notNull": true
8631+ },
8632+ "label": {
8633+ "name": "label",
8634+ "type": "text",
8635+ "primaryKey": false,
8636+ "notNull": false
8637+ },
8638+ "user_agent": {
8639+ "name": "user_agent",
8640+ "type": "text",
8641+ "primaryKey": false,
8642+ "notNull": false
8643+ },
8644+ "ip": {
8645+ "name": "ip",
8646+ "type": "text",
8647+ "primaryKey": false,
8648+ "notNull": false
8649+ },
8650+ "created_at": {
8651+ "name": "created_at",
8652+ "type": "timestamp with time zone",
8653+ "primaryKey": false,
8654+ "notNull": true,
8655+ "default": "now()"
8656+ },
8657+ "last_used_at": {
8658+ "name": "last_used_at",
8659+ "type": "timestamp with time zone",
8660+ "primaryKey": false,
8661+ "notNull": false
8662+ },
8663+ "expires_at": {
8664+ "name": "expires_at",
8665+ "type": "timestamp with time zone",
8666+ "primaryKey": false,
8667+ "notNull": true
8668+ },
8669+ "revoked_at": {
8670+ "name": "revoked_at",
8671+ "type": "timestamp with time zone",
8672+ "primaryKey": false,
8673+ "notNull": false
8674+ }
8675+ },
8676+ "indexes": {
8677+ "trusted_devices_user_idx": {
8678+ "name": "trusted_devices_user_idx",
8679+ "columns": [
8680+ {
8681+ "expression": "user_id",
8682+ "isExpression": false,
8683+ "asc": true,
8684+ "nulls": "last"
8685+ }
8686+ ],
8687+ "isUnique": false,
8688+ "concurrently": false,
8689+ "method": "btree",
8690+ "with": {}
8691+ },
8692+ "trusted_devices_token_uq": {
8693+ "name": "trusted_devices_token_uq",
8694+ "columns": [
8695+ {
8696+ "expression": "token_hash",
8697+ "isExpression": false,
8698+ "asc": true,
8699+ "nulls": "last"
8700+ }
8701+ ],
8702+ "isUnique": true,
8703+ "concurrently": false,
8704+ "method": "btree",
8705+ "with": {}
8706+ }
8707+ },
8708+ "foreignKeys": {},
8709+ "compositePrimaryKeys": {},
8710+ "uniqueConstraints": {},
8711+ "policies": {},
8712+ "checkConstraints": {},
8713+ "isRLSEnabled": false
8714+ },
8715+ "public.uploads": {
8716+ "name": "uploads",
8717+ "schema": "",
8718+ "columns": {
8719+ "id": {
8720+ "name": "id",
8721+ "type": "text",
8722+ "primaryKey": true,
8723+ "notNull": true
8724+ },
8725+ "user_id": {
8726+ "name": "user_id",
8727+ "type": "text",
8728+ "primaryKey": false,
8729+ "notNull": true
8730+ },
8731+ "kind": {
8732+ "name": "kind",
8733+ "type": "text",
8734+ "primaryKey": false,
8735+ "notNull": true
8736+ },
8737+ "path": {
8738+ "name": "path",
8739+ "type": "text",
8740+ "primaryKey": false,
8741+ "notNull": true
8742+ },
8743+ "mime": {
8744+ "name": "mime",
8745+ "type": "text",
8746+ "primaryKey": false,
8747+ "notNull": true
8748+ },
8749+ "bytes": {
8750+ "name": "bytes",
8751+ "type": "integer",
8752+ "primaryKey": false,
8753+ "notNull": true
8754+ },
8755+ "width": {
8756+ "name": "width",
8757+ "type": "integer",
8758+ "primaryKey": false,
8759+ "notNull": false
8760+ },
8761+ "height": {
8762+ "name": "height",
8763+ "type": "integer",
8764+ "primaryKey": false,
8765+ "notNull": false
8766+ },
8767+ "created_at": {
8768+ "name": "created_at",
8769+ "type": "timestamp with time zone",
8770+ "primaryKey": false,
8771+ "notNull": true,
8772+ "default": "now()"
8773+ }
8774+ },
8775+ "indexes": {
8776+ "uploads_user_idx": {
8777+ "name": "uploads_user_idx",
8778+ "columns": [
8779+ {
8780+ "expression": "user_id",
8781+ "isExpression": false,
8782+ "asc": true,
8783+ "nulls": "last"
8784+ }
8785+ ],
8786+ "isUnique": false,
8787+ "concurrently": false,
8788+ "method": "btree",
8789+ "with": {}
8790+ }
8791+ },
8792+ "foreignKeys": {},
8793+ "compositePrimaryKeys": {},
8794+ "uniqueConstraints": {},
8795+ "policies": {},
8796+ "checkConstraints": {},
8797+ "isRLSEnabled": false
8798+ },
8799+ "public.user_badges": {
8800+ "name": "user_badges",
8801+ "schema": "",
8802+ "columns": {
8803+ "user_id": {
8804+ "name": "user_id",
8805+ "type": "text",
8806+ "primaryKey": false,
8807+ "notNull": true
8808+ },
8809+ "badge": {
8810+ "name": "badge",
8811+ "type": "text",
8812+ "primaryKey": false,
8813+ "notNull": true
8814+ },
8815+ "evidence": {
8816+ "name": "evidence",
8817+ "type": "jsonb",
8818+ "primaryKey": false,
8819+ "notNull": true,
8820+ "default": "'{}'::jsonb"
8821+ },
8822+ "created_at": {
8823+ "name": "created_at",
8824+ "type": "timestamp with time zone",
8825+ "primaryKey": false,
8826+ "notNull": true,
8827+ "default": "now()"
8828+ }
8829+ },
8830+ "indexes": {
8831+ "user_badges_uq": {
8832+ "name": "user_badges_uq",
8833+ "columns": [
8834+ {
8835+ "expression": "user_id",
8836+ "isExpression": false,
8837+ "asc": true,
8838+ "nulls": "last"
8839+ },
8840+ {
8841+ "expression": "badge",
8842+ "isExpression": false,
8843+ "asc": true,
8844+ "nulls": "last"
8845+ }
8846+ ],
8847+ "isUnique": true,
8848+ "concurrently": false,
8849+ "method": "btree",
8850+ "with": {}
8851+ }
8852+ },
8853+ "foreignKeys": {},
8854+ "compositePrimaryKeys": {},
8855+ "uniqueConstraints": {},
8856+ "policies": {},
8857+ "checkConstraints": {},
8858+ "isRLSEnabled": false
8859+ }
8860+ },
8861+ "enums": {},
8862+ "schemas": {},
8863+ "sequences": {},
8864+ "roles": {},
8865+ "policies": {},
8866+ "views": {
8867+ "public.listings_multi_currency": {
8868+ "columns": {
8869+ "listing_id": {
8870+ "name": "listing_id",
8871+ "type": "text",
8872+ "primaryKey": false,
8873+ "notNull": false
8874+ },
8875+ "asset_id": {
8876+ "name": "asset_id",
8877+ "type": "text",
8878+ "primaryKey": false,
8879+ "notNull": false
8880+ },
8881+ "last_seen_at": {
8882+ "name": "last_seen_at",
8883+ "type": "timestamp with time zone",
8884+ "primaryKey": false,
8885+ "notNull": false
8886+ },
8887+ "price": {
8888+ "name": "price",
8889+ "type": "numeric(18, 4)",
8890+ "primaryKey": false,
8891+ "notNull": false
8892+ },
8893+ "currency": {
8894+ "name": "currency",
8895+ "type": "text",
8896+ "primaryKey": false,
8897+ "notNull": false
8898+ },
8899+ "price_usd": {
8900+ "name": "price_usd",
8901+ "type": "numeric(18, 4)",
8902+ "primaryKey": false,
8903+ "notNull": false
8904+ },
8905+ "price_cad": {
8906+ "name": "price_cad",
8907+ "type": "numeric(18, 4)",
8908+ "primaryKey": false,
8909+ "notNull": false
8910+ },
8911+ "price_eur": {
8912+ "name": "price_eur",
8913+ "type": "numeric(18, 4)",
8914+ "primaryKey": false,
8915+ "notNull": false
8916+ },
8917+ "price_gbp": {
8918+ "name": "price_gbp",
8919+ "type": "numeric(18, 4)",
8920+ "primaryKey": false,
8921+ "notNull": false
8922+ },
8923+ "price_jpy": {
8924+ "name": "price_jpy",
8925+ "type": "numeric(18, 4)",
8926+ "primaryKey": false,
8927+ "notNull": false
8928+ }
8929+ },
8930+ "definition": "\n select l.id as listing_id, l.asset_id, l.last_seen_at, l.price, l.currency, l.price_usd,\n l.price_usd * cad.rate as price_cad, l.price_usd * eur.rate as price_eur, l.price_usd * gbp.rate as price_gbp, l.price_usd * jpy.rate as price_jpy\n from listings l\n left join lateral (select f.rate from fx_rates f where f.base = 'USD' and f.quote = 'CAD' and f.date <= l.last_seen_at::date order by f.date desc limit 1) cad on true\n left join lateral (select f.rate from fx_rates f where f.base = 'USD' and f.quote = 'EUR' and f.date <= l.last_seen_at::date order by f.date desc limit 1) eur on true\n left join lateral (select f.rate from fx_rates f where f.base = 'USD' and f.quote = 'GBP' and f.date <= l.last_seen_at::date order by f.date desc limit 1) gbp on true\n left join lateral (select f.rate from fx_rates f where f.base = 'USD' and f.quote = 'JPY' and f.date <= l.last_seen_at::date order by f.date desc limit 1) jpy on true\n",
8931+ "name": "listings_multi_currency",
8932+ "schema": "public",
8933+ "isExisting": false,
8934+ "materialized": false
8935+ },
8936+ "public.sales_multi_currency": {
8937+ "columns": {
8938+ "sale_id": {
8939+ "name": "sale_id",
8940+ "type": "text",
8941+ "primaryKey": false,
8942+ "notNull": false
8943+ },
8944+ "asset_id": {
8945+ "name": "asset_id",
8946+ "type": "text",
8947+ "primaryKey": false,
8948+ "notNull": false
8949+ },
8950+ "sale_date": {
8951+ "name": "sale_date",
8952+ "type": "timestamp with time zone",
8953+ "primaryKey": false,
8954+ "notNull": false
8955+ },
8956+ "price": {
8957+ "name": "price",
8958+ "type": "numeric(18, 4)",
8959+ "primaryKey": false,
8960+ "notNull": false
8961+ },
8962+ "currency": {
8963+ "name": "currency",
8964+ "type": "text",
8965+ "primaryKey": false,
8966+ "notNull": false
8967+ },
8968+ "price_usd": {
8969+ "name": "price_usd",
8970+ "type": "numeric(18, 4)",
8971+ "primaryKey": false,
8972+ "notNull": false
8973+ },
8974+ "price_cad": {
8975+ "name": "price_cad",
8976+ "type": "numeric(18, 4)",
8977+ "primaryKey": false,
8978+ "notNull": false
8979+ },
8980+ "price_eur": {
8981+ "name": "price_eur",
8982+ "type": "numeric(18, 4)",
8983+ "primaryKey": false,
8984+ "notNull": false
8985+ },
8986+ "price_gbp": {
8987+ "name": "price_gbp",
8988+ "type": "numeric(18, 4)",
8989+ "primaryKey": false,
8990+ "notNull": false
8991+ },
8992+ "price_jpy": {
8993+ "name": "price_jpy",
8994+ "type": "numeric(18, 4)",
8995+ "primaryKey": false,
8996+ "notNull": false
8997+ },
8998+ "fx_date_cad": {
8999+ "name": "fx_date_cad",
9000+ "type": "date",
9001+ "primaryKey": false,
9002+ "notNull": false
9003+ },
9004+ "fx_date_eur": {
9005+ "name": "fx_date_eur",
9006+ "type": "date",
9007+ "primaryKey": false,
9008+ "notNull": false
9009+ },
9010+ "fx_date_gbp": {
9011+ "name": "fx_date_gbp",
9012+ "type": "date",
9013+ "primaryKey": false,
9014+ "notNull": false
9015+ },
9016+ "fx_date_jpy": {
9017+ "name": "fx_date_jpy",
9018+ "type": "date",
9019+ "primaryKey": false,
9020+ "notNull": false
9021+ }
9022+ },
9023+ "definition": "\n select s.id as sale_id, s.asset_id, s.sale_date, s.price, s.currency, s.price_usd,\n s.price_usd * cad.rate as price_cad, s.price_usd * eur.rate as price_eur, s.price_usd * gbp.rate as price_gbp, s.price_usd * jpy.rate as price_jpy,\n cad.date as fx_date_cad, eur.date as fx_date_eur, gbp.date as fx_date_gbp, jpy.date as fx_date_jpy\n from sales s\n left join lateral (select f.rate, f.date from fx_rates f where f.base = 'USD' and f.quote = 'CAD' and f.date <= s.sale_date::date order by f.date desc limit 1) cad on true\n left join lateral (select f.rate, f.date from fx_rates f where f.base = 'USD' and f.quote = 'EUR' and f.date <= s.sale_date::date order by f.date desc limit 1) eur on true\n left join lateral (select f.rate, f.date from fx_rates f where f.base = 'USD' and f.quote = 'GBP' and f.date <= s.sale_date::date order by f.date desc limit 1) gbp on true\n left join lateral (select f.rate, f.date from fx_rates f where f.base = 'USD' and f.quote = 'JPY' and f.date <= s.sale_date::date order by f.date desc limit 1) jpy on true\n",
9024+ "name": "sales_multi_currency",
9025+ "schema": "public",
9026+ "isExisting": false,
9027+ "materialized": false
9028+ }
9029+ },
9030+ "_meta": {
9031+ "columns": {},
9032+ "schemas": {},
9033+ "tables": {}
9034+ }
9035+}
\ No newline at end of file
modified packages/database/migrations/meta/_journal.json +7 −0
@@ -43,6 +43,13 @@
43 "when": 1788855117528, 43 "when": 1788855117528,
44 "tag": "0005_watery_weapon_omega", 44 "tag": "0005_watery_weapon_omega",
45 "breakpoints": true 45 "breakpoints": true
46+ },
47+ {
48+ "idx": 6,
49+ "version": "7",
50+ "when": 1789101341333,
51+ "tag": "0006_skinny_kulan_gath",
52+ "breakpoints": true
46 } 53 }
47 ] 54 ]
48 } 55 }
\ No newline at end of file
modified packages/database/src/schema/ai.ts +2 −2
@@ -1,4 +1,4 @@
1−import { pgTable, text, integer, real, index, jsonb, boolean } from 'drizzle-orm/pg-core'; 1+import { pgTable, text, integer, real, index, uniqueIndex, jsonb, boolean } from 'drizzle-orm/pg-core';
2 import { createdAt, ts, jsonObject } from './_common.js'; 2 import { createdAt, ts, jsonObject } from './_common.js';
3 3
4 /** 4 /**
@@ -78,5 +78,5 @@ export const aiQuotas = pgTable(
78 date: text('date').notNull(), 78 date: text('date').notNull(),
79 count: integer('count').notNull().default(0), 79 count: integer('count').notNull().default(0),
80 }, 80 },
81− (t) => [index('ai_quotas_uq').on(t.key, t.feature, t.date)], 81+ (t) => [uniqueIndex('ai_quotas_uq').on(t.key, t.feature, t.date)],
82 ); 82 );
modified packages/database/src/schema/assets.ts +12 −2
@@ -133,7 +133,17 @@ export const assetStats = pgTable(
133 views30d: integer('views_30d').notNull().default(0), 133 views30d: integer('views_30d').notNull().default(0),
134 updatedAt: updatedAt(), 134 updatedAt: updatedAt(),
135 }, 135 },
136− (t) => [index('asset_stats_riv_idx').on(t.rivUsd), index('asset_stats_trending_idx').on(t.trendingScore), index('asset_stats_liquidity_idx').on(t.liquidityScore)], 136+ (t) => [
137+ index('asset_stats_riv_idx').on(t.rivUsd),
138+ index('asset_stats_trending_idx').on(t.trendingScore),
139+ index('asset_stats_liquidity_idx').on(t.liquidityScore),
140+ // movers, screener and opportunity rails order by these
141+ index('asset_stats_change_7d_idx').on(t.change7d),
142+ index('asset_stats_change_30d_idx').on(t.change30d),
143+ index('asset_stats_change_1y_idx').on(t.change1y),
144+ index('asset_stats_opportunity_idx').on(t.valueOpportunity).where(sql`value_opportunity is not null`),
145+ index('asset_stats_sales_30d_idx').on(t.sales30d),
146+ ],
137 ); 147 );
138 148
139 export const variantStats = pgTable('variant_stats', { 149 export const variantStats = pgTable('variant_stats', {
@@ -154,7 +164,7 @@ export const variantStats = pgTable('variant_stats', {
154 minAskUsd: money('min_ask_usd'), 164 minAskUsd: money('min_ask_usd'),
155 liquidityScore: real('liquidity_score'), 165 liquidityScore: real('liquidity_score'),
156 updatedAt: updatedAt(), 166 updatedAt: updatedAt(),
157−}); 167+}, (t) => [index('variant_stats_asset_idx').on(t.assetId)]);
158 168
159 export const images = pgTable( 169 export const images = pgTable(
160 'images', 170 'images',
modified packages/database/src/schema/market.ts +5 −0
@@ -49,6 +49,8 @@ export const sales = pgTable(
49 index('sales_source_idx').on(t.sourceId, t.saleDate), 49 index('sales_source_idx').on(t.sourceId, t.saleDate),
50 index('sales_date_idx').on(t.saleDate), 50 index('sales_date_idx').on(t.saleDate),
51 index('sales_price_idx').on(t.priceUsd), 51 index('sales_price_idx').on(t.priceUsd),
52+ // every market query filters status = 'valid' with a date/price order
53+ index('sales_valid_date_idx').on(t.saleDate).where(sql`status = 'valid'`),
52 ], 54 ],
53 ); 55 );
54 56
@@ -102,6 +104,9 @@ export const listings = pgTable(
102 index('listings_avail_price_idx').on(t.availability, t.priceUsd), 104 index('listings_avail_price_idx').on(t.availability, t.priceUsd),
103 index('listings_ends_idx').on(t.endsAt), 105 index('listings_ends_idx').on(t.endsAt),
104 index('listings_last_seen_idx').on(t.lastSeenAt), 106 index('listings_last_seen_idx').on(t.lastSeenAt),
107+ // valuation worker updates asks per variant; deal rails / Deal Radar order by discount
108+ index('listings_variant_avail_idx').on(t.variantId, t.availability),
109+ index('listings_discount_idx').on(t.discountToRiv).where(sql`discount_to_riv is not null and availability = 'available'`),
105 ], 110 ],
106 ); 111 );
107 112
modified packages/taxonomy/src/index.ts +18 −6
@@ -153,23 +153,31 @@ export function getGrader(slugOrAlias: string | null | undefined): GraderDef | u
153 return graderByAlias.get(slugOrAlias.trim().toLowerCase()); 153 return graderByAlias.get(slugOrAlias.trim().toLowerCase());
154 } 154 }
155 155
156−const GRADE_RE = /\b(PSA|BGS|BECKETT|CGC|SGC|TAG|ACE|CBCS|PGX|WATA|VGA|PCGS|NGC|ANACS|ICCS|PMG)\b[\s:-]*(?:GEM\s*MT|GEM\s*MINT|MINT|NM-?MT|PRISTINE|BLACK\s*LABEL)?[\s:-]*(\d{1,3}(?:\.\d)?)?/gi; 156+// Grader token, optional "/DNA" (PSA/DNA autograph slabs), optional descriptive word, then either a
157+// numeric grade ("10", "9.5", "(8.5)") or an authentication-only marker ("Authentic", "AUTH", "A").
158+const GRADE_RE = /\b(PSA|BGS|BECKETT|CGC|SGC|TAG|ACE|CBCS|PGX|WATA|VGA|PCGS|NGC|ANACS|ICCS|PMG)\b(?:\s*\/?\s*DNA\b)?[\s:\-\[(]*(?:GEM\s*MT|GEM\s*MINT|MINT|NM-?MT|PRISTINE|BLACK\s*LABEL)?[\s:\-\[(]*(?:(\d{1,3}(?:\.\d)?)(?![\d.])|\b(AUTHENTIC|AUTH|CERTIFIED\s+AUTHENTIC)\b)?/gi;
157 /** Grader tokens that are also ordinary words ("Tag Team", "Ace Attorney") — only trusted when followed by a grade. */ 159 /** Grader tokens that are also ordinary words ("Tag Team", "Ace Attorney") — only trusted when followed by a grade. */
158 const AMBIGUOUS_GRADERS = new Set(['tag', 'ace']); 160 const AMBIGUOUS_GRADERS = new Set(['tag', 'ace']);
161+/** Authentication-only slabs (no numeric grade). Kept apart from raw and from graded variants. */
162+export const AUTHENTIC_GRADE = 'authentic';
159 163
160−/** Extract grader + grade from a free-text title. Returns nulls when absent. */ 164+/**
165+ * Extract grader + grade from a free-text title. Returns nulls when absent.
166+ * A grader token WITHOUT a readable grade yields `{ grader, grade: null }` — callers must treat that as
167+ * "graded, grade unknown" (never as raw): mixing such records into the raw variant polluted RIVs.
168+ */
161 export function parseGradeFromTitle(title: string): { grader: string | null; grade: string | null; qualifier: string | null } { 169 export function parseGradeFromTitle(title: string): { grader: string | null; grade: string | null; qualifier: string | null } {
162 const matches = [...title.matchAll(GRADE_RE)]; 170 const matches = [...title.matchAll(GRADE_RE)];
163 // Prefer the first match that carries a numeric grade; ambiguous tokens without a grade are ignored. 171 // Prefer the first match that carries a numeric grade; ambiguous tokens without a grade are ignored.
164− const m = matches.find((x) => x[2]) ?? matches.find((x) => !AMBIGUOUS_GRADERS.has(x[1]!.toLowerCase())); 172+ const m = matches.find((x) => x[2]) ?? matches.find((x) => x[3]) ?? matches.find((x) => !AMBIGUOUS_GRADERS.has(x[1]!.toLowerCase()));
165 if (!m) { 173 if (!m) {
166 if (/\b(raw|ungraded)\b/i.test(title)) return { grader: 'raw', grade: null, qualifier: null }; 174 if (/\b(raw|ungraded)\b/i.test(title)) return { grader: 'raw', grade: null, qualifier: null };
167 return { grader: null, grade: null, qualifier: null }; 175 return { grader: null, grade: null, qualifier: null };
168 } 176 }
169 const grader = getGrader(m[1]!)?.slug ?? m[1]!.toLowerCase(); 177 const grader = getGrader(m[1]!)?.slug ?? m[1]!.toLowerCase();
170− let grade = m[2] ?? null; 178+ let grade = m[2] ?? (m[3] ? AUTHENTIC_GRADE : null);
171 const g = getGrader(grader); 179 const g = getGrader(grader);
172− if (grade && g && g.scale.values.length && !g.scale.values.includes(grade)) { 180+ if (grade && grade !== AUTHENTIC_GRADE && g && g.scale.values.length && !g.scale.values.includes(grade)) {
173 // e.g. "PSA 10" where 10 is fine; "PSA 2021" is a year → reject 181 // e.g. "PSA 10" where 10 is fine; "PSA 2021" is a year → reject
174 if (Number(grade) > Number(g.scale.top)) grade = null; 182 if (Number(grade) > Number(g.scale.top)) grade = null;
175 } 183 }
@@ -187,11 +195,15 @@ export function variantKey(input: { grader?: string | null; grade?: string | nul
187 const labelParts: string[] = []; 195 const labelParts: string[] = [];
188 if (input.grader && input.grader !== 'raw' && input.grade) { 196 if (input.grader && input.grader !== 'raw' && input.grade) {
189 parts.push(input.grader, input.grade); 197 parts.push(input.grader, input.grade);
190− labelParts.push(`${getGrader(input.grader)?.name ?? input.grader.toUpperCase()} ${input.grade}`); 198+ labelParts.push(`${getGrader(input.grader)?.name ?? input.grader.toUpperCase()} ${input.grade === AUTHENTIC_GRADE ? 'Authentic' : input.grade}`);
191 if (input.qualifier) { 199 if (input.qualifier) {
192 parts.push(input.qualifier.toLowerCase().replace(/\s+/g, '_')); 200 parts.push(input.qualifier.toLowerCase().replace(/\s+/g, '_'));
193 labelParts.push(input.qualifier); 201 labelParts.push(input.qualifier);
194 } 202 }
203+ } else if (input.grader && input.grader !== 'raw') {
204+ // Slabbed by a known grader but the grade could not be read: its own variant, never "raw".
205+ parts.push(input.grader, 'unknown');
206+ labelParts.push(`${getGrader(input.grader)?.name ?? input.grader.toUpperCase()} · grade unknown`);
195 } else { 207 } else {
196 parts.push('raw'); 208 parts.push('raw');
197 if (input.completeness) { 209 if (input.completeness) {
modified packages/taxonomy/src/taxonomy.test.ts +12 −0
@@ -25,6 +25,18 @@ describe('taxonomy', () => {
25 expect(parseGradeFromTitle('Super Mario 64 WATA 9.8 A++')).toMatchObject({ grader: 'wata', grade: '9.8' }); 25 expect(parseGradeFromTitle('Super Mario 64 WATA 9.8 A++')).toMatchObject({ grader: 'wata', grade: '9.8' });
26 expect(parseGradeFromTitle('Rolex Daytona 116500LN')).toMatchObject({ grader: null, grade: null }); 26 expect(parseGradeFromTitle('Rolex Daytona 116500LN')).toMatchObject({ grader: null, grade: null });
27 expect(parseGradeFromTitle('1952 Topps Mantle raw')).toMatchObject({ grader: 'raw' }); 27 expect(parseGradeFromTitle('1952 Topps Mantle raw')).toMatchObject({ grader: 'raw' });
28+ // PSA/DNA autograph slabs and authentication-only slabs (COMC, mycomicshop titles)
29+ expect(parseGradeFromTitle('1986-87 Fleer - [Base] #128 Al Wood [PSA/DNA 3 VG]')).toMatchObject({ grader: 'psa', grade: '3' });
30+ expect(parseGradeFromTitle('1986-87 Fleer - [Base] #33 Vern Fleming [PSA/DNA 8 NM-MT]')).toMatchObject({ grader: 'psa', grade: '8' });
31+ expect(parseGradeFromTitle('1989 Upper Deck - Blank Back #535 Danny Cox [SGC Authentic]')).toMatchObject({ grader: 'sgc', grade: 'authentic' });
32+ expect(parseGradeFromTitle('Brad Davis [PSA/DNA Certified Authentic Card & Auto]')).toMatchObject({ grader: 'psa', grade: 'authentic' });
33+ expect(parseGradeFromTitle('Charizard PSA 2021 release')).toMatchObject({ grader: 'psa', grade: null }); // a year is not a grade
34+ expect(parseGradeFromTitle('Batman #1 CGC (8.5) white pages')).toMatchObject({ grader: 'cgc', grade: '8.5' });
35+ });
36+ it('keeps slabs with an unreadable grade out of the raw variant', () => {
37+ expect(variantKey({ grader: 'psa', grade: null })).toEqual({ key: 'psa|unknown', label: 'PSA · grade unknown' });
38+ expect(variantKey({ grader: 'sgc', grade: 'authentic' })).toEqual({ key: 'sgc|authentic', label: 'SGC Authentic' });
39+ expect(variantKey({ grader: 'raw', grade: null }).key).toBe('raw');
28 }); 40 });
29 it('builds variant keys', () => { 41 it('builds variant keys', () => {
30 expect(variantKey({ grader: 'psa', grade: '10' })).toEqual({ key: 'psa|10', label: 'PSA 10' }); 42 expect(variantKey({ grader: 'psa', grade: '10' })).toEqual({ key: 'psa|10', label: 'PSA 10' });
modified packages/valuation/src/scores.ts +106 −5
@@ -1,4 +1,4 @@
1−import { clamp, pctChange, round } from '@rareindex/shared'; 1+import { clamp, round } from '@rareindex/shared';
2 2
3 /** Liquidity Score 0–100 (§121). Null when there is no evidence at all. */ 3 /** Liquidity Score 0–100 (§121). Null when there is no evidence at all. */
4 export function liquidityScore(input: { salesPerMonth: number; activeListings: number; sources: number; medianDaysBetweenSales: number | null; askSoldSpread: number | null }): number | null { 4 export function liquidityScore(input: { salesPerMonth: number; activeListings: number; sources: number; medianDaysBetweenSales: number | null; askSoldSpread: number | null }): number | null {
@@ -36,10 +36,111 @@ export function momentumScore(input: { priceChange: number | null; volumeNow: nu
36 return round(100 * (0.7 * p + 0.3 * v), 1); 36 return round(100 * (0.7 * p + 0.3 * v), 1);
37 } 37 }
38 38
39−/** Value opportunity (§123): discount of an ask vs RIV; null when RIV confidence is insufficient. */ 39+// ---------- Ask vs RIV: sign convention, gates and the anomaly guard (§83–§84, §174, §196) ----------
40−export function valueOpportunity(askUsd: number | null, riv: number | null, confidence: number | null): number | null { 40+//
41− if (askUsd === null || riv === null || confidence === null || confidence < 0.5 || riv <= 0) return null; 41+// Everywhere in RareIndex `discount_to_riv` / `value_opportunity` = (ask − RIV) / RIV.
42− return round(pctChange(riv, askUsd) ?? 0, 4); // negative = below fair value 42+// negative → the ask is BELOW the valuation (a discount) e.g. −0.167 = 16.7 % below RIV
43+// positive → the ask is ABOVE the valuation (a premium)
44+// A number is only produced when the comparison is legitimate: same variant, transaction-based RIV
45+// with enough evidence, plausible ratio. Anything else is `null` + a verdict, never a "deal".
46+
47+/** An ask below 10 % or above 10× the valuation is almost always a mismatch (variant, lot, currency, typo). */
48+export const ASK_ANOMALY_LOW_RATIO = 0.1;
49+export const ASK_ANOMALY_HIGH_RATIO = 10;
50+/** Minimum valuation quality for an ask to be compared at all. */
51+export const ASK_MIN_CONFIDENCE = 0.5;
52+export const ASK_MIN_SAMPLE = 5;
53+/** Minimum identification confidence of the listing→asset match. */
54+export const ASK_MIN_MATCH_CONFIDENCE = 0.7;
55+/** Threshold below/above which an ask is called a deal / a premium. */
56+export const DEAL_THRESHOLD = -0.1;
57+export const PREMIUM_THRESHOLD = 0.1;
58+
59+export type AskVerdict = 'deal' | 'fair' | 'premium' | 'anomaly' | 'ungated';
60+
61+export interface AskAssessment {
62+ /** (ask − RIV) / RIV, 4 decimals; null when ungated or anomalous */
63+ discount: number | null;
64+ /** raw ratio ask / RIV, kept for diagnostics even when anomalous */
65+ ratio: number | null;
66+ verdict: AskVerdict;
67+ reasons: string[];
68+}
69+
70+export interface AskAssessmentInput {
71+ askUsd: number | null | undefined;
72+ rivUsd: number | null | undefined;
73+ confidence: number | null | undefined;
74+ sampleSize?: number | null;
75+ /** valuation basis; only transaction-based valuations may qualify an ask */
76+ basis?: 'transactions' | 'comps' | 'guide' | 'none' | null;
77+ /** identification confidence of the listing (0–1) */
78+ matchConfidence?: number | null;
79+ /** true when the listing's variant is the one the RIV was computed for */
80+ sameVariant?: boolean;
81+ /** buyer premium / shipping / fees to add to the ask before comparing (USD) */
82+ feesUsd?: number | null;
83+}
84+
85+/** Classify an ask against a valuation. Never returns a discount that failed a gate. */
86+export function assessAsk(i: AskAssessmentInput): AskAssessment {
87+ const reasons: string[] = [];
88+ const ask = i.askUsd ?? null;
89+ const riv = i.rivUsd ?? null;
90+ if (ask === null || !(ask > 0)) reasons.push('no_ask');
91+ if (riv === null || !(riv > 0)) reasons.push('no_riv');
92+ if (reasons.length) return { discount: null, ratio: null, verdict: 'ungated', reasons };
93+ const allIn = ask! + Math.max(0, i.feesUsd ?? 0);
94+ const ratio = allIn / riv!;
95+ if (i.sameVariant === false) reasons.push('variant_mismatch');
96+ if (i.basis && i.basis !== 'transactions') reasons.push(`riv_basis_${i.basis}`);
97+ if ((i.confidence ?? 0) < ASK_MIN_CONFIDENCE) reasons.push('riv_confidence');
98+ if (i.sampleSize !== undefined && i.sampleSize !== null && i.sampleSize < ASK_MIN_SAMPLE) reasons.push('riv_sample');
99+ if (i.matchConfidence !== undefined && i.matchConfidence !== null && i.matchConfidence < ASK_MIN_MATCH_CONFIDENCE) reasons.push('match_confidence');
100+ if (reasons.length) return { discount: null, ratio: round(ratio, 4), verdict: 'ungated', reasons };
101+ if (ratio < ASK_ANOMALY_LOW_RATIO || ratio > ASK_ANOMALY_HIGH_RATIO) {
102+ return { discount: null, ratio: round(ratio, 4), verdict: 'anomaly', reasons: [ratio < 1 ? 'ask_implausibly_low' : 'ask_implausibly_high'] };
103+ }
104+ const discount = round(ratio - 1, 4);
105+ return { discount, ratio: round(ratio, 4), verdict: discount <= DEAL_THRESHOLD ? 'deal' : discount >= PREMIUM_THRESHOLD ? 'premium' : 'fair', reasons };
106+}
107+
108+/**
109+ * Value opportunity (§123): (ask − RIV) / RIV for a gated, plausible comparison; null otherwise.
110+ * negative = below fair value. Thin wrapper over assessAsk kept for existing callers.
111+ */
112+export function valueOpportunity(askUsd: number | null, riv: number | null, confidence: number | null, sampleSize?: number | null): number | null {
113+ const a = assessAsk({ askUsd, rivUsd: riv, confidence, sampleSize: sampleSize ?? undefined });
114+ return a.verdict === 'anomaly' || a.verdict === 'ungated' ? null : a.discount;
115+}
116+
117+/**
118+ * Deal Score 0–100 (§83): how actionable a below-RIV ask is once valuation quality, match quality
119+ * and liquidity are considered. 0 for anything that is not a gated discount. Components are
120+ * multiplicative so one weak leg (e.g. an illiquid asset) cannot be compensated by a huge discount.
121+ */
122+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;
124+ const depth = clamp(-input.discount / 0.5, 0, 1) ** 0.7; // 50 % below → 1, concave so 10 % is already meaningful
125+ const conf = 0.4 + 0.6 * clamp(input.confidence ?? 0, 0, 1);
126+ const sample = 0.5 + 0.5 * clamp(Math.log10(1 + (input.sampleSize ?? 0)) / Math.log10(41), 0, 1); // 40 sales → 1
127+ const liq = 0.5 + 0.5 * clamp((input.liquidity ?? 30) / 100, 0, 1);
128+ const match = clamp(input.matchConfidence ?? 0.8, 0, 1);
129+ return round(100 * depth * conf * sample * liq * match, 1);
130+}
131+
132+/**
133+ * Plausibility gate for displayed percentage moves (§143, §196): anything beyond ±500 % on a
134+ * valuation change, or beyond the anomaly ratios on an ask, is a data/identity anomaly, not a signal.
135+ */
136+export const MAX_PLAUSIBLE_CHANGE = 5;
137+export function isPlausibleChange(value: number | null | undefined): boolean {
138+ return value !== null && value !== undefined && Number.isFinite(value) && Math.abs(value) <= MAX_PLAUSIBLE_CHANGE;
139+}
140+export function isPlausibleDiscount(value: number | null | undefined): boolean {
141+ if (value === null || value === undefined || !Number.isFinite(value)) return false;
142+ const ratio = 1 + value;
143+ return ratio >= ASK_ANOMALY_LOW_RATIO && ratio <= ASK_ANOMALY_HIGH_RATIO;
43 } 144 }
44 145
45 /** Trending (§152): geometric blend of normalised momenta (0..1 each) → 0–100. */ 146 /** Trending (§152): geometric blend of normalised momenta (0..1 each) → 0–100. */
modified packages/valuation/src/valuation.test.ts +38 −1
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
2 import { computeValuation } from './valuation.js'; 2 import { computeValuation } from './valuation.js';
3 import { detectOutliers } from './outliers.js'; 3 import { detectOutliers } from './outliers.js';
4 import { adjustmentFactor, computeGradePremiums } from './premiums.js'; 4 import { adjustmentFactor, computeGradePremiums } from './premiums.js';
5−import { liquidityScore, rarityScore, momentumScore, valueOpportunity, trendingScore } from './scores.js'; 5+import { assessAsk, dealScore, isPlausibleChange, liquidityScore, rarityScore, momentumScore, valueOpportunity, trendingScore } from './scores.js';
6 6
7 const now = new Date('2026-09-01T00:00:00Z'); 7 const now = new Date('2026-09-01T00:00:00Z');
8 const d = (daysAgo: number) => new Date(now.getTime() - daysAgo * 86_400_000); 8 const d = (daysAgo: number) => new Date(now.getTime() - daysAgo * 86_400_000);
@@ -89,6 +89,43 @@ describe('scores', () => {
89 expect(momentumScore({ priceChange: 0.25, volumeNow: 10, volumePrev: 5 })!).toBeGreaterThan(0); 89 expect(momentumScore({ priceChange: 0.25, volumeNow: 10, volumePrev: 5 })!).toBeGreaterThan(0);
90 expect(valueOpportunity(2950, 3850, 0.91)).toBeCloseTo(-0.2338, 3); 90 expect(valueOpportunity(2950, 3850, 0.91)).toBeCloseTo(-0.2338, 3);
91 expect(valueOpportunity(2950, 3850, 0.3)).toBeNull(); 91 expect(valueOpportunity(2950, 3850, 0.3)).toBeNull();
92+ expect(valueOpportunity(2950, 3850, 0.91, 2)).toBeNull(); // thin sample never qualifies an ask
92 expect(trendingScore({ priceMomentum: 50, volumeMomentum: 30, searchMomentum: null, listingMomentum: 10, newsMomentum: null })!).toBeGreaterThan(50); 93 expect(trendingScore({ priceMomentum: 50, volumeMomentum: 30, searchMomentum: null, listingMomentum: 10, newsMomentum: null })!).toBeGreaterThan(50);
93 }); 94 });
94 }); 95 });
96+
97+describe('ask vs RIV gate (§83–§84, §196)', () => {
98+ it('signs a discount negative and classifies deal / fair / premium', () => {
99+ expect(assessAsk({ askUsd: 2650, rivUsd: 3180, confidence: 0.8, sampleSize: 42, basis: 'transactions' })).toMatchObject({ verdict: 'deal', discount: -0.1667 });
100+ expect(assessAsk({ askUsd: 3200, rivUsd: 3180, confidence: 0.8, sampleSize: 42 }).verdict).toBe('fair');
101+ expect(assessAsk({ askUsd: 4000, rivUsd: 3180, confidence: 0.8, sampleSize: 42 }).verdict).toBe('premium');
102+ });
103+ it('never turns the 1986 Fleer case into a deal: a $472 slab against a $5.48 raw RIV is an anomaly', () => {
104+ const a = assessAsk({ askUsd: 472.2, rivUsd: 5.48, confidence: 0.78, sampleSize: 18, basis: 'transactions' });
105+ expect(a.verdict).toBe('anomaly');
106+ expect(a.discount).toBeNull();
107+ expect(a.ratio).toBeCloseTo(86.17, 1);
108+ expect(assessAsk({ askUsd: 0.4, rivUsd: 5.48, confidence: 0.78, sampleSize: 18 }).verdict).toBe('anomaly'); // implausibly low
109+ });
110+ it('refuses comps/guide valuations, thin samples, low confidence and variant mismatches', () => {
111+ expect(assessAsk({ askUsd: 100, rivUsd: 120, confidence: 0.9, sampleSize: 20, basis: 'comps' })).toMatchObject({ verdict: 'ungated', reasons: ['riv_basis_comps'] });
112+ expect(assessAsk({ askUsd: 100, rivUsd: 120, confidence: 0.9, sampleSize: 3 }).reasons).toContain('riv_sample');
113+ expect(assessAsk({ askUsd: 100, rivUsd: 120, confidence: 0.45, sampleSize: 30 }).reasons).toContain('riv_confidence');
114+ expect(assessAsk({ askUsd: 100, rivUsd: 120, confidence: 0.9, sampleSize: 30, sameVariant: false }).reasons).toContain('variant_mismatch');
115+ expect(assessAsk({ askUsd: 100, rivUsd: 120, confidence: 0.9, sampleSize: 30, matchConfidence: 0.5 }).reasons).toContain('match_confidence');
116+ });
117+ it('adds fees before comparing', () => {
118+ expect(assessAsk({ askUsd: 900, rivUsd: 1000, confidence: 0.9, sampleSize: 30, feesUsd: 150 }).verdict).toBe('fair');
119+ expect(assessAsk({ askUsd: 900, rivUsd: 1000, confidence: 0.9, sampleSize: 30 }).verdict).toBe('deal');
120+ });
121+ it('deal score is 0 without a gated discount and rewards confidence, depth and liquidity', () => {
122+ expect(dealScore({ discount: null, confidence: 0.9, sampleSize: 30, liquidity: 70 })).toBe(0);
123+ expect(dealScore({ discount: 0.2, confidence: 0.9, sampleSize: 30, liquidity: 70 })).toBe(0);
124+ const strong = dealScore({ discount: -0.3, confidence: 0.9, sampleSize: 40, liquidity: 80 });
125+ const weak = dealScore({ discount: -0.3, confidence: 0.55, sampleSize: 6, liquidity: 10 });
126+ expect(strong).toBeGreaterThan(weak);
127+ expect(strong).toBeLessThanOrEqual(100);
128+ expect(isPlausibleChange(85.17)).toBe(false);
129+ expect(isPlausibleChange(0.42)).toBe(true);
130+ });
131+});
modified pnpm-lock.yaml +3 −0
@@ -93,6 +93,9 @@ importers:
93 '@rareindex/taxonomy': 93 '@rareindex/taxonomy':
94 specifier: workspace:* 94 specifier: workspace:*
95 version: link:../../packages/taxonomy 95 version: link:../../packages/taxonomy
96+ '@rareindex/valuation':
97+ specifier: workspace:*
98+ version: link:../../packages/valuation
96 '@tanstack/react-query': 99 '@tanstack/react-query':
97 specifier: ^5.90.0 100 specifier: ^5.90.0
98 version: 5.102.8(react@19.2.8) 101 version: 5.102.8(react@19.2.8)
modified workers/cli.ts +20 −7
@@ -2,10 +2,10 @@
2 /** 2 /**
3 * RareIndex operator CLI. 3 * RareIndex operator CLI.
4 * ri connectors | ri crawl <id> [--mode probe|incremental|backfill] [--limit N] 4 * ri connectors | ri crawl <id> [--mode probe|incremental|backfill] [--limit N]
5− * ri backfill <id> [--reset] [--pause] [--status] [--start YYYY-MM-DD] [--end YYYY-MM-DD] 5+ * ri backfill <id…> [--start-only] [--reset] [--pause] [--status] [--start YYYY-MM-DD] [--end YYYY-MM-DD]
6 * ri certs [--limit N] [--grader psa] | ri certs --backfill (verify cert numbers via cert_lookup connectors / register certs from existing sales+listings) 6 * ri certs [--limit N] [--grader psa] | ri certs --backfill (verify cert numbers via cert_lookup connectors / register certs from existing sales+listings)
7 * ri normalize [--connector id] [--limit N] | ri resolve [--limit N] 7 * ri normalize [--connector id] [--limit N] | ri resolve [--limit N]
8− * ri value [--asset id | --all] [--history] | ri premiums [--category slug] 8+ * ri value [--asset id | --all] [--history] | ri premiums [--category slug] | ri regrade [--kind sale|listing|both] [--dry-run]
9 * ri index | ri snapshots | ri radar | ri health [--probe] | ri fx [--backfill] | ri benchmarks 9 * ri index | ri snapshots | ri radar | ri health [--probe] | ri fx [--backfill] | ri benchmarks
10 * ri expire | ri stats | ri run-all [--limit N] | ri worker 10 * ri expire | ri stats | ri run-all [--limit N] | ri worker
11 */ 11 */
@@ -21,6 +21,7 @@ import { flushCosts } from './lib/costs.ts';
21 import { activeBackfill, pauseBackfill, runCrawl, startBackfill } from './crawler/run.ts'; 21 import { activeBackfill, pauseBackfill, runCrawl, startBackfill } from './crawler/run.ts';
22 import { normalizeBatch } from './normalizer/index.ts'; 22 import { normalizeBatch } from './normalizer/index.ts';
23 import { pendingCount, resolveBatch } from './entity-resolution/index.ts'; 23 import { pendingCount, resolveBatch } from './entity-resolution/index.ts';
24+import { regradeRecords } from './entity-resolution/regrade.ts';
24 import { assetsNeedingValuation, computePremiums, valueAsset, valueMany } from './valuation/run.ts'; 25 import { assetsNeedingValuation, computePremiums, valueAsset, valueMany } from './valuation/run.ts';
25 import { runCategorySnapshots, runIndices, runRadar } from './indices/run.ts'; 26 import { runCategorySnapshots, runIndices, runRadar } from './indices/run.ts';
26 import { syncFx } from './fx.ts'; 27 import { syncFx } from './fx.ts';
@@ -96,10 +97,16 @@ async function main() {
96 print({ paused: await pauseBackfill(id) }); 97 print({ paused: await pauseBackfill(id) });
97 break; 98 break;
98 } 99 }
99− const campaign = await startBackfill(id, { reset: Boolean(flags.reset), startDate: str('start') ?? null, endDate: str('end') ?? null }); 100+ const ids = positional.length > 1 ? positional : [id];
100− print({ campaign: campaign.id, resumedFrom: campaign.lastCursor }); 101+ for (const cid of ids) {
101− print(await runCrawl(id, { mode: 'backfill', limit: num('limit'), trigger: 'manual' })); 102+ const campaign = await startBackfill(cid, { reset: Boolean(flags.reset), startDate: str('start') ?? null, endDate: str('end') ?? null });
102− print(await activeBackfill(id)); 103+ print({ connector: cid, campaign: campaign.id, status: campaign.status, resumedFrom: campaign.lastCursor });
104+ // --start-only: register the campaign and let the worker scheduler run the slices (recommended for many sources)
105+ if (!flags['start-only']) {
106+ print(await runCrawl(cid, { mode: 'backfill', limit: num('limit'), trigger: 'manual' }));
107+ print(await activeBackfill(cid));
108+ }
109+ }
103 break; 110 break;
104 } 111 }
105 case 'certs': 112 case 'certs':
@@ -117,6 +124,12 @@ async function main() {
117 case 'premiums': 124 case 'premiums':
118 print({ written: await computePremiums(str('category')) }); 125 print({ written: await computePremiums(str('category')) });
119 break; 126 break;
127+ case 'regrade': {
128+ // ri regrade [--kind sale|listing|both] [--connector id] [--limit N] [--dry-run]
129+ const kind = (str('kind') ?? 'both') as 'sale' | 'listing' | 'both';
130+ print(await regradeRecords({ kind, connectorId: str('connector'), limit: num('limit', 50_000), dryRun: Boolean(flags['dry-run']) }));
131+ break;
132+ }
120 case 'value': { 133 case 'value': {
121 const asset = str('asset'); 134 const asset = str('asset');
122 if (asset) print(await valueAsset(asset, { rebuildHistory: Boolean(flags.history) })); 135 if (asset) print(await valueAsset(asset, { rebuildHistory: Boolean(flags.history) }));
@@ -190,7 +203,7 @@ async function main() {
190 return; // keep running 203 return; // keep running
191 } 204 }
192 default: 205 default:
193− print(`RareIndex CLI\n ri connectors | crawl <id> [--mode probe|incremental|backfill] [--limit N] | normalize [--connector id] | resolve\n ri value [--asset id|--all] [--history] | premiums | index | snapshots | radar | health [--probe] | fx [--backfill] | benchmarks | expire\n ri stats | run-all [--limit N] [--history] | worker`); 206+ print(`RareIndex CLI\n ri connectors | crawl <id> [--mode probe|incremental|backfill] [--limit N] | normalize [--connector id] | resolve\n ri value [--asset id|--all] [--history] | premiums | regrade [--dry-run] | index | snapshots | radar | health [--probe] | fx [--backfill] | benchmarks | expire\n ri stats | run-all [--limit N] [--history] | worker`);
194 } 207 }
195 await flushCosts(); 208 await flushCosts();
196 await closeDb(); 209 await closeDb();
added workers/entity-resolution/regrade.ts +107 −0
@@ -0,0 +1,107 @@
1+/**
2+ * One-off / periodic repair (§84, §176): sales and listings whose grader was read but whose grade was
3+ * not (or that carry a grader token in the title the connector missed) were attached to the RAW
4+ * variant of their asset, polluting raw valuations and making graded asks look 80× "above RIV".
5+ *
6+ * `regradeRecords` re-parses the raw title with the current parser, moves the row to the proper
7+ * variant (creating it if needed; "grader · grade unknown" when the grade still cannot be read) and
8+ * marks the touched assets for revaluation by bumping `updated_at`/`created_at` watermarks the
9+ * valuation scheduler already watches. Nothing is deleted; every move is audited.
10+ */
11+import { and, eq, inArray, isNotNull, isNull, or, sql } from 'drizzle-orm';
12+import { assetVariants, listings, sales } from '@rareindex/database';
13+import { parseGradeFromTitle } from '@rareindex/taxonomy';
14+import { logger } from '@rareindex/shared';
15+import { db } from '../lib/db.ts';
16+import { auditMany } from '../lib/audit.ts';
17+import { ensureVariant } from './resolver.ts';
18+
19+const log = logger.child({ component: 'regrade' });
20+
21+export interface RegradeResult {
22+ scanned: number;
23+ moved: number;
24+ unknownGrade: number;
25+ unchanged: number;
26+ assets: number;
27+}
28+
29+interface Row {
30+ id: string;
31+ assetId: string;
32+ variantId: string | null;
33+ grader: string | null;
34+ grade: string | null;
35+ rawTitle: string;
36+ variantKey: string | null;
37+ vGrader: string | null;
38+ vGrade: string | null;
39+ condition: string | null;
40+ completeness: string | null;
41+ sizeLabel: string | null;
42+}
43+
44+async function candidates(kind: 'sale' | 'listing', limit: number, connectorId?: string): Promise<Row[]> {
45+ const t = kind === 'sale' ? sales : listings;
46+ const where = and(
47+ connectorId ? eq(t.connectorId, connectorId) : undefined,
48+ kind === 'listing' ? eq(listings.availability, 'available') : undefined,
49+ or(
50+ // grader known, grade unreadable → currently in the raw variant
51+ and(isNotNull(t.grader), isNull(t.grade)),
52+ // no grader recorded but the title carries a grader token (heuristic prefilter; parser decides)
53+ and(isNull(t.grader), sql`${t.rawTitle} ~* '\\m(PSA|BGS|BECKETT|CGC|SGC|CBCS|PGX|WATA|VGA|PCGS|NGC|ANACS|ICCS|PMG)\\M'`),
54+ ),
55+ );
56+ const rows = await db()
57+ .select({ id: t.id, assetId: t.assetId, variantId: t.variantId, grader: t.grader, grade: t.grade, rawTitle: t.rawTitle, variantKey: assetVariants.variantKey, vGrader: assetVariants.grader, vGrade: assetVariants.grade, condition: assetVariants.condition, completeness: assetVariants.completeness, sizeLabel: assetVariants.sizeLabel })
58+ .from(t)
59+ .leftJoin(assetVariants, eq(assetVariants.id, t.variantId))
60+ .where(where)
61+ .limit(limit);
62+ return rows as Row[];
63+}
64+
65+export async function regradeRecords(opts: { kind?: 'sale' | 'listing' | 'both'; limit?: number; connectorId?: string; dryRun?: boolean } = {}): Promise<RegradeResult> {
66+ const kinds: Array<'sale' | 'listing'> = opts.kind === 'sale' ? ['sale'] : opts.kind === 'listing' ? ['listing'] : ['sale', 'listing'];
67+ const limit = opts.limit ?? 50_000;
68+ const res: RegradeResult = { scanned: 0, moved: 0, unknownGrade: 0, unchanged: 0, assets: 0 };
69+ const touched = new Set<string>();
70+ for (const kind of kinds) {
71+ const rows = await candidates(kind, limit, opts.connectorId);
72+ res.scanned += rows.length;
73+ const audits: Array<{ entityType: string; entityId: string; action: string; reason: string; details: Record<string, unknown> }> = [];
74+ for (const r of rows) {
75+ const parsed = parseGradeFromTitle(r.rawTitle);
76+ const grader = r.grader && r.grader !== 'raw' ? r.grader : parsed.grader && parsed.grader !== 'raw' ? parsed.grader : null;
77+ if (!grader) {
78+ res.unchanged++;
79+ continue;
80+ }
81+ const grade = r.grade ?? (parsed.grader === grader ? parsed.grade : null);
82+ const target = await ensureVariant(r.assetId, { grader, grade, qualifier: parsed.qualifier, certificationNumber: null }, { condition: r.condition, completeness: r.completeness }, r.sizeLabel);
83+ if (target.id === r.variantId && r.grader === grader && r.grade === grade) {
84+ res.unchanged++;
85+ continue;
86+ }
87+ if (!grade) res.unknownGrade++;
88+ res.moved++;
89+ touched.add(r.assetId);
90+ if (opts.dryRun) continue;
91+ const t = kind === 'sale' ? sales : listings;
92+ await db().update(t).set({ variantId: target.id, grader, grade, ...(kind === 'listing' ? { discountToRiv: null } : {}) }).where(eq(t.id, r.id));
93+ audits.push({ entityType: kind, entityId: r.id, action: 'regraded', reason: 'grade_reparsed_from_title', details: { from: r.variantKey, to: target.key, grader, grade, title: r.rawTitle } });
94+ }
95+ if (audits.length) await auditMany(audits);
96+ log.info({ kind, scanned: rows.length, moved: res.moved, dryRun: Boolean(opts.dryRun) }, 'regrade pass');
97+ }
98+ res.assets = touched.size;
99+ if (!opts.dryRun && touched.size) {
100+ // Make the valuation scheduler pick these assets up (it watches max(listings.updated_at) / sales.created_at).
101+ const ids = [...touched];
102+ for (let i = 0; i < ids.length; i += 1000) await db().update(listings).set({ updatedAt: new Date() }).where(and(inArray(listings.assetId, ids.slice(i, i + 1000)), eq(listings.availability, 'available')));
103+ // assets without an available listing: touch their stats watermark backwards so they re-qualify
104+ for (let i = 0; i < ids.length; i += 1000) await db().execute(sql`update asset_stats set updated_at = '1970-01-01' where asset_id in ${ids.slice(i, i + 1000)}`);
105+ }
106+ return res;
107+}
modified workers/entity-resolution/writers.ts +37 −17
@@ -1,6 +1,7 @@
1 import { and, eq, sql } from 'drizzle-orm'; 1 import { and, eq, sql } from 'drizzle-orm';
2 import { auctionLots, auctions, images, listingEvents, listings, news, populationReports, priceObservations, sales, sources } from '@rareindex/database'; 2 import { auctionLots, auctions, images, listingEvents, listings, news, populationReports, priceObservations, sales, sources } from '@rareindex/database';
3−import { normalizeCondition } from '@rareindex/taxonomy'; 3+import { normalizeCondition, parseGradeFromTitle } from '@rareindex/taxonomy';
4+import type { Grade } from '@rareindex/shared';
4 import { newId, sha256, toDateOnly, type NormalizedAuctionLot, type NormalizedCatalogItem, type NormalizedListing, type NormalizedNewsItem, type NormalizedPopulationReport, type NormalizedPriceObservation, type NormalizedRecord, type NormalizedSale } from '@rareindex/shared'; 5 import { newId, sha256, toDateOnly, type NormalizedAuctionLot, type NormalizedCatalogItem, type NormalizedListing, type NormalizedNewsItem, type NormalizedPopulationReport, type NormalizedPriceObservation, type NormalizedRecord, type NormalizedSale } from '@rareindex/shared';
5 import { db } from '../lib/db.ts'; 6 import { db } from '../lib/db.ts';
6 import { toUsd } from '../lib/fx.ts'; 7 import { toUsd } from '../lib/fx.ts';
@@ -9,10 +10,11 @@ import { enrichAttributesFromTitle, ensureVariant, resolveAsset, type Resolution
9 import { recordCertificate } from './certificates.ts'; 10 import { recordCertificate } from './certificates.ts';
10 11
11 /** Record a slab certification sighting when a record carries one (SPEC §22); never throws into the writer. */ 12 /** Record a slab certification sighting when a record carries one (SPEC §22); never throws into the writer. */
12−async function trackCert(rec: { grade: { grader: string | null; grade: string | null; qualifier: string | null; certificationNumber: string | null }; sourceId: string; connectorId: string; sourceUrl: string }, kind: 'sale' | 'listing' | 'auction_lot', targetId: string | null, assetId: string | null, variantId: string | null, priceUsd: number | null, price: number | null, currency: string | null, observedAt: Date): Promise<void> { 13+async function trackCert(rec: { grade: { grader: string | null; grade: string | null; qualifier: string | null; certificationNumber: string | null }; sourceId: string; connectorId: string; sourceUrl: string; rawTitle?: string | null }, kind: 'sale' | 'listing' | 'auction_lot', targetId: string | null, assetId: string | null, variantId: string | null, priceUsd: number | null, price: number | null, currency: string | null, observedAt: Date): Promise<void> {
13− if (!rec.grade.certificationNumber || !rec.grade.grader) return; 14+ const g = refineGrade(rec.grade, rec.rawTitle);
15+ if (!g.certificationNumber || !g.grader) return;
14 try { 16 try {
15− await recordCertificate({ grader: rec.grade.grader, certNumber: rec.grade.certificationNumber, assetId, variantId, grade: rec.grade.grade, qualifier: rec.grade.qualifier, kind, targetId, sourceId: rec.sourceId, connectorId: rec.connectorId, sourceUrl: rec.sourceUrl, priceUsd, price, currency, observedAt }); 17+ await recordCertificate({ grader: g.grader, certNumber: g.certificationNumber, assetId, variantId, grade: g.grade, qualifier: g.qualifier, kind, targetId, sourceId: rec.sourceId, connectorId: rec.connectorId, sourceUrl: rec.sourceUrl, priceUsd, price, currency, observedAt });
16 } catch { 18 } catch {
17 /* cert tracking is best-effort; the canonical write already succeeded */ 19 /* cert tracking is best-effort; the canonical write already succeeded */
18 } 20 }
@@ -82,6 +84,20 @@ export async function applyRecord(rec: NormalizedRecord): Promise<ApplyResult> {
82 } 84 }
83 } 85 }
84 86
87+/**
88+ * Grade refinement (§18, §84): when a connector did not read the slab (or read the grader but not the
89+ * grade), parse the raw title. A grader with no readable grade stays "grader · grade unknown" — it must
90+ * never fall into the raw/ungraded variant, which is what compared PSA slabs against raw valuations.
91+ * An explicit `raw` from the connector is trusted over the title.
92+ */
93+export function refineGrade(grade: Grade, rawTitle: string | null | undefined): Grade {
94+ if (!rawTitle || grade.grader === 'raw' || (grade.grader && grade.grade)) return grade;
95+ const parsed = parseGradeFromTitle(rawTitle);
96+ if (!parsed.grader || parsed.grader === 'raw') return grade;
97+ if (grade.grader && parsed.grader !== grade.grader) return grade; // never contradict the connector's grader
98+ return { ...grade, grader: grade.grader ?? parsed.grader, grade: grade.grade ?? parsed.grade, qualifier: grade.qualifier ?? parsed.qualifier };
99+}
100+
85 async function applyCatalog(rec: NormalizedCatalogItem): Promise<ApplyResult> { 101 async function applyCatalog(rec: NormalizedCatalogItem): Promise<ApplyResult> {
86 const r = await resolveAsset(rec.attributes, { enrich: true, imageUrls: rec.imageUrls, description: rec.description }); 102 const r = await resolveAsset(rec.attributes, { enrich: true, imageUrls: rec.imageUrls, description: rec.description });
87 if (!r) return { assetId: null, variantId: null, targetId: null, method: null, confidence: null, status: 'unmatched' }; 103 if (!r) return { assetId: null, variantId: null, targetId: null, method: null, confidence: null, status: 'unmatched' };
@@ -90,10 +106,11 @@ async function applyCatalog(rec: NormalizedCatalogItem): Promise<ApplyResult> {
90 } 106 }
91 107
92 async function applySale(rec: NormalizedSale): Promise<ApplyResult> { 108 async function applySale(rec: NormalizedSale): Promise<ApplyResult> {
109+ const grade = refineGrade(rec.grade, rec.rawTitle);
93 const r = await resolveAsset(enrichAttributesFromTitle(rec.attributes, rec.rawTitle), { imageUrls: rec.imageUrls }); 110 const r = await resolveAsset(enrichAttributesFromTitle(rec.attributes, rec.rawTitle), { imageUrls: rec.imageUrls });
94 if (!r) return { assetId: null, variantId: null, targetId: null, method: null, confidence: null, status: 'unmatched' }; 111 if (!r) return { assetId: null, variantId: null, targetId: null, method: null, confidence: null, status: 'unmatched' };
95 const condition = normCond(rec.attributes.categorySlug, rec.condition.conditionRaw, rec.condition.condition); 112 const condition = normCond(rec.attributes.categorySlug, rec.condition.conditionRaw, rec.condition.condition);
96− const variant = await ensureVariant(r.assetId, rec.grade, { condition, completeness: rec.condition.completeness }, rec.attributes.size); 113+ const variant = await ensureVariant(r.assetId, grade, { condition, completeness: rec.condition.completeness }, rec.attributes.size);
97 const fx = await toUsd(rec.price, rec.currency, rec.saleDate); 114 const fx = await toUsd(rec.price, rec.currency, rec.saleDate);
98 if (!fx) throw new FxMissingError(rec.currency, rec.saleDate); 115 if (!fx) throw new FxMissingError(rec.currency, rec.saleDate);
99 const dedupeKey = sha256(['sale', rec.sourceId, rec.externalId ?? rec.sourceUrl, toDateOnly(rec.saleDate), rec.price.toFixed(2)].join('|')); 116 const dedupeKey = sha256(['sale', rec.sourceId, rec.externalId ?? rec.sourceUrl, toDateOnly(rec.saleDate), rec.price.toFixed(2)].join('|'));
@@ -126,16 +143,16 @@ async function applySale(rec: NormalizedSale): Promise<ApplyResult> {
126 quantity: rec.quantity, 143 quantity: rec.quantity,
127 isBundle: rec.isBundle, 144 isBundle: rec.isBundle,
128 condition, 145 condition,
129− grader: rec.grade.grader, 146+ grader: grade.grader,
130− grade: rec.grade.grade, 147+ grade: grade.grade,
131− certificationNumber: rec.grade.certificationNumber, 148+ certificationNumber: grade.certificationNumber,
132 location: rec.location, 149 location: rec.location,
133 auctionHouse: rec.auctionHouse, 150 auctionHouse: rec.auctionHouse,
134 lotNumber: rec.lotNumber, 151 lotNumber: rec.lotNumber,
135 imageUrls: rec.imageUrls, 152 imageUrls: rec.imageUrls,
136 rawTitle: rec.rawTitle, 153 rawTitle: rec.rawTitle,
137 confidence, 154 confidence,
138− dataQuality: dataQuality({ date: rec.saleDate, price: rec.price, currency: rec.currency, images: rec.imageUrls, grade: rec.grade.grade, condition, externalId: rec.externalId, type: rec.saleType === 'unknown' ? null : rec.saleType }, trust, confidence), 155+ dataQuality: dataQuality({ date: rec.saleDate, price: rec.price, currency: rec.currency, images: rec.imageUrls, grade: grade.grade, condition, externalId: rec.externalId, type: rec.saleType === 'unknown' ? null : rec.saleType }, trust, confidence),
139 status, 156 status,
140 flags, 157 flags,
141 dedupeKey, 158 dedupeKey,
@@ -150,10 +167,11 @@ async function applySale(rec: NormalizedSale): Promise<ApplyResult> {
150 } 167 }
151 168
152 async function applyListing(rec: NormalizedListing): Promise<ApplyResult> { 169 async function applyListing(rec: NormalizedListing): Promise<ApplyResult> {
170+ const grade = refineGrade(rec.grade, rec.rawTitle);
153 const r = await resolveAsset(enrichAttributesFromTitle(rec.attributes, rec.rawTitle), { imageUrls: rec.imageUrls }); 171 const r = await resolveAsset(enrichAttributesFromTitle(rec.attributes, rec.rawTitle), { imageUrls: rec.imageUrls });
154 if (!r) return { assetId: null, variantId: null, targetId: null, method: null, confidence: null, status: 'unmatched' }; 172 if (!r) return { assetId: null, variantId: null, targetId: null, method: null, confidence: null, status: 'unmatched' };
155 const condition = normCond(rec.attributes.categorySlug, rec.condition.conditionRaw, rec.condition.condition); 173 const condition = normCond(rec.attributes.categorySlug, rec.condition.conditionRaw, rec.condition.condition);
156− const variant = await ensureVariant(r.assetId, rec.grade, { condition, completeness: rec.condition.completeness }, rec.attributes.size); 174+ const variant = await ensureVariant(r.assetId, grade, { condition, completeness: rec.condition.completeness }, rec.attributes.size);
157 let priceUsd: number | null = null; 175 let priceUsd: number | null = null;
158 if (rec.price !== null && rec.currency) { 176 if (rec.price !== null && rec.currency) {
159 const fx = await toUsd(rec.price, rec.currency, rec.observedAt); 177 const fx = await toUsd(rec.price, rec.currency, rec.observedAt);
@@ -165,7 +183,7 @@ async function applyListing(rec: NormalizedListing): Promise<ApplyResult> {
165 const confidence = Math.min(rec.confidence, r.confidence); 183 const confidence = Math.min(rec.confidence, r.confidence);
166 const now = rec.observedAt; 184 const now = rec.observedAt;
167 const [existing] = await db().select({ id: listings.id, price: listings.price, availability: listings.availability }).from(listings).where(and(eq(listings.sourceId, rec.sourceId), eq(listings.externalId, externalId))).limit(1); 185 const [existing] = await db().select({ id: listings.id, price: listings.price, availability: listings.availability }).from(listings).where(and(eq(listings.sourceId, rec.sourceId), eq(listings.externalId, externalId))).limit(1);
168− const quality = dataQuality({ price: rec.price, currency: rec.currency, images: rec.imageUrls, seller: rec.seller, condition, grade: rec.grade.grade, listedAt: rec.listedAt, description: rec.description }, trust, confidence); 186+ const quality = dataQuality({ price: rec.price, currency: rec.currency, images: rec.imageUrls, seller: rec.seller, condition, grade: grade.grade, listedAt: rec.listedAt, description: rec.description }, trust, confidence);
169 if (!existing) { 187 if (!existing) {
170 const id = newId('listing'); 188 const id = newId('listing');
171 await db().insert(listings).values({ 189 await db().insert(listings).values({
@@ -186,9 +204,9 @@ async function applyListing(rec: NormalizedListing): Promise<ApplyResult> {
186 shippingCost: rec.shippingCost, 204 shippingCost: rec.shippingCost,
187 quantity: rec.quantity, 205 quantity: rec.quantity,
188 condition, 206 condition,
189− grader: rec.grade.grader, 207+ grader: grade.grader,
190− grade: rec.grade.grade, 208+ grade: grade.grade,
191− certificationNumber: rec.grade.certificationNumber, 209+ certificationNumber: grade.certificationNumber,
192 imageUrls: rec.imageUrls, 210 imageUrls: rec.imageUrls,
193 rawTitle: rec.rawTitle, 211 rawTitle: rec.rawTitle,
194 description: rec.description, 212 description: rec.description,
@@ -232,10 +250,11 @@ async function applyListing(rec: NormalizedListing): Promise<ApplyResult> {
232 } 250 }
233 251
234 async function applyObservation(rec: NormalizedPriceObservation): Promise<ApplyResult> { 252 async function applyObservation(rec: NormalizedPriceObservation): Promise<ApplyResult> {
253+ const grade = refineGrade(rec.grade, rec.rawTitle);
235 const r = await resolveAsset(rec.attributes, { imageUrls: rec.imageUrls }); 254 const r = await resolveAsset(rec.attributes, { imageUrls: rec.imageUrls });
236 if (!r) return { assetId: null, variantId: null, targetId: null, method: null, confidence: null, status: 'unmatched' }; 255 if (!r) return { assetId: null, variantId: null, targetId: null, method: null, confidence: null, status: 'unmatched' };
237 const condition = normCond(rec.attributes.categorySlug, rec.condition.conditionRaw, rec.condition.condition); 256 const condition = normCond(rec.attributes.categorySlug, rec.condition.conditionRaw, rec.condition.condition);
238− const variant = await ensureVariant(r.assetId, rec.grade, { condition, completeness: rec.condition.completeness }, rec.attributes.size); 257+ const variant = await ensureVariant(r.assetId, grade, { condition, completeness: rec.condition.completeness }, rec.attributes.size);
239 const fx = await toUsd(rec.price, rec.currency, rec.observationDate); 258 const fx = await toUsd(rec.price, rec.currency, rec.observationDate);
240 if (!fx) throw new FxMissingError(rec.currency, rec.observationDate); 259 if (!fx) throw new FxMissingError(rec.currency, rec.observationDate);
241 const dedupeKey = sha256(['obs', rec.sourceId, rec.externalId ?? rec.sourceUrl, rec.priceKind, variant.key, toDateOnly(rec.observationDate)].join('|')); 260 const dedupeKey = sha256(['obs', rec.sourceId, rec.externalId ?? rec.sourceUrl, rec.priceKind, variant.key, toDateOnly(rec.observationDate)].join('|'));
@@ -250,6 +269,7 @@ async function applyObservation(rec: NormalizedPriceObservation): Promise<ApplyR
250 } 269 }
251 270
252 async function applyAuctionLot(rec: NormalizedAuctionLot): Promise<ApplyResult> { 271 async function applyAuctionLot(rec: NormalizedAuctionLot): Promise<ApplyResult> {
272+ const grade = refineGrade(rec.grade, rec.rawTitle);
253 const r = await resolveAsset(enrichAttributesFromTitle(rec.attributes, rec.rawTitle), { imageUrls: rec.imageUrls, allowCreate: rec.confidence >= 0.7 }); 273 const r = await resolveAsset(enrichAttributesFromTitle(rec.attributes, rec.rawTitle), { imageUrls: rec.imageUrls, allowCreate: rec.confidence >= 0.7 });
254 const auctionUrl = (rec.attributes.metadata?.auctionUrl as string | undefined) ?? `${new URL(rec.sourceUrl).origin}#${rec.auctionHouse}:${rec.auctionName ?? 'auction'}`; 274 const auctionUrl = (rec.attributes.metadata?.auctionUrl as string | undefined) ?? `${new URL(rec.sourceUrl).origin}#${rec.auctionHouse}:${rec.auctionName ?? 'auction'}`;
255 const auctionId = sha256(auctionUrl).slice(0, 24); 275 const auctionId = sha256(auctionUrl).slice(0, 24);
@@ -258,11 +278,11 @@ async function applyAuctionLot(rec: NormalizedAuctionLot): Promise<ApplyResult>
258 .values({ id: `auc_${auctionId}`, sourceId: rec.sourceId, auctionHouse: rec.auctionHouse, name: rec.auctionName ?? rec.auctionHouse, url: auctionUrl, startsAt: rec.startsAt, endsAt: rec.endsAt, location: rec.location, categorySlugs: [rec.attributes.categorySlug], status: rec.status === 'unknown' ? 'upcoming' : rec.status, currency: rec.currency }) 278 .values({ id: `auc_${auctionId}`, sourceId: rec.sourceId, auctionHouse: rec.auctionHouse, name: rec.auctionName ?? rec.auctionHouse, url: auctionUrl, startsAt: rec.startsAt, endsAt: rec.endsAt, location: rec.location, categorySlugs: [rec.attributes.categorySlug], status: rec.status === 'unknown' ? 'upcoming' : rec.status, currency: rec.currency })
259 .onConflictDoUpdate({ target: auctions.url, set: { endsAt: sql`coalesce(excluded.ends_at, ${auctions.endsAt})`, status: sql`excluded.status`, updatedAt: new Date() } }); 279 .onConflictDoUpdate({ target: auctions.url, set: { endsAt: sql`coalesce(excluded.ends_at, ${auctions.endsAt})`, status: sql`excluded.status`, updatedAt: new Date() } });
260 let variantId: string | null = null; 280 let variantId: string | null = null;
261− if (r) variantId = (await ensureVariant(r.assetId, rec.grade, { condition: normCond(rec.attributes.categorySlug, rec.condition.conditionRaw, rec.condition.condition), completeness: rec.condition.completeness }, rec.attributes.size)).id; 281+ if (r) variantId = (await ensureVariant(r.assetId, grade, { condition: normCond(rec.attributes.categorySlug, rec.condition.conditionRaw, rec.condition.condition), completeness: rec.condition.completeness }, rec.attributes.size)).id;
262 const id = `lot_${sha256(rec.sourceUrl).slice(0, 24)}`; 282 const id = `lot_${sha256(rec.sourceUrl).slice(0, 24)}`;
263 await db() 283 await db()
264 .insert(auctionLots) 284 .insert(auctionLots)
265− .values({ id, auctionId: `auc_${auctionId}`, assetId: r?.assetId ?? null, variantId, sourceId: rec.sourceId, lotNumber: rec.lotNumber, title: rec.rawTitle, url: rec.sourceUrl, estimateLow: rec.estimateLow, estimateHigh: rec.estimateHigh, currentBid: rec.currentBid, currency: rec.currency, startsAt: rec.startsAt, endsAt: rec.endsAt, status: rec.status === 'unknown' ? 'upcoming' : rec.status, imageUrls: rec.imageUrls, grader: rec.grade.grader, grade: rec.grade.grade }) 285+ .values({ id, auctionId: `auc_${auctionId}`, assetId: r?.assetId ?? null, variantId, sourceId: rec.sourceId, lotNumber: rec.lotNumber, title: rec.rawTitle, url: rec.sourceUrl, estimateLow: rec.estimateLow, estimateHigh: rec.estimateHigh, currentBid: rec.currentBid, currency: rec.currency, startsAt: rec.startsAt, endsAt: rec.endsAt, status: rec.status === 'unknown' ? 'upcoming' : rec.status, imageUrls: rec.imageUrls, grader: grade.grader, grade: grade.grade })
266 .onConflictDoUpdate({ target: auctionLots.url, set: { currentBid: sql`excluded.current_bid`, status: sql`excluded.status`, endsAt: sql`coalesce(excluded.ends_at, ${auctionLots.endsAt})`, assetId: sql`coalesce(${auctionLots.assetId}, excluded.asset_id)`, updatedAt: new Date() } }); 286 .onConflictDoUpdate({ target: auctionLots.url, set: { currentBid: sql`excluded.current_bid`, status: sql`excluded.status`, endsAt: sql`coalesce(excluded.ends_at, ${auctionLots.endsAt})`, assetId: sql`coalesce(${auctionLots.assetId}, excluded.asset_id)`, updatedAt: new Date() } });
267 await trackCert(rec, 'auction_lot', id, r?.assetId ?? null, variantId, null, rec.currentBid, rec.currency, rec.observedAt); 287 await trackCert(rec, 'auction_lot', id, r?.assetId ?? null, variantId, null, rec.currentBid, rec.currency, rec.observedAt);
268 return { assetId: r?.assetId ?? null, variantId, targetId: id, method: r?.method ?? null, confidence: r?.confidence ?? null, status: r ? 'applied' : 'unmatched' }; 288 return { assetId: r?.assetId ?? null, variantId, targetId: id, method: r?.method ?? null, confidence: r?.confidence ?? null, status: r ? 'applied' : 'unmatched' };
modified workers/indices/run.ts +7 −4
@@ -91,7 +91,7 @@ export async function runIndices(opts: { now?: Date } = {}): Promise<{ published
91 log.info({ constituents: cons.length }, 'eligible constituents'); 91 log.info({ constituents: cons.length }, 'eligible constituents');
92 const published: string[] = []; 92 const published: string[] = [];
93 const skipped: string[] = []; 93 const skipped: string[] = [];
94− const subSeries: Record<string, Array<{ date: string; value: number; transactions: number }>> = {}; 94+ const subSeries: Record<string, Array<{ date: string; value: number; transactions: number; constituents: number }>> = {};
95 const today = toDateOnly(now); 95 const today = toDateOnly(now);
96 96
97 for (const def of defs.filter((d) => !d.isFlagship)) { 97 for (const def of defs.filter((d) => !d.isFlagship)) {
@@ -120,7 +120,7 @@ export async function runIndices(opts: { now?: Date } = {}): Promise<{ published
120 .values(rows.slice(i, i + 500)) 120 .values(rows.slice(i, i + 500))
121 .onConflictDoUpdate({ target: [indexValues.indexId, indexValues.date], set: { value: sql`excluded.value`, constituentsCount: sql`excluded.constituents_count`, transactions: sql`excluded.transactions`, volumeUsd: sql`excluded.volume_usd`, medianSaleUsd: sql`excluded.median_sale_usd`, avgSaleUsd: sql`excluded.avg_sale_usd`, marketCapEstUsd: sql`excluded.market_cap_est_usd`, marketCapConfidence: sql`excluded.market_cap_confidence`, liquidityScore: sql`excluded.liquidity_score`, momentum: sql`excluded.momentum`, breadth: sql`excluded.breadth`, trackedAssets: sql`excluded.tracked_assets`, coverage: sql`excluded.coverage` } }); 121 .onConflictDoUpdate({ target: [indexValues.indexId, indexValues.date], set: { value: sql`excluded.value`, constituentsCount: sql`excluded.constituents_count`, transactions: sql`excluded.transactions`, volumeUsd: sql`excluded.volume_usd`, medianSaleUsd: sql`excluded.median_sale_usd`, avgSaleUsd: sql`excluded.avg_sale_usd`, marketCapEstUsd: sql`excluded.market_cap_est_usd`, marketCapConfidence: sql`excluded.market_cap_confidence`, liquidityScore: sql`excluded.liquidity_score`, momentum: sql`excluded.momentum`, breadth: sql`excluded.breadth`, trackedAssets: sql`excluded.tracked_assets`, coverage: sql`excluded.coverage` } });
122 } 122 }
123− subSeries[def.ticker] = rows.map((r) => ({ date: r.date, value: r.value, transactions: r.transactions })); 123+ subSeries[def.ticker] = rows.map((r) => ({ date: r.date, value: r.value, transactions: r.transactions, constituents: r.constituentsCount }));
124 published.push(def.ticker); 124 published.push(def.ticker);
125 await emit('index_updated', { type: 'index', id: def.id }, { ticker: def.ticker, value: points.at(-1)!.value, date: points.at(-1)!.date, constituents: points.at(-1)!.constituents }); 125 await emit('index_updated', { type: 'index', id: def.id }, { ticker: def.ticker, value: points.at(-1)!.value, date: points.at(-1)!.date, constituents: points.at(-1)!.constituents });
126 } 126 }
@@ -129,7 +129,10 @@ export async function runIndices(opts: { now?: Date } = {}): Promise<{ published
129 const flagship = defs.find((d) => d.isFlagship); 129 const flagship = defs.find((d) => d.isFlagship);
130 if (flagship) { 130 if (flagship) {
131 const tickers = Object.keys(subSeries); 131 const tickers = Object.keys(subSeries);
132− if (tickers.length >= 1) { 132+ // The flagship publishes only when its own minimum (25 distinct priced constituents across the
133+ // published subindices, as stated on /rareindex) is met — not as soon as one subindex exists.
134+ const flagshipConstituents = tickers.reduce((a, t) => a + (subSeries[t]!.at(-1)?.constituents ?? 0), 0);
135+ if (tickers.length >= 1 && flagshipConstituents >= flagship.minConstituents) {
133 const dates = [...new Set(tickers.flatMap((t) => subSeries[t]!.map((p) => p.date)))].sort(); 136 const dates = [...new Set(tickers.flatMap((t) => subSeries[t]!.map((p) => p.date)))].sort();
134 const last = new Map<string, number>(); 137 const last = new Map<string, number>();
135 let level = flagship.baseValue; 138 let level = flagship.baseValue;
@@ -166,7 +169,7 @@ export async function runIndices(opts: { now?: Date } = {}): Promise<{ published
166 await db().insert(indexValues).values(rows.slice(i, i + 500)).onConflictDoUpdate({ target: [indexValues.indexId, indexValues.date], set: { value: sql`excluded.value`, constituentsCount: sql`excluded.constituents_count`, transactions: sql`excluded.transactions`, breadth: sql`excluded.breadth`, coverage: sql`excluded.coverage` } }); 169 await db().insert(indexValues).values(rows.slice(i, i + 500)).onConflictDoUpdate({ target: [indexValues.indexId, indexValues.date], set: { value: sql`excluded.value`, constituentsCount: sql`excluded.constituents_count`, transactions: sql`excluded.transactions`, breadth: sql`excluded.breadth`, coverage: sql`excluded.coverage` } });
167 } 170 }
168 published.push(flagship.ticker); 171 published.push(flagship.ticker);
169− } else skipped.push('RARE (no subindex published)'); 172+ } else skipped.push(tickers.length ? `RARE (${flagshipConstituents}/${flagship.minConstituents} constituents across ${tickers.length} subindices)` : 'RARE (no subindex published)');
170 } 173 }
171 174
172 // correlations between subindices 175 // correlations between subindices
modified workers/valuation/run.ts +48 −18
@@ -1,6 +1,6 @@
1 import { and, desc, eq, gte, inArray, isNull, or, sql } from 'drizzle-orm'; 1 import { and, desc, eq, gte, inArray, isNull, or, sql } from 'drizzle-orm';
2 import { assetStats, assetVariants, assets, gradePremiums, listings, populationReports, priceObservations, priceSnapshots, sales, sources, valuations, variantStats } from '@rareindex/database'; 2 import { assetStats, assetVariants, assets, gradePremiums, listings, populationReports, priceObservations, priceSnapshots, sales, sources, valuations, variantStats } from '@rareindex/database';
3−import { adjustmentFactor, assetDataQuality, computeGradePremiums, computeValuation, detectOutliers, liquidityScore, momentumScore, rarityScore, trendingScore, valueOpportunity, 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, adjustmentFactor, assetDataQuality, computeGradePremiums, computeValuation, detectOutliers, liquidityScore, momentumScore, rarityScore, trendingScore, type GradePremium, type SaleInput } from '@rareindex/valuation';
4 import { logger, median, newId, pctChange, toDateOnly } from '@rareindex/shared'; 4 import { logger, median, newId, pctChange, toDateOnly } from '@rareindex/shared';
5 import { db } from '../lib/db.ts'; 5 import { db } from '../lib/db.ts';
6 import { auditMany } from '../lib/audit.ts'; 6 import { auditMany } from '../lib/audit.ts';
@@ -8,6 +8,7 @@ import { emit } from '../lib/events.ts';
8 8
9 const log = logger.child({ component: 'valuation' }); 9 const log = logger.child({ component: 'valuation' });
10 const DAY = 86_400_000; 10 const DAY = 86_400_000;
11+const safeRatioV = (v: number | null): number | null => (v === null || !Number.isFinite(v) || Math.abs(v) >= 99 ? null : v);
11 const asDate = (v: unknown): Date | null => (v instanceof Date ? v : typeof v === 'string' ? new Date(v) : null); 12 const asDate = (v: unknown): Date | null => (v instanceof Date ? v : typeof v === 'string' ? new Date(v) : null);
12 13
13 const premiumCache = new Map<string, { at: number; rows: GradePremium[] }>(); 14 const premiumCache = new Map<string, { at: number; rows: GradePremium[] }>();
@@ -85,8 +86,11 @@ export async function valueAsset(assetId: string, opts: { now?: Date; rebuildHis
85 const toInput = (s: SaleRow): SaleInput => ({ id: s.id, priceUsd: Number(s.priceUsd), date: s.saleDate, trust: trust.get(s.sourceId) ?? 0.6, confidence: Number(s.confidence), grader: s.grader, grade: s.grade, quantity: s.quantity, isBundle: s.isBundle, status: s.status as SaleInput['status'] }); 86 const toInput = (s: SaleRow): SaleInput => ({ id: s.id, priceUsd: Number(s.priceUsd), date: s.saleDate, trust: trust.get(s.sourceId) ?? 0.6, confidence: Number(s.confidence), grader: s.grader, grade: s.grade, quantity: s.quantity, isBundle: s.isBundle, status: s.status as SaleInput['status'] });
86 87
87 // 3. per-variant valuations 88 // 3. per-variant valuations
88− const results: Array<{ variantId: string; out: ReturnType<typeof computeValuation>; salesCount: number; sales30d: number }> = []; 89+ const results: Array<{ variantId: string; isDefault: boolean; out: ReturnType<typeof computeValuation>; salesCount: number; sales30d: number; activeL: { count: number; minAsk: number | null } }> = [];
89 const today = toDateOnly(now); 90 const today = toDateOnly(now);
91+ // Ask-vs-RIV is recomputed from scratch on every pass: a stale discount computed against a
92+ // previous (possibly wrong-variant) valuation must never survive (§84, §196).
93+ await db().update(listings).set({ discountToRiv: null }).where(and(eq(listings.assetId, assetId), eq(listings.availability, 'available'), sql`${listings.discountToRiv} is not null`));
90 for (const v of variants) { 94 for (const v of variants) {
91 const own = (byVariant.get(v.id) ?? []).map(toInput); 95 const own = (byVariant.get(v.id) ?? []).map(toInput);
92 const comps: NonNullable<Parameters<typeof computeValuation>[0]['comps']> = []; 96 const comps: NonNullable<Parameters<typeof computeValuation>[0]['comps']> = [];
@@ -107,7 +111,8 @@ export async function valueAsset(assetId: string, opts: { now?: Date; rebuildHis
107 }); 111 });
108 const salesCount = own.filter((s) => (s.status ?? 'valid') === 'valid').length; 112 const salesCount = own.filter((s) => (s.status ?? 'valid') === 'valid').length;
109 const sales30d = own.filter((s) => (s.status ?? 'valid') === 'valid' && now.getTime() - s.date.getTime() <= 30 * DAY).length; 113 const sales30d = own.filter((s) => (s.status ?? 'valid') === 'valid' && now.getTime() - s.date.getTime() <= 30 * DAY).length;
110− results.push({ variantId: v.id, out, salesCount, sales30d }); 114+ const activeL = await activeListingStats(assetId, v.id);
115+ results.push({ variantId: v.id, isDefault: v.isDefault, out, salesCount, sales30d, activeL });
111 if (out.riv === null && salesCount === 0 && (obsByVariant.get(v.id) ?? []).length === 0) continue; 116 if (out.riv === null && salesCount === 0 && (obsByVariant.get(v.id) ?? []).length === 0) continue;
112 await db().insert(valuations).values({ 117 await db().insert(valuations).values({
113 id: newId('valuation'), 118 id: newId('valuation'),
@@ -129,33 +134,52 @@ export async function valueAsset(assetId: string, opts: { now?: Date; rebuildHis
129 }); 134 });
130 const prev30 = await snapshotValue(assetId, v.id, new Date(now.getTime() - 30 * DAY)); 135 const prev30 = await snapshotValue(assetId, v.id, new Date(now.getTime() - 30 * DAY));
131 const prev1y = await snapshotValue(assetId, v.id, new Date(now.getTime() - 365 * DAY)); 136 const prev1y = await snapshotValue(assetId, v.id, new Date(now.getTime() - 365 * DAY));
132− const activeL = await activeListingStats(assetId, v.id);
133 const latest = own[0]; 137 const latest = own[0];
134 await db() 138 await db()
135 .insert(variantStats) 139 .insert(variantStats)
136− .values({ variantId: v.id, assetId, rivUsd: out.riv, rivLowUsd: out.low, rivHighUsd: out.high, rivConfidence: out.confidence, rivSampleSize: out.sampleSize, latestSaleUsd: latest?.priceUsd ?? null, latestSaleAt: latest?.date ?? null, change30d: pctChange(prev30, out.riv), change1y: pctChange(prev1y, out.riv), salesCount, sales30d, activeListings: activeL.count, minAskUsd: activeL.minAsk, liquidityScore: liquidityScore({ salesPerMonth: (own.length / 36) || 0, activeListings: activeL.count, sources: new Set((byVariant.get(v.id) ?? []).map((s) => s.sourceId)).size, medianDaysBetweenSales: medianGapDays(own.map((s) => s.date)), askSoldSpread: activeL.minAsk && out.riv ? (activeL.minAsk - out.riv) / out.riv : null }), updatedAt: now }) 140+ .values({ variantId: v.id, assetId, rivUsd: out.riv, rivLowUsd: out.low, rivHighUsd: out.high, rivConfidence: out.confidence, rivSampleSize: out.sampleSize, latestSaleUsd: latest?.priceUsd ?? null, latestSaleAt: latest?.date ?? null, change30d: safeRatioV(pctChange(prev30, out.riv)), change1y: safeRatioV(pctChange(prev1y, out.riv)), salesCount, sales30d, activeListings: activeL.count, minAskUsd: activeL.minAsk, liquidityScore: liquidityScore({ salesPerMonth: (own.length / 36) || 0, activeListings: activeL.count, sources: new Set((byVariant.get(v.id) ?? []).map((s) => s.sourceId)).size, medianDaysBetweenSales: medianGapDays(own.map((s) => s.date)), askSoldSpread: activeL.minAsk && out.riv ? (activeL.minAsk - out.riv) / out.riv : null }), updatedAt: now })
137− .onConflictDoUpdate({ target: variantStats.variantId, set: { rivUsd: out.riv, rivLowUsd: out.low, rivHighUsd: out.high, rivConfidence: out.confidence, rivSampleSize: out.sampleSize, latestSaleUsd: latest?.priceUsd ?? null, latestSaleAt: latest?.date ?? null, change30d: pctChange(prev30, out.riv), change1y: pctChange(prev1y, out.riv), salesCount, sales30d, activeListings: activeL.count, minAskUsd: activeL.minAsk, updatedAt: now } }); 141+ .onConflictDoUpdate({ target: variantStats.variantId, set: { rivUsd: out.riv, rivLowUsd: out.low, rivHighUsd: out.high, rivConfidence: out.confidence, rivSampleSize: out.sampleSize, latestSaleUsd: latest?.priceUsd ?? null, latestSaleAt: latest?.date ?? null, change30d: safeRatioV(pctChange(prev30, out.riv)), change1y: safeRatioV(pctChange(prev1y, out.riv)), salesCount, sales30d, activeListings: activeL.count, minAskUsd: activeL.minAsk, updatedAt: now } });
138 await db() 142 await db()
139 .insert(priceSnapshots) 143 .insert(priceSnapshots)
140 .values({ assetId, variantId: v.id, date: today, rivUsd: out.riv, latestSaleUsd: latest?.priceUsd ?? null, medianUsd: out.distribution.median, salesCount: own.filter((s) => toDateOnly(s.date) === today).length, volumeUsd: own.filter((s) => toDateOnly(s.date) === today).reduce((a, s) => a + s.priceUsd, 0) || null, listingsCount: activeL.count, minAskUsd: activeL.minAsk, observationUsd: out.methods.guide }) 144 .values({ assetId, variantId: v.id, date: today, rivUsd: out.riv, latestSaleUsd: latest?.priceUsd ?? null, medianUsd: out.distribution.median, salesCount: own.filter((s) => toDateOnly(s.date) === today).length, volumeUsd: own.filter((s) => toDateOnly(s.date) === today).reduce((a, s) => a + s.priceUsd, 0) || null, listingsCount: activeL.count, minAskUsd: activeL.minAsk, observationUsd: out.methods.guide })
141 .onConflictDoUpdate({ target: [priceSnapshots.assetId, priceSnapshots.variantId, priceSnapshots.date], set: { rivUsd: out.riv, latestSaleUsd: latest?.priceUsd ?? null, medianUsd: out.distribution.median, listingsCount: activeL.count, minAskUsd: activeL.minAsk, observationUsd: out.methods.guide } }); 145 .onConflictDoUpdate({ target: [priceSnapshots.assetId, priceSnapshots.variantId, priceSnapshots.date], set: { rivUsd: out.riv, latestSaleUsd: latest?.priceUsd ?? null, medianUsd: out.distribution.median, listingsCount: activeL.count, minAskUsd: activeL.minAsk, observationUsd: out.methods.guide } });
142− // value opportunity on active listings of this variant 146+ // Ask vs RIV on the active listings of THIS variant only (§84 data-quality gate):
143− if (out.riv !== null && out.confidence >= 0.5) { 147+ // – the valuation must rest on ≥ ASK_MIN_SAMPLE transactions with confidence ≥ ASK_MIN_CONFIDENCE
148+ // (comps-only / guide-only estimates never qualify an ask);
149+ // – the listing must be confidently matched and carry a readable grade when it is slabbed;
150+ // – an ask outside [0.1×, 10×] RIV is flagged `riv_anomaly` (identity/data problem), not priced.
151+ if (out.riv !== null && out.basis === 'transactions' && out.confidence >= ASK_MIN_CONFIDENCE && out.sampleSize >= ASK_MIN_SAMPLE) {
152+ const lo = out.riv * ASK_ANOMALY_LOW_RATIO;
153+ const hi = out.riv * ASK_ANOMALY_HIGH_RATIO;
154+ const scope = and(eq(listings.variantId, v.id), eq(listings.availability, 'available'), sql`${listings.priceUsd} > 0`);
155+ await db()
156+ .update(listings)
157+ .set({ discountToRiv: sql`round((${listings.priceUsd} - ${out.riv}) / ${out.riv}, 4)`, flags: sql`array_remove(${listings.flags}, 'riv_anomaly')` })
158+ .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)`));
144 await db() 159 await db()
145 .update(listings) 160 .update(listings)
146− .set({ discountToRiv: sql`case when ${listings.priceUsd} is null then null else round((${listings.priceUsd} - ${out.riv}) / ${out.riv}, 4) end` }) 161+ .set({ discountToRiv: null, flags: sql`case when 'riv_anomaly' = any(${listings.flags}) then ${listings.flags} else array_append(${listings.flags}, 'riv_anomaly') end` })
147− .where(and(eq(listings.variantId, v.id), eq(listings.availability, 'available'))); 162+ .where(and(scope, sql`(${listings.priceUsd} < ${lo} or ${listings.priceUsd} > ${hi})`));
148 } 163 }
149 } 164 }
150 165
151− // 4. asset-level: representative variant = highest confidence, then most sales 166+ // 4. asset-level representative variant (the headline RIV). Preference order:
167+ // transaction-based valuation with confidence ≥ 0.5 and ≥ 5 sales → the DEFAULT (raw/base) variant
168+ // when it qualifies, else the qualifying variant with the most sales; fallback: any priced variant
169+ // by confidence. Rationale: a graded sub-variant with a tighter distribution used to win over the
170+ // base market and made the headline value (and every ask comparison) wrong by an order of magnitude.
152 const priced = results.filter((r) => r.out.riv !== null).sort((a, b) => b.out.confidence - a.out.confidence || b.salesCount - a.salesCount); 171 const priced = results.filter((r) => r.out.riv !== null).sort((a, b) => b.out.confidence - a.out.confidence || b.salesCount - a.salesCount);
153− const rep = priced[0] ?? null; 172+ const qualified = priced.filter((r) => r.out.basis === 'transactions' && r.out.confidence >= ASK_MIN_CONFIDENCE && r.out.sampleSize >= ASK_MIN_SAMPLE);
173+ const rep = qualified.find((r) => r.isDefault) ?? qualified.sort((a, b) => b.salesCount - a.salesCount || b.out.confidence - a.out.confidence)[0] ?? priced[0] ?? null;
154 const validSales = saleRows.filter((s) => s.status === 'valid'); 174 const validSales = saleRows.filter((s) => s.status === 'valid');
155 const latest = validSales[0]; 175 const latest = validSales[0];
156− const prevAsset = { d1: await snapshotValue(assetId, '', new Date(now.getTime() - 1 * DAY)), d7: await snapshotValue(assetId, '', new Date(now.getTime() - 7 * DAY)), d30: await snapshotValue(assetId, '', new Date(now.getTime() - 30 * DAY)), d90: await snapshotValue(assetId, '', new Date(now.getTime() - 90 * DAY)), y1: await snapshotValue(assetId, '', new Date(now.getTime() - 365 * DAY)) }; 176+ // Changes are measured on the representative variant's own series so that a change of
177+ // representative variant never shows up as a price move.
178+ const repSeries = rep?.variantId ?? '';
179+ const prevAsset = { d1: await snapshotValue(assetId, repSeries, new Date(now.getTime() - 1 * DAY)), d7: await snapshotValue(assetId, repSeries, new Date(now.getTime() - 7 * DAY)), d30: await snapshotValue(assetId, repSeries, new Date(now.getTime() - 30 * DAY)), d90: await snapshotValue(assetId, repSeries, new Date(now.getTime() - 90 * DAY)), y1: await snapshotValue(assetId, repSeries, new Date(now.getTime() - 365 * DAY)) };
157 const riv = rep?.out.riv ?? null; 180 const riv = rep?.out.riv ?? null;
158 const activeAll = await activeListingStats(assetId, null); 181 const activeAll = await activeListingStats(assetId, null);
182+ const repActive = rep?.activeL ?? null;
159 const salesCount = allSalesCount.n; 183 const salesCount = allSalesCount.n;
160 const sales30d = validSales.filter((s) => now.getTime() - s.saleDate.getTime() <= 30 * DAY).length; 184 const sales30d = validSales.filter((s) => now.getTime() - s.saleDate.getTime() <= 30 * DAY).length;
161 const sales1y = validSales.filter((s) => now.getTime() - s.saleDate.getTime() <= 365 * DAY).length; 185 const sales1y = validSales.filter((s) => now.getTime() - s.saleDate.getTime() <= 365 * DAY).length;
@@ -163,13 +187,18 @@ export async function valueAsset(assetId: string, opts: { now?: Date; rebuildHis
163 const pop = await latestPopulation(assetId); 187 const pop = await latestPopulation(assetId);
164 const [obsCount] = await db().select({ n: sql<number>`count(*)::int` }).from(priceObservations).where(eq(priceObservations.assetId, assetId)); 188 const [obsCount] = await db().select({ n: sql<number>`count(*)::int` }).from(priceObservations).where(eq(priceObservations.assetId, assetId));
165 const sourcesCount = new Set([...saleRows.map((s) => s.sourceId), ...obsRows.map((o) => o.sourceId)]).size; 189 const sourcesCount = new Set([...saleRows.map((s) => s.sourceId), ...obsRows.map((o) => o.sourceId)]).size;
166− const liq = liquidityScore({ salesPerMonth: sales1y / 12, activeListings: activeAll.count, sources: sourcesCount, medianDaysBetweenSales: medianGapDays(validSales.map((s) => s.saleDate)), askSoldSpread: activeAll.minAsk && riv ? (activeAll.minAsk - riv) / riv : null }); 190+ const liq = liquidityScore({ salesPerMonth: sales1y / 12, activeListings: activeAll.count, sources: sourcesCount, medianDaysBetweenSales: medianGapDays(validSales.map((s) => s.saleDate)), askSoldSpread: repActive?.minAsk && riv ? (repActive.minAsk - riv) / riv : null });
167 const rar = rarityScore({ population: pop?.total ?? null, productionQuantity: asset.productionQuantity, listingsPerYear: activeAll.count > 0 || sales1y > 0 ? activeAll.count * 4 : null, salesPerYear: sales1y > 0 || salesCount > 0 ? sales1y : null, populationGrowthPct: pop?.growth ?? null }); 191 const rar = rarityScore({ population: pop?.total ?? null, productionQuantity: asset.productionQuantity, listingsPerYear: activeAll.count > 0 || sales1y > 0 ? activeAll.count * 4 : null, salesPerYear: sales1y > 0 || salesCount > 0 ? sales1y : null, populationGrowthPct: pop?.growth ?? null });
168− const ch = { d1: pctChange(prevAsset.d1, riv), d7: pctChange(prevAsset.d7, riv), d30: pctChange(prevAsset.d30, riv), d90: pctChange(prevAsset.d90, riv), y1: pctChange(prevAsset.y1, riv) }; 192+ // ratio columns are numeric(8,6): anything ≥ ±99 (9,900 %) would overflow and abort the asset; such a
193+ // move is never a market signal but a data/identity artefact → stored as null (§143, §196).
194+ const safeRatio = (v: number | null): number | null => (v === null || !Number.isFinite(v) || Math.abs(v) >= 99 ? null : v);
195+ const ch = { d1: safeRatio(pctChange(prevAsset.d1, riv)), d7: safeRatio(pctChange(prevAsset.d7, riv)), d30: safeRatio(pctChange(prevAsset.d30, riv)), d90: safeRatio(pctChange(prevAsset.d90, riv)), y1: safeRatio(pctChange(prevAsset.y1, riv)) };
169 const mom = { m7: momentumScore({ priceChange: ch.d7, volumeNow: validSales.filter((s) => now.getTime() - s.saleDate.getTime() <= 7 * DAY).length, volumePrev: validSales.filter((s) => { const a = now.getTime() - s.saleDate.getTime(); return a > 7 * DAY && a <= 14 * DAY; }).length }), m30: momentumScore({ priceChange: ch.d30, volumeNow: sales30d, volumePrev: salesPrev30 }), m90: momentumScore({ priceChange: ch.d90, volumeNow: validSales.filter((s) => now.getTime() - s.saleDate.getTime() <= 90 * DAY).length, volumePrev: validSales.filter((s) => { const a = now.getTime() - s.saleDate.getTime(); return a > 90 * DAY && a <= 180 * DAY; }).length }), y1: momentumScore({ priceChange: ch.y1, volumeNow: sales1y, volumePrev: validSales.length - sales1y }) }; 196 const mom = { m7: momentumScore({ priceChange: ch.d7, volumeNow: validSales.filter((s) => now.getTime() - s.saleDate.getTime() <= 7 * DAY).length, volumePrev: validSales.filter((s) => { const a = now.getTime() - s.saleDate.getTime(); return a > 7 * DAY && a <= 14 * DAY; }).length }), m30: momentumScore({ priceChange: ch.d30, volumeNow: sales30d, volumePrev: salesPrev30 }), m90: momentumScore({ priceChange: ch.d90, volumeNow: validSales.filter((s) => now.getTime() - s.saleDate.getTime() <= 90 * DAY).length, volumePrev: validSales.filter((s) => { const a = now.getTime() - s.saleDate.getTime(); return a > 90 * DAY && a <= 180 * DAY; }).length }), y1: momentumScore({ priceChange: ch.y1, volumeNow: sales1y, volumePrev: validSales.length - sales1y }) };
170 const listingMomentum = activeAll.count > 0 ? Math.min(100, activeAll.count * 5) : null; 197 const listingMomentum = activeAll.count > 0 ? Math.min(100, activeAll.count * 5) : null;
171 const trending = trendingScore({ priceMomentum: mom.m30, volumeMomentum: momentumScore({ priceChange: null, volumeNow: sales30d, volumePrev: salesPrev30 }), searchMomentum: null, listingMomentum, newsMomentum: null }); 198 const trending = trendingScore({ priceMomentum: mom.m30, volumeMomentum: momentumScore({ priceChange: null, volumeNow: sales30d, volumePrev: salesPrev30 }), searchMomentum: null, listingMomentum, newsMomentum: null });
172− const [bestListing] = await db().select({ d: sql<number>`min(${listings.discountToRiv})` }).from(listings).where(and(eq(listings.assetId, assetId), eq(listings.availability, 'available'))); 199+ // Best gated ask across the asset's variants (negative = below its own variant's RIV). No fallback:
200+ // comparing the cheapest ask of ANY variant against the headline RIV was the −8,500 % bug.
201+ 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`));
173 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 }; 202 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 };
174 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 }); 203 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 });
175 const statsRow = { 204 const statsRow = {
@@ -196,7 +225,8 @@ export async function valueAsset(assetId: string, opts: { now?: Date; rebuildHis
196 sales1y, 225 sales1y,
197 volume30dUsd: sales30d ? validSales.filter((s) => now.getTime() - s.saleDate.getTime() <= 30 * DAY).reduce((a, s) => a + Number(s.priceUsd), 0) : null, 226 volume30dUsd: sales30d ? validSales.filter((s) => now.getTime() - s.saleDate.getTime() <= 30 * DAY).reduce((a, s) => a + Number(s.priceUsd), 0) : null,
198 activeListings: activeAll.count, 227 activeListings: activeAll.count,
199− minAskUsd: activeAll.minAsk, 228+ // lowest ask of the representative variant (comparable to the headline RIV); all variants otherwise
229+ minAskUsd: repActive?.minAsk ?? activeAll.minAsk,
200 observationsCount: obsCount?.n ?? 0, 230 observationsCount: obsCount?.n ?? 0,
201 sourcesCount, 231 sourcesCount,
202 liquidityScore: liq, 232 liquidityScore: liq,
@@ -206,7 +236,7 @@ export async function valueAsset(assetId: string, opts: { now?: Date; rebuildHis
206 momentum90d: mom.m90, 236 momentum90d: mom.m90,
207 momentum1y: mom.y1, 237 momentum1y: mom.y1,
208 trendingScore: trending, 238 trendingScore: trending,
209− valueOpportunity: bestListing?.d !== null && bestListing?.d !== undefined ? Number(bestListing.d) : (riv && activeAll.minAsk ? valueOpportunity(activeAll.minAsk, riv, rep?.out.confidence ?? null) : null), 239+ valueOpportunity: bestListing?.d !== null && bestListing?.d !== undefined ? Number(bestListing.d) : null,
210 dataQuality: dq, 240 dataQuality: dq,
211 updatedAt: now, 241 updatedAt: now,
212 }; 242 };
213 243
Menu

Theme