TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { NextResponse } from 'next/server';2import { screenAssetsExport } from '@/lib/queries/screener';3import { SCREENER_EXPORT_MAX, isPlausibleChange, parseScreenerParams, toCsv } from '@/lib/screener';4import type { SP } from '@/lib/search-params';56export const runtime = 'nodejs';7export const dynamic = 'force-dynamic';89const ATTRIBUTION = 'Source: RareIndex (www.rareindex.io). Valuations are model estimates with stated confidence; not investment advice.';1011const HEADER = ['asset_id', 'slug', 'title', 'category', 'year', 'brand', 'set', 'riv_usd', 'riv_low_usd', 'riv_high_usd', 'riv_confidence', 'riv_sample_size', 'change_30d', 'change_1y', 'liquidity_score', 'rarity_score', 'sales_30d', 'sales_1y', 'sales_total', 'active_listings', 'min_ask_usd', 'spread_vs_riv', 'value_opportunity', 'ath_usd', 'drawdown', 'volume_30d_usd', 'stats_updated_at', 'url'];1213/** GET /screener/export?…same filters…[&format=json] — current screen, capped at 5 000 rows (§160). */14export async function GET(req: Request) {15 const url = new URL(req.url);16 const sp: SP = Object.fromEntries(url.searchParams.entries());17 const filters = parseScreenerParams(sp);18 const items = await screenAssetsExport(filters, SCREENER_EXPORT_MAX);19 const asOf = new Date().toISOString();20 const held = (v: number | null) => (isPlausibleChange(v) ? v : null); // artefacts are exported as empty, never as numbers21 const rows = items.map((a) => [a.id, a.slug, a.title, a.categorySlug, a.year, a.brand, a.setName, a.rivUsd, a.rivLowUsd, a.rivHighUsd, a.rivConfidence, a.rivSampleSize, held(a.change30d), held(a.change1y), a.liquidityScore, a.rarityScore, a.sales30d, a.sales1y, a.salesCount, a.activeListings, a.minAskUsd, a.spread, a.valueOpportunity, a.athUsd, a.drawdown, a.volume30dUsd, a.updatedAt, `https://www.rareindex.io/asset/${a.slug}`]);22 if (url.searchParams.get('format') === 'json') {23 return NextResponse.json(24 { data: rows.map((r) => Object.fromEntries(HEADER.map((h, i) => [h, r[i] ?? null]))), meta: { count: rows.length, as_of: asOf, truncated: items.length >= SCREENER_EXPORT_MAX, attribution: ATTRIBUTION, filters } },25 { headers: { 'cache-control': 'private, max-age=60' } },26 );27 }28 const csv = toCsv(HEADER, rows, [`as_of=${asOf}`, ATTRIBUTION, `rows=${rows.length}${items.length >= SCREENER_EXPORT_MAX ? ' (truncated to the export cap)' : ''}`, `filters=${url.search.replace(/^\?/, '') || 'none'}`]);29 return new NextResponse(csv, {30 headers: {31 'content-type': 'text/csv; charset=utf-8',32 'content-disposition': 'attachment; filename="rareindex-screener.csv"',33 'cache-control': 'private, max-age=60',34 },35 });36}37