all docsdocs/reference/claude-design-spec.md

CardIntel.ai — Claude Design Prototype Specification

Reference documentation of the Claude Design prototype ("CardIntel.ai"), captured so it can inform the rebuild of Frankie (credit card intelligence platform).

Sources of truth:

  • design_script.js (73KB) — React-style component: all state, seed data, and derived-value logic.
  • design_markup.html (135KB) — static HTML snapshot of the rendered screens (layout/styling detail).

Companion file: design_seed.json — the full seed dataset (20 cards, 26 change events) extracted verbatim into named-key JSON.


1. Overview

CardIntel.ai is a B2B competitive-intelligence dashboard for the US credit card market. The fictional user is Dana Reyes, an operator at a card issuer/fintech who monitors competitor cards: terms, fees, APRs, sign-up bonuses, benefit changes, affiliate payouts, marketing creative, and risk/sentiment signals. The core value loop is: track cards → detect field-level changes (old → new diffs) → classify each change as more/less competitive → alert, benchmark, and report on it.

Everything is client-side and synthetic: seed data plus deterministic pseudo-random series (rand/series) generate 12-month "trend" charts. Data is stamped "as of" a frozen today = 2026-08-28.

Information architecture (sidebar)

Left sidebar (226px; 196px under 980px viewport) with the CardIntel.ai wordmark, six nav groups, a "Live coverage" stat card (% of cards verified < 7 days, green progress bar), and the user chip (avatar "DR", name, current role label).

GroupViews (state key → label)
Monitordash Dashboard · alerts Alerts · watch Watchlists · directory Card directory · compare Compare
Benchmarkcalc Rewards calculator · leaders Leaderboards · issuers Issuer rollup · launches Launches & sunsets
Growth intelbounty Affiliate bounties · creative Creative & ads
Risk signalssignals Sentiment & CFPB
Betafit Fit simulation
Workspacereports Reports · api API & export · admin Data management

Nav badges: Alerts shows an unread count (changes in the last 3 days) in a red pill; Compare shows the number of selected cards (max 4) in an accent pill. The card-detail view (detail) highlights "Card directory" as the active nav item.

Role toggle

The Dashboard header has a "View as" segmented control: Growth / Compliance / Product / Admin. In the prototype it only swaps the dashboard headline:

  • growth → "Where the field moved overnight"
  • compliance → "Terms changes needing review"
  • product → "Competitive product movement"
  • admin → "Coverage and data health"

The sidebar user chip shows "{Role} view". No other content is role-gated — a deliberate hint that role-based dashboards are a product direction, not implemented depth.

Global props

The component accepts accentColor (default #2a4fb8), highlightBest (compare-table best-cell highlighting, default on), and showBetaModules (shows/hides the Fit Score beta card on card detail, default on).


2. Data model

2.1 Card schema (from seed())

Cards are built by a constructor C(id, issuer, name, network, category, tier, color, annual, apr[], bonus[], earn[], pv, fees[], benefits[], changes[], extra{}) which expands positional arrays into named objects. Resulting record shape:

FieldTypeMeaningExample (Chase Sapphire Reserve)
idstring slugUnique key, issuer-shortname style"chase-sapphire-reserve"
issuerstringIssuing bank/fintech (10 issuers seeded)"Chase"
namestringMarketing name of the card"Sapphire Reserve"
networkenumVisa | Mastercard | Amex | Discover"Visa"
categoryenumtravel | cashback | business | secured | fair credit"travel"
tierenumRecommended credit tier: excellent (740–850) | good (670–739) | fair (580–669)"excellent"
colorhexCard-art swatch color used for the fake card graphic everywhere"#1d3557"
apr.pmin / apr.pmaxnumber %Purchase APR range floor/ceiling; pmin === 0 renders as "No interest" (Chime)21.49 / 28.49
apr.introMonthsnumber0% intro-APR period in months (0 = none)0
apr.btnumber %Balance-transfer APR (0 = N/A)21.49
apr.penaltynumber %Penalty APR (0 = none)29.99
bonus.amountstringHeadline sign-up bonus as marketed ("100,000 pts", "$200", "Cashback Match", "None")"100,000 pts"
bonus.spendstringSpend requirement, display string ("$5,000", "—")"$5,000"
bonus.timeframestringQualification window ("3 months", "12 months", "—")"3 months"
bonus.estnumber $CardIntel's estimated dollar value of the bonus; 0 = no current offer2000
earn.groceries/dining/travel/gas/othernumberEarn rate per category (points multiplier or cashback %){1, 3, 8, 1, 1}
pvnumberPoint value in cents ("CardIntel valuation, updated monthly"). 1 = cash back at face value; >1 renders rates as "Nx" instead of "N%"1.7
fees.annualnumber $Annual fee (the annual arg is copied into fees.annual)795
fees.ftfnumber|stringForeign transaction fee; 0 = "None", else a display string like "3%"0
fees.btstringBalance-transfer fee schedule, display string ("5% ($5 min)", "3% for 120d, then 5%", "Not offered")"5% ($5 min)"
fees.latenumber $Late fee (0 = none — Discover)40
fees.castringCash-advance fee, display string"5% ($10 min)"
benefitsstring[]3 marketing benefit bullets, rendered as pill tags["$300 travel credit", "Priority Pass access", "Primary rental coverage"]
changesChange[]Field-level change log, newest first (see 2.2)3 entries
statusstringAlways "live" in seed"live"
linkurlAffiliate offer linkhttps://example.com/offers/csr
sourceenum-ish stringData provenance: "Issuer T&C page", "Affiliate feed", "Manual entry", "Issuer help center""Issuer T&C page"
verifiedISO dateLast human/robot verification date; drives freshness/staleness badges"2026-08-28"
launchedstring yearYear the product launched"2016"
bountynumber $Base affiliate payout per approved account400
appRatingnumberIssuer app-store rating (used as a sentiment proxy)4.8
geonull | {states: string[], note}Geo-targeted offer flag (3 cards have one, e.g. Amex BCP: {states:["CA","NY","TX"], note:"Elevated $300 offer in 3 states"})null

20 cards seeded across 10 issuers: Amex (3), Chase (4), Capital One (4), Citi (2), Discover (2), Wells Fargo, Bank of America, U.S. Bank, Chime, Mission Lane (1 each). Spread covers $0-to-$795 fees, premium travel to secured/subprime — deliberately exercising every UI state (geo flags, no-bonus cards, 0% APR product, stale verification).

2.2 Change event (the prototype's crown jewel)

Built by ch(field, old, new, date, dir, note):

{ field: "Annual fee", old: "$550", new: "$795", date: "2026-08-25",
  dir: "bad", note: "Largest single fee increase tracked this year" }
  • field — human label of what changed ("Annual fee", "Sign-up bonus", "Purchase APR", "Lounge guest policy", "Q4 categories"…).
  • old / new — display strings; equal values with dir:"flat" represent re-verification events ("Re-verified, unchanged") — the system logs "we checked and nothing moved", which powers the freshness story.
  • dir"good" (more competitive vs. you: rendered green tag "More competitive"), "bad" (less competitive: red "Less competitive"), "flat" (gray "Re-verified").
  • note — analyst-style annotation (defaults to "Detected in issuer terms").

Alerts are not a separate model: the alert feed is all cards' changes flattened (allChanges), sorted by date desc, and classified into a type by regex on field:

alertType(field) {
  if (/bonus/i.test(field)) return "Bonus";
  if (/apr/i.test(field)) return "APR";
  if (/fee/i.test(field)) return "Fee";
  if (/rewards|category|cap|credit|program|graduation|deposit|lounge|policy/i.test(field)) return "Benefit";
  return "Terms";
}

2.3 Watchlists

{ id: "w1", name: "Our direct competitors",
  cards: ["chase-sapphire-reserve", "amex-platinum", "cap1-venture-x", "chase-sapphire-preferred"],
  email: true, slack: true, teams: false }

Three seeded lists ("Our direct competitors", "No-fee cashback field", "Subprime / builder watch"). email/slack/teams are per-list notification channel toggles. state.watchId is the active list; pinning anywhere in the app pins to the active list. New list ids are "w" + (count+1) + Date.now().toString().slice(-3).

2.4 Persona (Fit simulation)

persona: { fico: 720, goal: "rewards" | "apr" | "build", balance: 3200,
           held: ["chase-freedom-unlimited"],
           spend: { groceries: 700, dining: 450, travel: 300, gas: 180, other: 900 } }

A separate calcSpend object ({groceries: 650, dining: 400, travel: 350, gas: 150, other: 800}) drives the Rewards calculator, Compare "modeled" rows, Leaderboards, and card-detail modeled values — persona spend is only for Fit simulation.

2.5 Supplementary static datasets

  • ISSUERS — per-issuer: cfpb (trailing-12-mo complaint volume), dir (up/down/flat trend), top (top-3 complaint categories), app (app rating). E.g. "Capital One": { cfpb: 1420, dir: "up", top: ["Fraud or scam", "Billing disputes", "Credit limit changes"], app: 4.7 }.
  • EVENTS — 6 market events {type: "launch"|"change"|"sunset", name, issuer, date, note} (e.g. "Citi Strata Elite" launch, "Chime cashback partner program" sunset).
  • ADS — 6 detected ad creatives {issuer, card, channel (Paid search/Meta/YouTube/TikTok/Display), copy, tag (Fee justification/Bonus-led/Brand/Simplicity/Access/Pre-qual), date}.
  • CREATIVE — 8 landing-page/onboarding captures {issuer, card, kind, date, note}.
  • TC_DIFF — a hardcoded line-level T&C diff for Sapphire Reserve: {t: text, k: "same"|"add"|"del"} (13 lines showing the $550→$795 fee edit, AU lounge-access removal, APR bump).

2.6 Other state

view, role, detailId, detailTab, query, sort (updated|fee|apr|bonus|bounty), filters {issuer[], network[], category[], tier[]}, feeCap (0–700, 700 = "Any"), compare (≤4 ids), alertFilter, severity (all|material|neg), watchId, newWatch, calcSpend, calcYears, leaderCat, issuerId, creativeTab, creativeIssuer, diffPair, reportSections[], reportState (idle|running|done), draftId/draft (admin edit), vw (viewport width; narrow < 1180, tight < 980 collapse grids).

2.7 Shared helpers & formulas

// Deterministic pseudo-random & trend series (all charts are synthetic)
rand(seed, i) { let h = 0; for (k...) h = (h*31 + seed.charCodeAt(k)) % 9973;
                return Math.abs(Math.sin((h + i*137.13) * 0.0177)) % 1; }
series(seed, base, spread, n=12) { v = base; each step: v += (rand(seed,i) - 0.42) * spread; }
chart(vals, w, h, color, fill)  // → {points, area path, color, fill, last, first, delta} for SVG polyline + area

// THE core economics formula — modeled annual rewards value at a spend profile
annualValue(c, spend) {
  let v = 0;
  ["groceries","dining","travel","gas","other"].forEach((k) => {
    v += (spend[k] || 0) * 12 * (c.earn[k] * c.pv) / 100;
  });
  return v;
}

// Composite score used by Leaderboards
composite(c, spend) {
  const val = this.annualValue(c, spend) - c.fees.annual;      // net of fee
  const aprPen = c.apr.pmin === 0 ? 0 : (c.apr.pmin - 17) * 6; // APR penalty
  return val - aprPen + c.bonus.est * 0.25;                    // + 25% of bonus value
}

Freshness color semantics: aprColor — green if 0 or <19%, amber <22%, red otherwise. feeColor — green $0, amber ≤$150, red above. Verified: green ≤7d, red >30d ("stale"), gray between. lastChange(c) = newest change date, falling back to verified.


3. Screens

3.1 Dashboard (dash)

Purpose: morning briefing — "what moved overnight" across the tracked market.

Layout: page header (eyebrow "Market monitor · Fri, Aug 28 2026", role-driven H1, role toggle) → 5-tile stat row → "Share-steal opportunities" band → two-column grid (1.6fr / 1fr): change feed left; APR index + issuer bars right.

Components:

  1. Stat tiles (5-up; 2-up when tight):
    • Cards trackedcards.length (20), delta "+3" (hardcoded), sub "across N issuers".
    • Changes · 7 daysallChanges with days(date) <= 7; delta "+N today" (<= 1 day).
    • Unfavorable movesdir === "bad" && days <= 30, red.
    • Avg annual fee — mean of fees.annual market-wide; delta "+$18" (hardcoded).
    • Bounty indexhardcoded "$168", "+6%", sub "avg affiliate payout" (concept tile, not computed).
  2. Share-steal opportunities (amber-tinted band, "Auto-detected" badge): top 3 of vulnList() (see Signals, 3.13 for the formula) as clickable cards with an "opportunity NN" score, generated narrative ("{card} · {field} moved {old}{new}. Switch-intent window open for the next 30–60 days."), and mono signal chips.
  3. Change feed (left card, "Verified data" green badge, "Open alerts →" link): the 8 newest changes. Each row: card-art swatch, card name, competitive tag (More competitive green / Less competitive red / Re-verified gray), and the signature old→new diff line{field} · ~~old~~ → **new** with the new value colored by direction; right-aligned relative time + issuer.
  4. Market APR index (right): SVG area+line chart of series("market-apr", 19.4, 0.6, 12) clamped ≥18, last point labeled as current avg APR floor; month axis Sep 25 → Aug 26.
  5. Top issuers by cards tracked: horizontal bars per issuer — {count} · {changes-90d} chg, width = count/maxIssuer, palette-colored; click → issuer rollup.

State/interactions: role toggle; every feed row/vuln/issuer bar navigates (openCard, issuer rollup).

3.2 Alerts (alerts)

Purpose: the full change stream as a triage inbox.

Layout: header with summary line ("{n} changes · {severity} severity · 20 cards monitored") + type filter chips (All / APR / Fees / Bonuses / Benefits / Terms) → two columns (1fr / 320px).

Components:

  1. Alert list: every change (filtered), each row with a 4px left color bar (direction color), unfavorable rows get a faint red row tint (#fffbfb). Row content: card name (click → detail), issuer, type badge (direction-tinted), watchlist badge if the card is in any list, the old→new diff (mono, strikethrough old), note + source: provenance line, relative + exact date, and a Pin/Pinned button targeting the active watchlist.
  2. Filtering logic:
    if (alertFilter !== "all" && alertFilter !== alertType(field)) skip;
    if (severity === "material" && dir === "flat") skip;  // material = actual movement
    if (severity === "neg" && dir !== "bad") skip;        // unfavorable only
    
  3. Notification routing panel (right): per watchlist, three toggle buttons Email/Slack/Teams (dark when on); footer "Severity floor" select (All changes / Material only / Unfavorable only). Sub-copy: "Digest sends 8:00am ET."
  4. Alert volume · 8 weeks: SVG bar chart of series("alert-vol", 7, 5, 8) (min 2, rounded), current week bar in red.

3.3 Watchlists (watch)

Purpose: manage named card portfolios that drive alert priority and report scope.

Layout: 260px / 1fr — list nav left, active list contents right.

Components: watchlist nav rows (name, "N cards · M changes 7d", green dot if any channel enabled); "New watchlist name" input + Add button; active list header (name, "N cards · alerts to email, Slack"; "Add cards" → directory); empty state ("No cards pinned yet — Pin cards from the directory or straight off an alert."); per-card rows with swatch, name/issuer/category, three mini-stats (Annual fee colored, APR colored, Changes 30d) and Unpin.

3.4 Card directory (directory)

Purpose: the searchable/filterable card database — grid of card tiles.

Layout: header (result label + search input + sort select + "Compare N" button) → 212px / 1fr: sticky filter rail left, repeat(auto-fill, minmax(272px, 1fr)) card grid right.

Filters (filtered()): free-text query over name + issuer + category; multi-select chip groups for Issuer, Network (Visa/Mastercard/Amex/Discover), Category (5), Credit tier (3); Max annual fee range slider 0–700 step 25 (700 shows "Any"); Reset link. Sorts: last updated (default, by lastChange desc), fee ↑, APR ↑, bonus value ↓, affiliate bounty ↓. Result label: "{n} of 20 cards · sorted by {label}".

Card tile: card-art swatch (color + network wordmark), badges column ("Updated {n}d ago" amber pill when lastChange ≤ 7d; "Geo offer" blue pill when geo), category label, issuer + name, 3-stat row (Annual / Purch. APR / Bonus, mono, semantically colored), footer with ☆ Pin / "Compare +" (becomes accent "Selected ✓", tile gets accent border+ring). Whole tile opens detail; pin/compare stop propagation. Compare selection caps at 4.

3.5 Card detail (detail)

Purpose: single-card dossier; the deepest screen. Reached from anywhere via openCard(id).

Layout: "← Directory" back link → hero (large card art 112×71, issuer + category badge + optional "Geo-targeted offer" badge, H1 name, provenance line "Verified {date} · {ago} · {source} · launched {year}"; actions: Pin to watchlist, Add to compare, Edit record (jumps to Admin with draft open), Affiliate offer ↗ accent CTA) → 5 tab chips → 1fr / 336px grid: tab content left, persistent rail right.

Tabs (left column):

  • OverviewRewards structure table (per category: rate {earn}x or {earn}% — green when ≥3; progress bar pct = min(100, earn * 12); right column "{money(spend[k]*12*earn*pv/100)}/yr at modeled spend" using calcSpend); point-value note ("Points valued at 1.70¢ (CardIntel valuation, updated monthly)" vs "Cash back, valued at face value"). APR card: Purchase (range or "No interest"), Intro ("0% for N mo"), Balance transfer, Penalty — each color-coded. Fees card: Annual, Foreign transaction, Balance transfer, Late ("Up to $40"), Cash advance. Sign-up bonus 4-stat card (Bonus / Spend req. / Window / Est. value) with "Live offer" (green) / "No current offer" (gray) badge. Benefits & terms pill list.
  • Trends — two synthetic SVG area charts: "Purchase APR floor · 12 months" (series(id+"apr", pmin-1.2, 0.5) with last point forced to actual pmin; label "X% → Y%") and "Est. rewards value at modeled spend" (series(id+"val", annualValue*0.86, 60), last = actual).
  • T&C diff — the standout concept: a red/green line-level diff of two T&C captures ("Comparing captures 5/12/2026 → 8/25/2026 · 3 material edits"; legend "+ added / − removed"). Lines from TC_DIFF: added = green bg #e9f2ec, removed = red bg #fbeaea + strikethrough, mono font. (Static data — same diff shown for every card.)
  • Risk signals — two cards: CFPB complaints · {issuer} (big volume number, Rising/Falling/Flat direction colored, 12-mo synthetic chart, "Top categories" list) and App store sentiment (rating ★, chart, "Recurring themes" list — hardcoded: payment posting delays, dispute resolution time, login/2FA friction).
  • Growth intelAffiliate bounty · 12 months chart + 3 network tiles (CJ / Impact / Rakuten; per-network value = bounty * factor * (0.94 + rand(id+net, i) * 0.12) with factors 1 / 0.92 / 1.08; Rakuten labeled "Best"); Creative captures (3 placeholder thumbnails with [ landing-page-capture ] slugs + notes, filtered to this issuer); Ad creative detected (channel badge, date · tag, quoted copy).

Right rail (always visible): Change history — vertical timeline, dot colored by direction, field, date, old → new (mono), note + relative time; count in header. Credit requirement — tier label + score range over a red→amber→green gradient bar with a marker at tierPos = {excellent: 88, good: 62, fair: 34}%. Geo-targeted offer card (only when geo): note + per-state bars (pct = 90 - i*22). Simulated Fit Score beta teaser (dashed border, "Beta" badge, "Simulated output, not verified market data", CTA → Fit simulation; hidden when showBetaModules === false). Provenance card: Source / Verified / Offer link.

3.6 Compare (compare)

Purpose: side-by-side matrix of up to 4 cards with best-in-row highlighting.

Layout: header ("Side-by-side", sub "Best value per row highlighted. Modeled rows use your rewards-calculator spend profile"; Add cards / Clear) → empty state if <2 selected → sticky-first-column matrix: 208px row-label column + one flex column per card (art swatch, × remove, issuer, name → detail).

Rows (compareRows) — each has label, hint, a get(c) display fn and a sc(c) scoring fn; the max score wins, cell tinted #eef6f0 green (suppressed if all tie, or if highlightBest prop is false; rows with sc: () => null never highlight):

RowValueScore (higher wins)
Annual feemoney(fees.annual) / "no annual fee"-fees.annual
Purchase APRrange or "No interest"-apr.pmin
Intro APR"0% / N mo"introMonths
Sign-up bonusest $ + amount subbonus.est
Modeled rewardsannualValue(c, calcSpend)/yr ("gross of fee")same
Net year 1annualValue(c, calcSpend) + bonus.est − fees.annual ("first-year value")same
Foreign transaction"None" or fee0 if none else -parseFloat(ftf)
Credit tierscore range + tier labelnone
Affiliate bountymoney(bounty) "per approved account"bounty
CFPB volumeissuer volume + trend-cfpb
Last changeago(lastChange) + "N logged"none

3.7 Rewards calculator (calc)

Purpose: "Real annual dollar value at a given spend profile — net of annual fee, not headline rates." This is the prototype's effective annual value engine (the closest thing to an "effective annual fee": the Net/yr column, rewards minus fee).

Layout: 300px / 1fr — sticky spend panel left, ranking table right.

Inputs: 5 sliders (Groceries, Dining, Travel, Gas, Everything else; 0–3000 step 25, monthly) with live $X/mo labels; header shows total /mo and ×12 /yr. Footnote: "Points valued with CardIntel monthly valuations. Caps and rotating categories applied at their published limits." (aspirational — no caps in the math).

Math (rankByValue(calcSpend)), per card:

gross = annualValue(c, spend)              // Σ spend[k] * 12 * earn[k] * pv / 100
net   = gross - c.fees.annual              // "Net / yr" column (ranking key, desc)
y1    = gross + c.bonus.est - c.fees.annual // "Year 1 w/ bonus"

Table columns: # / Card (name, issuer, bar of net/maxNet, leader bar green) / Rewards (gross) / Fee (−$X) / Net / yr (green if >0, red if negative) / Year 1 w/ bonus. Rank-1 row tinted green. Rows navigate to detail.

3.8 Leaderboards (leaders)

Purpose: ranked "best card" list per category with a normalized composite score.

Layout: header + category chips (Cashback / Travel / Business / Secured / Fair credit) → single ranked table.

Math: filter to category, score by composite(c, calcSpend) (see 2.7: net value − APR penalty (pmin−17)×6 + bonus.est × 0.25), sort desc, then normalize for display:

score = Math.round(50 + (composite / maxAbsComposite) * 45)   // ~5..95 scale
pct   = max(4, |composite|/max * 100)                          // bar width
move  = rank 1 ? "▲ leader" : rand > .5 ? "▲ 1..3" : "▼ 1..3" // synthetic movement

Row: rank, card art, name/issuer, score + movement + bar (leader green, others gray), then Rewards (annualValue/yr), APR, Fee columns. Sub-header: "Composite score: modeled rewards value, APR penalty, fee drag, bonus weighting."

3.9 Issuer rollup (issuers)

Purpose: portfolio-level view per issuer — "are they loosening or tightening?"

Layout: issuer picker chips → 5 stat tiles → 1.6fr/1fr: portfolio table left; APR chart + recent movement right.

Derived (issuerRollup(name)): cards for issuer; all their changes flattened + sorted; 90-day window q; bad/good counts; Direction = bad > good ? "Tightening" (red) : good > bad ? "Loosening" (green) : "Stable" with note "{bad} unfavorable · {good} favorable · 90 days"; avg fee and avg APR floor (simple means); CFPB volume + trend + app rating from ISSUERS; synthetic 12-mo avg-APR chart.

Tiles: Cards tracked / Changes·90d / Direction / Avg fee·APR / CFPB·app★. Portfolio table: swatch, name, category, fee, APR, changes-90d ("N chg") → detail. Recent movement: last 5 changes with card, date, field: ~~old~~ → new.

3.10 Launches & sunsets (launches)

Purpose: market-entry/exit timeline. Single timeline card over EVENTS: dot + badge per type — "New launch" (green #e6f1e9), "Discontinued" (red #fbeaea), "Major change" (amber #fdf3e2) — name, date + relative, "{issuer} · {note}". Static list; no interactions.

3.11 Affiliate bounties (bounty)

Purpose: payout-per-approved-account tracking across affiliate networks. Sub-copy: "Modeled from observed network rate cards — directional, not contractual."

Table (bountyRows(), sorted by bounty desc): Card / CJ / Impact / Rakuten / Best / 12-mo trend. Per-network value = bounty * {CJ:1, Impact:0.92, Rakuten:1.08} * (0.94 + rand(id+net, j)*0.12); the max cell is green-highlighted and repeated in Best. Trend: sparkline of series(id+"bty", bounty*0.82, 22) with last = actual, plus Δ% = (last-first)/first colored green/red. Rows → detail.

3.12 Creative & ads (creative)

Purpose: competitor marketing surveillance. Two tabs: Landing pages & onboarding (default) and Ad creative monitor, plus an issuer filter chip row (All + issuers).

  • Screens tab: card grid of CREATIVE captures — placeholder thumbnail (striped background with [ landing-page-capture ] mono slug), issuer + date, "card · kind", and the analyst note ("Fee increase messaging added above the fold").
  • Ads tab: table of ADS — issuer/card, quoted copy, channel badge + strategy tag badge (blue), date + relative.

3.13 Sentiment & CFPB (signals)

Purpose: risk/sentiment signals joined to tracked cards; feeds the dashboard's share-steal band.

Components:

  1. Share-steal opportunity flags — full vulnList(). The opportunity score:
    bad   = changes where dir === "bad" and days(date) <= 45          // recent unfavorable moves
    score = bad * 30
          + (ISSUERS[issuer].dir === "up" ? 25 : 0)                    // CFPB volume rising
          + (c.appRating < 4.6 ? 20 : 0);                              // weak app sentiment
    score = min(97, score);  keep only score >= 45;  sort desc;
    
    Signals chips are generated from the same three conditions ("2 unfavorable changes", "CFPB volume rising", "App rating 4.2"). Row: card/issuer, narrative text ({field} {old} → {new} · complaint volume rising · app rating N), score + bar (red > 70, else amber).
  2. CFPB complaint volume by issuer — horizontal bars sorted desc, ▲ rising (red bar) / ▼ falling / – flat, "Top:" categories line; click → issuer rollup.
  3. App store rating trend — first 6 cards: sparkline (series(id+"app", rating-0.2, 0.1), last = actual), rating★, delta ±0.NN colored, and a dominant complaint theme cycled from a fixed 6-item list.

3.14 Fit simulation (fit) — Beta

Purpose: rank the field for a synthetic consumer persona; explicitly disclaimed: "Beta · simulated data … Phase 2 replaces the synthetic profile with licensed bureau data." Whole page tinted #f7f8fb with slate-blue chrome (#45495c) to look visually distinct from verified-data screens.

Persona builder (left, sticky): FICO slider 560–820 (label "720 · Good"), avg revolving balance slider 0–12000 ("$3,200 avg revolving"), Primary goal select (Maximize rewards / Carry a balance cheaply / Build-rebuild credit), 5 monthly-spend sliders.

Switch/churn propensity card:

score = round(min(94, 30 + balance/120 + (fico >= 700 ? 14 : 4) + held.length * 6));
label = score > 70 ? "Actively shopping" : score > 50 ? "Receptive" : "Low intent";

Big score + label + bar, and 4 signal rows with weights (+22 revolving balance above median, +14 portfolio gap, +14/+4 FICO, +9 no recent account opening — display-only strings).

Predicted fit ranking (fitRows(), top 8; excludes excellent-tier cards when fico < 700):

val      = annualValue(c, persona.spend) - c.fees.annual;
elig     = clamp((fico - tierMin(tier) + 60) / 120, 0, 1);            // tierMin: 740/670/580
aprRisk  = balance > 1500 && pmin > 22        ? -0.18
         : balance > 1500 && introMonths >= 15 ?  0.12 : 0;
goalBoost= goal === "rewards" ? 0
         : goal === "apr"     ? (introMonths >= 15 ? 0.14 : -0.05)
         : /* build */          (tier === "fair" ? 0.16 : -0.04);
raw      = elig * 0.4 + aprRisk + goalBoost + clamp(val / 900, 0, 0.6);
// display: normalize to leader
score    = round(clamp((raw / topRaw) * 94, 38, 97));

Each row carries a generated rationale sentence: earn strength on the persona's top spend category, "0% for N mo fits a revolving balance" / "High APR is a risk given the modeled balance", and "Approval odds thin at FICO N" when tierMin > fico. "Currently held" badge for held cards. Score bar green ≥80, amber ≥60, gray below.

3.15 Reports (reports)

Purpose: compile a competitive snapshot for internal decks; "Every figure carries its source and verified date."

Components: section checklist (Alerts summary 30d / Rewards benchmark / Issuer comparison / Risk & sentiment signals / Fit simulation preview beta) — toggling resets state to idle; footer "Est. {6 + 3×sections} pages · watchlist scope" + accent Generate button. Right panel is a 3-state machine: idle empty state → running (1.4s setTimeout, skeleton progress bars, "Compiling alerts, benchmarks, and provenance footnotes…") → done ("Snapshot ready", 3 download rows: CardIntel_Competitive_Snapshot_Aug2026.pdf 2.4 MB, .pptx 5.1 MB, alerts_export_30d.csv 88 KB).

3.16 API & export (api)

Purpose: sell the data-out story. Components: API keys table (name, masked key ci_live_9f4a••••••••e21b, scope read:cards read:alerts, last used, Rotate); Endpoints list — GET /v1/cards, GET /v1/cards/{id}/changelog, GET /v1/alerts?since=, GET /v1/issuers/{name}/rollup, POST /v1/simulate/fit (beta); CSV export sample (mono block); JSON sample for GET /v1/cards/:id on a dark code panel — notable: the API sample proposes cleaner production field names (purchase_min, spend_requirement, est_value, change_log[].severity: "material", last_verified_date, source: "issuer_tc_page") — a useful head start for Frankie's real schema.

3.17 Data management (admin)

Purpose: manual entry/verification workflow "until issuer feeds land. Every edit appends a change-log entry." Header stat: "20 records · N stale (>30d)".

Components: records table (Card+issuer·network / Category / Annual / APR / Source / Verified — relative, green ≤7d, red >30d / Edit) → clicking opens a sticky Edit record side panel (grid becomes 1fr / 316px) with 10 text fields (name, issuer, network, category, annual fee, APR min/max, bonus, source, verified). Save & log change (saveDraft): diffs annual fee, APR range, and bonus against the record; for each difference appends {field, old, new, date: today, dir: fee/APR up = "bad" else "good" (bonus = "flat"), note: "Manual edit by Dana Reyes"} to the card's changes and re-stamps verified to today — so manual edits flow straight into the alert feed. Footnote states exactly this.


4. Design system (as observed in the prototype)

Typography. Two-family system: IBM Plex Sans (Helvetica fallback) for all prose/UI; IBM Plex Mono for numerals, data values, dates, badges, eyebrows, code, and axis labels — the mono treatment is what gives the product its "terminal for card data" character. Weights 400/500/600. Scale: H1 24–27px/600/−0.025em; card titles 13.5px/600; body 12–13px; sub/meta 10.5–11.5px; mono data 12–25px (stat tiles 25px); overline labels 9.5–10.5px mono, uppercase, letter-spacing 0.06–0.1em.

Color.

  • Surfaces: page #f4f4f2, sidebar #fbfbfa, cards #fff, hover #fafaf8, zebra #fdfdfc, inset #f1f1ed/#f4f4f0/#f6f6f3.
  • Borders: #e4e4df (card), #ecece8 (card header), #f1f1ed/#f4f4f0 (row dividers), #e0e0da (inputs).
  • Ink: #16171b (primary, also selected-chip fill); text grays #33353c#5d6067#6f7178#9a9c9f#a4a6aa/#b4b6ba.
  • Accent: #2a4fb8 (prop-overridable) — links, compare selection, watchlist badges, primary CTAs; tint #eef1fb, border tint #d9dbe6.
  • Semantic (oklch): GOOD oklch(0.5 0.12 150) green, BAD oklch(0.53 0.17 25) red, WARN oklch(0.6 0.12 80) amber; darker tag-text variants oklch(0.45 0.11 150) / oklch(0.48 0.15 25). Tint backgrounds: green #e6f1e9/#eef6f0/#e9f2ec, red #fbeaea/#fffbfb/#f7ecec, amber #fdf3e2/#fdf0e2/#f6e7d6, gray #f1f1ed.
  • Beta/simulated: slate family — bg #f7f8fb/#f8f9fc, border #d9dbe6/dashed #c4c8d8, ink #45495c, badge #e3e6f2/#4a5170. Simulated features are visually quarantined from verified data.
  • Card-art palette: per-card muted deep hues (#1d3557, #33333a, #4a3b6b, #8a4b2a, …) + logo gold #e8b658.

Shape & spacing. Radii: 12px cards/tiles, 7–9px buttons/chips/inputs, 4–5px badges and card-art swatches, 20px pills (benefits, nav badges). Page padding 30px 38px 64px (26/24/60 narrow); grid gaps 12–18px; card padding 15–18px. Sidebar 226px sticky; filter rails and edit panels sticky at top: 20px.

Recurring patterns. (1) Chip/segmented filter: white bordered chip, selected = solid #16171b with #fbfbfa text. (2) Overline+value stat tile (mono uppercase label, big mono number, colored delta, gray sub). (3) Old→new diff: strikethrough gray old, arrow, bold direction-colored new — used in feed, alerts, change history, issuer rollup. (4) Competitive tag badges: mono uppercase 9.5px on tinted backgrounds. (5) Inline progress/score bars 4–7px tall on #f1f1ed tracks. (6) SVG sparkline/area charts (preserveAspectRatio: none, 2px polyline over soft area fill) with mono month-axis labels. (7) Card-art swatch as universal entity avatar. (8) Left color-rail severity on alert rows; timeline dot+line for histories.


5. MVP mapping (prototype → Frankie)

Frankie MVP scope: Dashboard (market overview), Card database table, Card detail, Compare, Issuers, Market changes feed, AI analyst stub. Deferred: alerts/watchlists/calculator/fit simulation/affiliate/creative/sentiment.

Prototype screenMVP dispositionRationale
DashboardKeepDirect MVP match; keep stat tiles + change feed + issuer bars; drop role toggle and share-steal band to a later phase (band can return with real signal data).
Change feed (dashboard component)Keep — highest valueThe old→new diff row with More/Less-competitive tags and re-verification events is the product's core differentiator; make it the Market changes feed screen.
Card directoryAdaptBecomes the Card database table; keep search/filters/fee-cap/sorts, render as a table rather than tile grid for MVP density (tiles are a nice later view mode).
Card detailAdaptKeep Overview + change-history rail + provenance + credit-tier bar; defer Trends (synthetic), Growth intel, Risk signals tabs; T&C diff tab is high-value once real captures exist.
CompareKeepIn MVP scope; keep best-in-row highlighting and Net-year-1 math (swap calcSpend for a default spend profile since the calculator is deferred).
Issuer rollupKeepIn MVP scope as Issuers; the Tightening/Loosening direction stat computed from 90-day change directions is cheap and compelling.
Launches & sunsetsAdaptFold into the Market changes feed as event types (launch/sunset/major change) instead of a separate screen.
AlertsDeferNeeds notification infra; MVP change feed covers read-only triage. Keep the type-regex classification and severity floor concepts for later.
WatchlistsDeferValuable retention feature but not needed to prove the data; pin-to-list touches many screens, so design the data model early.
Rewards calculatorDeferDeferred per MVP; but annualValue() is trivial and powers Compare's modeled rows — keep the function, not the screen.
LeaderboardsDeferComposite score is opinionated/arbitrary; revisit after real point-valuation data.
Affiliate bountiesDeferAffiliate data sourcing is a separate business problem; strong later monetization angle.
Creative & adsDeferRequires a capture pipeline; keep the "analyst note per capture" pattern.
Sentiment & CFPBDeferCFPB data is genuinely public/free — a strong fast-follow; opportunity-score formula is a good starting heuristic.
Fit simulation (beta)DeferExplicitly beta/synthetic in the prototype; keep the visual quarantine pattern for any future simulated feature.
ReportsDeferReplace with the AI analyst stub in MVP (the report generator is the natural AI-analyst surface later).
API & exportDefer (screen), keep (schema)The JSON sample's field naming (change_log[].severity, last_verified_date, source enums) should seed Frankie's actual API/schema design.
Data management (admin)Adapt (minimal)Some ingest/edit surface is needed on day one; the "every manual edit appends a change-log entry + re-stamps verified" rule is essential and cheap.

Highest-value prototype ideas to carry forward

  1. Field-level change events with old→new diffs, direction ("More/Less competitive"), analyst note, and re-verification entries — the entire product hangs off this one data structure.
  2. Provenance + freshness as first-class UI (source, verified date, stale >30d flags, "Live coverage %" in the sidebar) — trust is the sales pitch for scraped/managed data.
  3. T&C capture diffing (red/green line diff between dated captures of issuer terms) — unique, defensible, and demo-magic.
  4. Issuer-level Tightening/Loosening direction derived purely from counting good/bad changes in a 90-day window — a headline insight at near-zero cost.
  5. Simulated-vs-verified visual quarantine (slate-blue beta styling + "Simulated" badges) — an honest-UI pattern Frankie should adopt for any AI/modeled output, including the AI analyst stub.

Also worth stealing: the manual-edit-appends-changelog admin rule, the alertType regex taxonomy (APR/Fee/Bonus/Benefit/Terms), and net-year-1 math (rewards + bonus − fee) for Compare.