Back
Front
1986 Topps Garbage Pail Kids Series 4 STICKERS #152A Whisperin' Woody front
Back
Price history · PSA 9
image from latest verified sale
1986 GARBAGE PAIL KIDS STICKERS

#152a WHISPERIN' WOODY

STICKERS

PSA 9

— Loading
Pop
53
Garbage Pail Kids
TCA Value iTCAxis Value is a recency-weighted market value from REAL sales of this exact card+grade (recent comps count most, outliers stripped) — steadier and harder to game than a single 'last sale', which can be one lucky auction or a shill bid. When a card has few or no recent comps, we estimate transparently from its own older sales, its other grades, and comparable cards (same subject/set/rarity tier), factoring subject trend and scarcity — and we LABEL it as an estimate until real sales arrive.
Confidence
★★★★★
Last Sale
Avg Sale (30d)
across — sales
Volume (30d)
— vs prior 30d
Comp Sales (All)
all grades
Now vs 1 mo ago
Now vs 2 mo ago
Now vs 3 mo ago
High Low Avg sales Subject trend
Current values by grade
PSA 10$47
PSA 9$7
PSA 8$14
PSA 6$18
Recent sales
2026-07-02PSA 10$30
2026-05-10PSA 6$26
2026-05-07PSA 10$46
2026-05-07PSA 10$50
2026-03-29PSA 10$52
2026-02-15PSA 9$8
2026-01-30PSA 8$10
2026-01-28PSA 6$12
2026-01-27PSA 10$22
2026-01-20PSA 8$20
const SPEC_ID = "garbage_pail_kids|1986|topps|garbage_pail_kids|152a|whisperin'_woody|psa|9"; // Bill 2026-07-06: GPK cards keep the GPK identity — logo top-left links back to the guide, GPK palette. if (String(SPEC_ID).indexOf('garbage_pail_kids|') === 0) { try { document.documentElement.classList.add('gpk-skin'); var st = document.createElement('style'); st.textContent = '.gpk-skin body{background:#000 !important}' + '.gpk-skin #tcaValue{color:#a3e635 !important}' + '.gpk-skin h2{color:#22a7e6}' + '.gpk-skin .badge{border-color:rgba(163,230,53,.4)}' + '#gpkHomeLogo{position:fixed;top:6px;left:10px;z-index:1000;width:300px;filter:drop-shadow(0 6px 18px rgba(0,0,0,.7))}'; document.head.appendChild(st); var a = document.createElement('a'); a.href='/gpk-price-guide'; a.title='GPK Price Guide'; a.innerHTML = ''; document.body.appendChild(a); var oldLogo = document.querySelector('header img, nav img'); if (oldLogo) oldLogo.style.visibility='hidden'; } catch(e){} } const FIRST_CERT = "96712251"; const fmt = v => v == null ? '—' : '$' + Math.round(Number(v)).toLocaleString(); function searchGo(q) { q = (q || '').trim(); if (!q) return; if (/^\d{6,12}$/.test(q)) { window.location = '/cert/' + q; return; } window.location = '/?q=' + encodeURIComponent(q); } function openLightbox(src) { document.getElementById('lightboxImg').src = src; document.getElementById('lightbox').classList.add('open'); } function selectTab(name) { document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('tab-active', b.dataset.tab === name)); document.querySelectorAll('.tab-panel').forEach(p => p.classList.toggle('hidden', p.id !== 'tab-' + name)); } document.querySelectorAll('.tab-btn').forEach(b => b.addEventListener('click', () => selectTab(b.dataset.tab))); async function load() { let data; // Bill 2026-06-10: surface errors visibly instead of silently bailing. const setBadgeErr = (msg) => { const b = document.getElementById('axisScore'); if (b) { b.className = 'badge'; b.style.background='rgba(248,113,113,.2)'; b.style.color='#fca5a5'; b.style.padding='8px 14px'; b.textContent = msg; } }; // Bill 2026-07-02: checklist / never-sold cards have pop + an estimate value // but no VETTED cert, so /api/cert returns 'not yet vetted'. Rather than bail // (a wall of dashes), populate hero value + pop + grade ladder from the spec. // Bill 2026-07-06: sales history must load even when /api/cert is empty/broken // (fragmented GPK cards). Extracted so the fallback path can call it too. var __allGradeSales = []; function loadSales(fallbackSales){ var sales = fallbackSales || []; __allGradeSales = sales; fetch('/api/spec/' + encodeURIComponent(SPEC_ID) + '/all-grade-sales').then(function(r){return r.json();}).then(function(d){ __allGradeSales = (d.sales || sales || []); var _ts=document.getElementById('totalSales'); if(_ts && __allGradeSales.length) _ts.textContent = __allGradeSales.length.toLocaleString(); renderSales(__allGradeSales); var gset = [...new Set(__allGradeSales.map(function(s){return s.psa_grade || s.grade;}).filter(Boolean))].sort(function(a,b){return (+b)-(+a);}); var gsel = document.getElementById('salesGrade'); if(gsel){ gsel.innerHTML = ''; gset.forEach(function(g){ var o=document.createElement('option'); o.value=g; o.textContent='PSA '+g; gsel.appendChild(o); }); var _curG = String(SPEC_ID.split('|').pop()); if (gset.map(String).includes(_curG)) { gsel.value = _curG; renderSales(__allGradeSales); } } }).catch(function(){ renderSales(sales); }); var sf=document.getElementById('salesFilter'); if(sf && !sf._wired){ sf._wired=1; sf.addEventListener('input', function(){ renderSales(__allGradeSales); }); } var sg=document.getElementById('salesGrade'); if(sg && !sg._wired){ sg._wired=1; sg.addEventListener('change', function(){ renderSales(__allGradeSales); }); } } async function populateFromSpec(neverSoldHint) { try { const sr = await fetch('/api/spec/' + encodeURIComponent(SPEC_ID)); const sd = await sr.json(); const sp = (sd && sd.spec) || {}; const st = sp.tca_status || ''; const priced = (sp.slabaxis_value > 0) && (st === 'ok' || st === 'ok_thin'); const neverSold = (neverSoldHint === true) ? true : (neverSoldHint === false ? !priced : (st === 'estimate' || !priced)); document.getElementById('tcaValue').textContent = fmt(sp.slabaxis_value); document.getElementById('tcaSubtext').textContent = 'PSA ' + (sp.grade || '') + (neverSold ? ' · estimate — never sold' : ' · this grade'); var pbg = sp.pop_by_grade || sp.psa_pop_by_grade; if (typeof pbg === 'string') { try { pbg = JSON.parse(pbg); } catch(e){ pbg = null; } } var popTotal = null; if (sp.psa_pop_total != null) popTotal = Number(sp.psa_pop_total); else if (pbg) popTotal = Object.values(pbg).reduce(function(a,b){return a+(Number(b)||0);},0); document.getElementById('popValue').textContent = (popTotal != null) ? popTotal.toLocaleString() : '—'; if (pbg) { var grades = Object.keys(pbg).sort(function(a,b){return parseFloat(b)-parseFloat(a);}); var gl = document.getElementById('gradeLadder'); if (gl) gl.textContent = 'All grades — ' + grades.map(function(g){return 'PSA '+g;}).join(' / '); } var b = document.getElementById('axisScore'); if (b) { b.className='badge'; b.style.background='rgba(148,163,184,.2)'; b.style.color='#cbd5e1'; b.style.padding='8px 14px'; b.textContent = neverSold ? 'Never sold — estimate' : 'Catalog card'; } try { var _ff = sp.psa_image_front || sp.cardhedger_image || sp.image_front || sp.display_image; if (_ff) { var _fw=document.getElementById('frontImgWrap')||document.getElementById('imgWrap'); if(_fw) _fw.innerHTML = ''; } var _bb = sp.psa_image_back || sp.image_back; var _bw = document.getElementById('backImgWrap'); if (_bw) _bw.innerHTML = _bb ? '' : '
No back image
'; } catch(e){} try{var _pl=document.getElementById('psaLink');if(_pl&&FIRST_CERT){_pl.href='https://www.psacard.com/cert/'+encodeURIComponent(FIRST_CERT);var _pll=document.getElementById('psaLinkLabel');if(_pll)_pll.textContent='#'+FIRST_CERT;}}catch(e){} try { loadSales([]); } catch(e){} } catch(e) { console.error('spec fallback err:', e); setBadgeErr('No data'); } } if (!FIRST_CERT) { await populateFromSpec(false); return; } try { const ctrl = new AbortController(); const tm = setTimeout(()=>ctrl.abort(), 15000); const r = await fetch('/api/cert/' + FIRST_CERT, {signal: ctrl.signal}); clearTimeout(tm); if (!r.ok) { await populateFromSpec(false); return; } data = await r.json(); if (data.error) { await populateFromSpec(false); return; } } catch (e) { console.error('fetch err:', e); await populateFromSpec(false); return; } const variants = (data.grade_variants || []).slice(); variants.sort((a,b) => (+b.grade) - (+a.grade)); // Bill 2026-06-10: a spec page represents the CARD — show every grade. const gl = document.getElementById('gradeLadder'); if (gl && variants.length) { gl.textContent = 'All grades — ' + variants.map(function(v){ return 'PSA ' + v.grade; }).join(' / '); } const gc = document.getElementById('gradeChips'); if (gc && variants.length) { var CURG = (SPEC_ID.split('|').pop() || ''); gc.innerHTML = variants.map(function(v){ var cur = String(v.grade) === CURG; var style = cur ? 'background:rgba(34,197,94,.3);color:#bbf7d0;border:1px solid rgba(34,197,94,.6)' : 'background:rgba(34,197,94,.12);color:#86efac;border:1px solid rgba(34,197,94,.3)'; return 'PSA ' + v.grade + ''; }).join(''); } // Image back var _bk = data.psa_image_back || data.image_back; // PSA raw scan first (Bill: no slab when PSA photo exists) if (_bk) { document.getElementById('backImgWrap').innerHTML = ``; } else { document.getElementById('backImgWrap').innerHTML = '
No back image
'; } // Pop big card if (data.psa_pop_total != null && Number(data.psa_pop_total) > 0) { document.getElementById('popValue').textContent = Number(data.psa_pop_total).toLocaleString(); } // Stats const totalSales = variants.reduce((s, v) => s + (v.sale_count || 0), 0); document.getElementById('totalSales').textContent = totalSales.toLocaleString(); // 2026-06-28: this page is for ONE grade — use the CURRENT grade's variant for // the hero value, tiles and confidence (data.sales only holds this grade, so // keying off the highest grade left lower-grade pages with empty tiles). Fall // back to the highest grade only if the current grade isn't among the variants. const _curGrade = String(SPEC_ID.split('|').pop()); const top = variants.find(v => String(v.grade) === _curGrade) || variants[0]; if (top) { const _isHighest = variants[0] && String(variants[0].grade) === String(top.grade); document.getElementById('tcaValue').textContent = fmt(top.slabaxis_value); document.getElementById('tcaSubtext').textContent = 'PSA ' + top.grade + (_isHighest ? ' · highest-graded comp' : ' · this grade'); // eBay search links const PLAYER_S = "Whisperin\u0027 Woody"; const YEAR_S = "1986"; const BRAND_S = "Topps"; const playerQ = encodeURIComponent(PLAYER_S.replace(/'/g,'')); const gradeStr = 'PSA ' + top.grade; document.getElementById('ebayActiveLink').href = 'https://www.ebay.com/sch/i.html?_nkw=' + encodeURIComponent(gradeStr + ' ' + PLAYER_S + ' ' + YEAR_S); document.getElementById('ebaySoldLink').href = 'https://www.ebay.com/sch/i.html?LH_Sold=1&LH_Complete=1&_nkw=' + encodeURIComponent(gradeStr + ' ' + PLAYER_S + ' ' + YEAR_S); document.getElementById('ebayActiveLabel').textContent = gradeStr + ' ' + PLAYER_S; document.getElementById('ebaySoldLabel').textContent = gradeStr + ' ' + PLAYER_S; // PSA link: prefer cert detail page if we have a cert; fall back to PSA pop search const firstCert = "96712251"; const psaEl = document.getElementById('psaLink'); const psaLbl = document.getElementById('psaLinkLabel'); if (firstCert) { psaEl.href = 'https://www.psacard.com/cert/' + encodeURIComponent(firstCert); psaLbl.textContent = '#' + firstCert; } else { var APRID = "525791"; if (APRID) { psaEl.href = 'https://www.psacard.com/spec/psa/' + APRID; psaLbl.textContent = 'sales history'; } else { psaEl.href = 'https://www.psacard.com/pop/search?searchTerm=' + encodeURIComponent(PLAYER_S + ' ' + YEAR_S + ' ' + BRAND_S); } psaLbl.textContent = 'pop search'; } } // Confidence stars (1-5) — derived from sale_count: 0=1*, 1-2=2*, 3-9=3*, 10-29=4*, 30+=5* if (top) { const n = top.sale_count || 0; const stars = n >= 30 ? 5 : n >= 10 ? 4 : n >= 3 ? 3 : n >= 1 ? 2 : 1; document.getElementById('tcaStars').textContent = '★'.repeat(stars) + '☆'.repeat(5-stars); document.getElementById('tcaConfidenceLabel').textContent = n + ' comp sales'; } // Last sale + 30d Avg + Volume + Trend strip const sales = data.sales || []; const sortedSales = sales.filter(s => s.sale_date).sort((a,b) => b.sale_date.localeCompare(a.sale_date)); if (sortedSales.length && top) { const topG = String(top.grade); let topGSales = sortedSales.filter(s => (s.psa_grade||s.grade) == topG); // 2026-06-21 P0: band-guard (0.33-3x median) so contaminating sales (a wrong // card / raw mislabeled - e.g. an $820 sale on a $13k PSA 10) can't drive the // Last Sale / Avg / trend tiles. Mirrors tca_recompute_spec's outlier band. (function(){ const v = topGSales.map(s=>+s.price).filter(x=>x>0).sort((a,b)=>a-b); if (v.length >= 4) { const m0 = v[Math.floor(v.length/2)]; if (m0 > 0) { const banded = topGSales.filter(s => { const x=+s.price; return x>=0.33*m0 && x<=3*m0; }); if (banded.length) topGSales = banded; } } })(); if (topGSales.length) { const ls = topGSales[0]; document.getElementById('lastSalePrice').textContent = fmt(ls.all_in_price || ls.price); document.getElementById('lastSaleDate').textContent = ls.sale_date + ' · PSA ' + (ls.psa_grade || ls.grade); } const now = new Date(); const ago = d => Math.floor((now - new Date(d)) / 86400000); const med = a => { if(!a.length) return null; const s = [...a].sort((x,y)=>x-y); return s[Math.floor(s.length/2)]; }; const avg = a => a.length ? a.reduce((s,x)=>s+x,0)/a.length : null; const w30 = topGSales.filter(s => ago(s.sale_date)>=0 && ago(s.sale_date)<=30).map(s => +s.price); const w60 = topGSales.filter(s => ago(s.sale_date)>30 && ago(s.sale_date)<=60).map(s => +s.price); const w90 = topGSales.filter(s => ago(s.sale_date)>60 && ago(s.sale_date)<=90).map(s => +s.price); const w120 = topGSales.filter(s => ago(s.sale_date)>90 && ago(s.sale_date)<=120).map(s => +s.price); const avg30 = avg(w30); document.getElementById('avgSale30').textContent = avg30 != null ? fmt(avg30) : '—'; document.getElementById('avgSale30Sub').textContent = 'across ' + w30.length + ' sales'; document.getElementById('vol30').textContent = w30.length + (w30.length===1?' sale':' sales'); if (w60.length) { const dpct = ((w30.length - w60.length) / w60.length) * 100; const sign = dpct >= 0 ? '+' : ''; document.getElementById('vol30Delta').textContent = sign + dpct.toFixed(0) + '% SALES COUNT vs prior 30d'; } function pctTile(id, recent, prior) { const rm = med(recent), pm = med(prior); if (!rm || !pm) { document.getElementById(id).textContent = '—'; return; } const r = (rm - pm) / pm * 100; const el = document.getElementById(id); el.textContent = (r >= 0 ? '+' : '') + r.toFixed(1) + '%'; el.style.color = r >= 0 ? '#34d399' : '#f87171'; } // Bill 2026-06-11: every tile = TODAY'S market (0-30d) vs N days ago. pctTile('trend30', w30, w60); pctTile('trend60', w30, w90); const w180 = topGSales.filter(s => ago(s.sale_date)>90 && ago(s.sale_date)<=180).map(s=>+s.price); pctTile('trend90', w30, w120.length ? w120 : w180); computeAxisScore(top, sortedSales); } else { computeAxisScore(variants[0], sortedSales); } // Grades grid renderGrades(variants); document.getElementById('gradeSort').addEventListener('change', () => renderGrades(variants)); // Sales — load across all grades (runs in BOTH cert + fallback paths) loadSales(sales); // Movers renderMovers(variants, sales); // Active eBay listings loadActiveListings(); // Flipper initFlipper(); // Pop renderPop(data.psa_pop_by_grade || {}); // Chart renderChart(sales, variants); } function computeAxisScore(top, sortedSales) { if (!top) return; const sig = document.getElementById('axisScore'); if (!sig) return; // Factors: // • 30d trend % (top grade only) // • 90d trend % // • Volume (last 30d) — more = higher confidence // • Pop scarcity (lower pop = bullish) const topG = String(top.grade); const ago = d => Math.floor((new Date() - new Date(d)) / 86400000); const med = a => { if(!a.length) return null; const s=[...a].sort((x,y)=>x-y); return s[Math.floor(s.length/2)]; }; const grade30 = sortedSales.filter(s => (s.psa_grade||s.grade)==topG && ago(s.sale_date)>=0 && ago(s.sale_date)<=30).map(s=>+s.price); const grade60 = sortedSales.filter(s => (s.psa_grade||s.grade)==topG && ago(s.sale_date)>30 && ago(s.sale_date)<=60).map(s=>+s.price); const grade90 = sortedSales.filter(s => (s.psa_grade||s.grade)==topG && ago(s.sale_date)>60 && ago(s.sale_date)<=90).map(s=>+s.price); const m30 = med(grade30), m60 = med(grade60), m90 = med(grade90); let score = 0; // -2 strong sell ... +2 strong buy let reasons = []; if (m30 && m60) { const r = (m30 - m60) / m60 * 100; if (r >= 15) { score += 2; reasons.push(`30d +${r.toFixed(0)}%`); } else if (r >= 5) { score += 1; reasons.push(`30d +${r.toFixed(0)}%`); } else if (r <= -15){ score -= 2; reasons.push(`30d ${r.toFixed(0)}%`); } else if (r <= -5) { score -= 1; reasons.push(`30d ${r.toFixed(0)}%`); } } if (m60 && m90) { const r = (m60 - m90) / m90 * 100; if (r >= 10) { score += 1; reasons.push(`60d trend up`); } else if (r <= -10){ score -= 1; reasons.push(`60d trend down`); } } // Volume bonus if (grade30.length >= 10) reasons.push(`${grade30.length} recent comps`); else if (grade30.length < 3) reasons.push(`thin volume`); // Map score to label + class let label, cls; if (score >= 3) { label = '⤴ Strong Buy'; cls = 'signal-buy'; } else if (score >= 1) { label = '↑ Buy'; cls = 'signal-buy'; } else if (score <= -3) { label = '⤵ Strong Sell'; cls = 'signal-sell'; } else if (score <= -1) { label = '↓ Sell'; cls = 'signal-sell'; } else { label = '— Hold'; cls = 'signal-hold'; } sig.className = 'badge ' + cls + ' text-sm'; sig.style.padding = '8px 14px'; sig.style.fontSize = '13px'; sig.innerHTML = label + (reasons.length ? `· ${reasons.slice(0,2).join(' · ')}` : ''); } function popPerGrade(variants, byGrade) { return variants.map(v => v.psa_pop ?? byGrade[v.grade] ?? null); } function renderGrades(variants) { const sort = document.getElementById('gradeSort').value; const sorted = [...variants]; if (sort === 'grade_desc') sorted.sort((a,b) => (+b.grade) - (+a.grade)); if (sort === 'grade_asc') sorted.sort((a,b) => (+a.grade) - (+b.grade)); if (sort === 'value_desc') sorted.sort((a,b) => (b.slabaxis_value||0) - (a.slabaxis_value||0)); if (sort === 'value_asc') sorted.sort((a,b) => (a.slabaxis_value||0) - (b.slabaxis_value||0)); document.getElementById('grades').innerHTML = sorted.map(v => { const pop = v.psa_pop ?? '—'; return `
PSA ${v.grade ?? '—'}
Pop ${pop}
${fmt(v.slabaxis_value)}
`; }).join(''); } // 2026-06-28: drop band-outliers per grade (0.33-3x median, grade needs >=4 // sales) so a contaminating point (e.g. a $2,905 sale on a ~$145 card) never // shows in the table. Mirrors tca_recompute_spec's outlier band. function bandGuardByGrade(rows) { const byG = {}; rows.forEach(s => { const g = String(s.psa_grade || s.grade || ''); (byG[g] = byG[g] || []).push(s); }); const keep = []; Object.keys(byG).forEach(g => { const arr = byG[g]; const v = arr.map(s => +s.price).filter(x => x > 0).sort((a, b) => a - b); if (v.length >= 4) { const m0 = v[Math.floor(v.length / 2)]; if (m0 > 0) { keep.push(...arr.filter(s => { const x = +s.price; return x >= 0.33 * m0 && x <= 3 * m0; })); return; } } keep.push(...arr); }); return keep; } let _salesFiltered = []; let _salesShown = 15; function renderSales(sales) { const q = (document.getElementById('salesFilter').value || '').toLowerCase(); const grade = document.getElementById('salesGrade').value; let filtered = sales.filter(s => { if (grade && (s.psa_grade || s.grade) != grade) return false; if (!q) return true; return JSON.stringify(s).toLowerCase().includes(q); }); filtered = bandGuardByGrade(filtered); filtered.sort((a, b) => (b.sale_date || '').localeCompare(a.sale_date || '')); _salesFiltered = filtered; _salesShown = 15; paintSales(); } function paintSales() { const filtered = _salesFiltered; document.getElementById('salesCount').textContent = filtered.length + ' results'; const body = document.getElementById('salesBody'); if (!filtered.length) { body.innerHTML = 'No sales'; return; } let html = filtered.slice(0, _salesShown).map(s => ` ${s.sale_date || ''} ${fmt(s.price)} PSA ${s.psa_grade || s.grade || '—'} ${s.cert || ''} ${s.auction_or_bin || ''} `).join(''); if (filtered.length > _salesShown) { const remaining = filtered.length - _salesShown; html += ` `; } body.innerHTML = html; const btn = document.getElementById('loadMoreSales'); if (btn) btn.addEventListener('click', () => { _salesShown += 15; paintSales(); }); } function renderMovers(variants, sales) { // For each grade, compute % change last 30d vs prior 30d const now = new Date(); const ago = d => Math.floor((now - new Date(d)) / 86400000); const med = a => { const s = [...a].sort((x,y)=>x-y); return s[Math.floor(s.length/2)]; }; const rows = variants.map(v => { const recent = sales.filter(s => (s.psa_grade||s.grade) == String(v.grade) && ago(s.sale_date) >= 0 && ago(s.sale_date) <= 30).map(s => +s.price); const prior = sales.filter(s => (s.psa_grade||s.grade) == String(v.grade) && ago(s.sale_date) > 30 && ago(s.sale_date) <= 60).map(s => +s.price); const pct = (recent.length >= 2 && prior.length >= 2) ? ((med(recent) - med(prior)) / med(prior)) * 100 : null; return { grade: v.grade, value: v.slabaxis_value, recent: recent.length, pct }; }).filter(r => r.pct != null).sort((a,b) => Math.abs(b.pct) - Math.abs(a.pct)); const el = document.getElementById('movers'); if (!rows.length) { el.innerHTML = '
Not enough recent sales yet to show movement.
'; return; } el.innerHTML = rows.map(r => { const up = r.pct >= 0; return `
PSA ${r.grade}
${fmt(r.value)}
${r.recent} sales last 30d
${up?'+':''}${r.pct.toFixed(1)}%
30d vs prior 30d
`; }).join(''); } function renderPop(byGrade) { const entries = Object.entries(byGrade).sort((a,b) => (+b[0]) - (+a[0])); if (!entries.length) { document.getElementById('popBody').innerHTML = 'No pop data'; return; } const total = entries.reduce((s, [_, n]) => s + (+n || 0), 0); document.getElementById('popBody').innerHTML = entries.map(([g, n]) => { const share = total > 0 ? ((+n / total) * 100).toFixed(1) + '%' : '—'; return ` PSA ${g} ${(+n || 0).toLocaleString()} ${share} `; }).join(''); // chart const ctx = document.getElementById('popChart').getContext('2d'); new Chart(ctx, { type: 'bar', data: { labels: entries.map(([g]) => 'PSA ' + g), datasets: [{ data: entries.map(([_, n]) => +n || 0), backgroundColor: '#3b82f6' }] }, options: { plugins: { legend: { display: false } }, scales: { x: { grid: { color: 'rgba(96,128,192,.1)' }, ticks: { color: '#9ca3af' } }, y: { grid: { color: 'rgba(96,128,192,.1)' }, ticks: { color: '#9ca3af' } } } } }); } let _trendDays = 90; function initFlipper() { const $ = id => document.getElementById(id); const recompute = () => { const ask = +$('flipAsk').value || 0; const shipBuyer = +$('flipShip').value || 0; const shipCost = +$('flipShipCost').value || 0; const feePct = (+$('flipFee').value || 0) / 100; const cost = +$('flipCost').value || 0; const buyerPays = ask + shipBuyer; const fee = buyerPays * feePct + 0.40; // $0.40 per-order eBay const takeHome = buyerPays - fee - shipCost; $('flipBuyerPays').textContent = fmt(buyerPays); $('flipFeeOut').textContent = fmt(fee); $('flipTakeHome').textContent = fmt(takeHome); if (cost > 0) { const profit = takeHome - cost; const pct = (profit / cost) * 100; const el = $('flipProfit'); el.textContent = fmt(profit) + ' (' + (pct >= 0 ? '+' : '') + pct.toFixed(0) + '%)'; el.style.color = profit >= 0 ? '#34d399' : '#f87171'; } else { $('flipProfit').textContent = '—'; $('flipProfit').style.color = '#9ca3af'; } // target ask const tgt = +$('flipTargetPct').value || 0; if (cost > 0 && feePct < 1) { const wantedNet = cost * (1 + tgt/100); // wantedNet = ask + shipBuyer - (ask + shipBuyer) * feePct - 0.40 - shipCost // wantedNet = (ask + shipBuyer) * (1 - feePct) - 0.40 - shipCost const needBuyerPays = (wantedNet + 0.40 + shipCost) / (1 - feePct); const needAsk = needBuyerPays - shipBuyer; $('flipTargetAsk').textContent = fmt(needAsk); } else { $('flipTargetAsk').textContent = '— enter cost basis'; } }; ['flipAsk','flipShip','flipShipCost','flipFee','flipCost','flipTargetPct'].forEach(id => { $(id).addEventListener('input', recompute); }); recompute(); // Active comparison fetch('/api/spec/' + encodeURIComponent(SPEC_ID) + '/active-ebay').then(r => r.json()).then(d => { const items = (d.listings || []).sort((a,b) => (a.ebay_all_in||0) - (b.ebay_all_in||0)); const el = $('flipperActive'); if (!items.length) { el.innerHTML = '
No active asks right now.
'; return; } el.innerHTML = items.map(x => { return `
${fmt(x.ebay_all_in)}
PSA ${x.grade} · cert #${x.cert}
View →
`; }).join(''); }); } async function loadActiveListings() { try { const r = await fetch('/api/spec/' + encodeURIComponent(SPEC_ID) + '/active-ebay'); const d = await r.json(); const el = document.getElementById('activeListings'); const items = d.listings || []; if (!items.length) { el.innerHTML = '
No active listings on eBay right now for this card.
'; return; } el.innerHTML = items.map(x => { const hot = x.is_hot; const pct = x.pct_of_comp != null ? x.pct_of_comp + '% of comp' : ''; const badge = hot ? `HOT · ${pct}` : (x.pct_of_comp != null ? `${pct}` : ''); const img = x.ebay_image_front ? `` : ''; return ` ${img}
PSA ${x.grade} ${badge}
${fmt(x.ebay_all_in)}
comp ${fmt(x.slabaxis_value)} · cert #${x.cert}
View on eBay →
`; }).join(''); } catch(e) { console.error(e); } } function renderChart(sales, variants) { const wf = document.getElementById('trendWindow'); if (wf) wf.querySelectorAll('button').forEach(b => b.addEventListener('click', () => { _trendDays = parseInt(b.dataset.d) || 0; wf.querySelectorAll('button').forEach(x => { x.classList.toggle('bg-blue-600', x === b); x.classList.toggle('text-white', x === b); x.classList.toggle('border-blue-600', x === b); x.classList.toggle('font-semibold', x === b); x.classList.toggle('bg-gray-900/40', x !== b); x.classList.toggle('border-blue-900/40', x !== b); }); drawChart(); })); const fbl = document.getElementById('chartGradeFilter'); const grades = [...new Set(sales.map(s => s.psa_grade || s.grade).filter(Boolean))].sort((a,b) => (+b) - (+a)); const selected = new Set(grades.slice(0, 3)); function buildChips() { fbl.innerHTML = grades.map(g => ` `).join(''); fbl.querySelectorAll('button').forEach(b => b.addEventListener('click', () => { const g = b.dataset.g; if (selected.has(g)) selected.delete(g); else selected.add(g); buildChips(); drawChart(); })); } function drawChart() { const ctx = document.getElementById('trendChart').getContext('2d'); if (window._chart) window._chart.destroy(); const colors = ['#3b82f6', '#10b981', '#a855f7', '#f59e0b', '#ef4444', '#06b6d4']; const datasets = [...selected].map((g, i) => { const cutoff = _trendDays > 0 ? new Date(Date.now() - _trendDays * 86400000).toISOString().slice(0,10) : '0000-00-00'; const pts = sales.filter(s => (s.psa_grade || s.grade) === g && s.sale_date && s.sale_date >= cutoff) .map(s => ({ x: s.sale_date, y: +s.price })) .sort((a,b) => a.x.localeCompare(b.x)); const c = colors[i % colors.length]; // Bill 2026-07-03: same smooth filled-area style as the chart under the // card pics — soft per-grade fill, rounded line, minimal points. const fillC = c + '26'; // ~15% alpha hex suffix return { label: 'PSA ' + g, data: pts, tension: .3, fill: true, borderColor: c, backgroundColor: fillC, pointBackgroundColor: c, pointRadius: 2, borderWidth: 2 }; }); window._chart = new Chart(ctx, { type: 'line', data: { datasets }, options: { parsing: false, scales: { x: { type: 'category', grid: { display: false }, ticks: { color: '#64748b', maxTicksLimit: 8, font: { size: 10 } } }, y: { grid: { color: 'rgba(59,130,246,.08)' }, ticks: { color: '#64748b', font: { size: 10 }, callback: v => '$' + Number(v).toLocaleString() } } }, plugins: { legend: { position: 'top', labels: { color: '#d1d5db', boxWidth: 12, font: { size: 11 } } } } } }); } buildChips(); drawChart(); } load().catch(e => console.error(e));