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).
| Group | Views (state key → label) |
|---|---|
| Monitor | dash Dashboard · alerts Alerts · watch Watchlists · directory Card directory · compare Compare |
| Benchmark | calc Rewards calculator · leaders Leaderboards · issuers Issuer rollup · launches Launches & sunsets |
| Growth intel | bounty Affiliate bounties · creative Creative & ads |
| Risk signals | signals Sentiment & CFPB |
| Beta | fit Fit simulation |
| Workspace | reports 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:
| Field | Type | Meaning | Example (Chase Sapphire Reserve) |
|---|---|---|---|
id | string slug | Unique key, issuer-shortname style | "chase-sapphire-reserve" |
issuer | string | Issuing bank/fintech (10 issuers seeded) | "Chase" |
name | string | Marketing name of the card | "Sapphire Reserve" |
network | enum | Visa | Mastercard | Amex | Discover | "Visa" |
category | enum | travel | cashback | business | secured | fair credit | "travel" |
tier | enum | Recommended credit tier: excellent (740–850) | good (670–739) | fair (580–669) | "excellent" |
color | hex | Card-art swatch color used for the fake card graphic everywhere | "#1d3557" |
apr.pmin / apr.pmax | number % | Purchase APR range floor/ceiling; pmin === 0 renders as "No interest" (Chime) | 21.49 / 28.49 |
apr.introMonths | number | 0% intro-APR period in months (0 = none) | 0 |
apr.bt | number % | Balance-transfer APR (0 = N/A) | 21.49 |
apr.penalty | number % | Penalty APR (0 = none) | 29.99 |
bonus.amount | string | Headline sign-up bonus as marketed ("100,000 pts", "$200", "Cashback Match", "None") | "100,000 pts" |
bonus.spend | string | Spend requirement, display string ("$5,000", "—") | "$5,000" |
bonus.timeframe | string | Qualification window ("3 months", "12 months", "—") | "3 months" |
bonus.est | number $ | CardIntel's estimated dollar value of the bonus; 0 = no current offer | 2000 |
earn.groceries/dining/travel/gas/other | number | Earn rate per category (points multiplier or cashback %) | {1, 3, 8, 1, 1} |
pv | number | Point value in cents ("CardIntel valuation, updated monthly"). 1 = cash back at face value; >1 renders rates as "Nx" instead of "N%" | 1.7 |
fees.annual | number $ | Annual fee (the annual arg is copied into fees.annual) | 795 |
fees.ftf | number|string | Foreign transaction fee; 0 = "None", else a display string like "3%" | 0 |
fees.bt | string | Balance-transfer fee schedule, display string ("5% ($5 min)", "3% for 120d, then 5%", "Not offered") | "5% ($5 min)" |
fees.late | number $ | Late fee (0 = none — Discover) | 40 |
fees.ca | string | Cash-advance fee, display string | "5% ($10 min)" |
benefits | string[] | 3 marketing benefit bullets, rendered as pill tags | ["$300 travel credit", "Priority Pass access", "Primary rental coverage"] |
changes | Change[] | Field-level change log, newest first (see 2.2) | 3 entries |
status | string | Always "live" in seed | "live" |
link | url | Affiliate offer link | https://example.com/offers/csr |
source | enum-ish string | Data provenance: "Issuer T&C page", "Affiliate feed", "Manual entry", "Issuer help center" | "Issuer T&C page" |
verified | ISO date | Last human/robot verification date; drives freshness/staleness badges | "2026-08-28" |
launched | string year | Year the product launched | "2016" |
bounty | number $ | Base affiliate payout per approved account | 400 |
appRating | number | Issuer app-store rating (used as a sentiment proxy) | 4.8 |
geo | null | {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 withdir:"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/flattrend),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:
- Stat tiles (5-up; 2-up when tight):
- Cards tracked —
cards.length(20), delta "+3" (hardcoded), sub "across N issuers". - Changes · 7 days —
allChangeswithdays(date) <= 7; delta "+N today" (<= 1day). - Unfavorable moves —
dir === "bad" && days <= 30, red. - Avg annual fee — mean of
fees.annualmarket-wide; delta "+$18" (hardcoded). - Bounty index — hardcoded
"$168", "+6%", sub "avg affiliate payout" (concept tile, not computed).
- Cards tracked —
- 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. - 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 competitivegreen /Less competitivered /Re-verifiedgray), and the signature old→new diff line —{field} · ~~old~~ → **new**with the new value colored by direction; right-aligned relative time + issuer. - 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. - 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:
- 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. - 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 - 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."
- 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):
- Overview — Rewards structure table (per category: rate
{earn}xor{earn}%— green when ≥3; progress barpct = min(100, earn * 12); right column"{money(spend[k]*12*earn*pv/100)}/yr at modeled spend"usingcalcSpend); 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 actualpmin; 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 intel — Affiliate 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):
| Row | Value | Score (higher wins) |
|---|---|---|
| Annual fee | money(fees.annual) / "no annual fee" | -fees.annual |
| Purchase APR | range or "No interest" | -apr.pmin |
| Intro APR | "0% / N mo" | introMonths |
| Sign-up bonus | est $ + amount sub | bonus.est |
| Modeled rewards | annualValue(c, calcSpend)/yr ("gross of fee") | same |
| Net year 1 | annualValue(c, calcSpend) + bonus.est − fees.annual ("first-year value") | same |
| Foreign transaction | "None" or fee | 0 if none else -parseFloat(ftf) |
| Credit tier | score range + tier label | none |
| Affiliate bounty | money(bounty) "per approved account" | bounty |
| CFPB volume | issuer volume + trend | -cfpb |
| Last change | ago(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
CREATIVEcaptures — 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:
- Share-steal opportunity flags — full
vulnList(). The opportunity score:
Signals chips are generated from the same three conditions ("2 unfavorable changes", "CFPB volume rising", "App rating 4.2"). Row: card/issuer, narrative text (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;{field} {old} → {new} · complaint volume rising · app rating N), score + bar (red > 70, else amber). - CFPB complaint volume by issuer — horizontal bars sorted desc,
▲ rising(red bar) /▼ falling/– flat, "Top:" categories line; click → issuer rollup. - 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, BADoklch(0.53 0.17 25)red, WARNoklch(0.6 0.12 80)amber; darker tag-text variantsoklch(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 screen | MVP disposition | Rationale |
|---|---|---|
| Dashboard | Keep | Direct 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 value | The 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 directory | Adapt | Becomes 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 detail | Adapt | Keep 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. |
| Compare | Keep | In 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 rollup | Keep | In MVP scope as Issuers; the Tightening/Loosening direction stat computed from 90-day change directions is cheap and compelling. |
| Launches & sunsets | Adapt | Fold into the Market changes feed as event types (launch/sunset/major change) instead of a separate screen. |
| Alerts | Defer | Needs notification infra; MVP change feed covers read-only triage. Keep the type-regex classification and severity floor concepts for later. |
| Watchlists | Defer | Valuable retention feature but not needed to prove the data; pin-to-list touches many screens, so design the data model early. |
| Rewards calculator | Defer | Deferred per MVP; but annualValue() is trivial and powers Compare's modeled rows — keep the function, not the screen. |
| Leaderboards | Defer | Composite score is opinionated/arbitrary; revisit after real point-valuation data. |
| Affiliate bounties | Defer | Affiliate data sourcing is a separate business problem; strong later monetization angle. |
| Creative & ads | Defer | Requires a capture pipeline; keep the "analyst note per capture" pattern. |
| Sentiment & CFPB | Defer | CFPB data is genuinely public/free — a strong fast-follow; opportunity-score formula is a good starting heuristic. |
| Fit simulation (beta) | Defer | Explicitly beta/synthetic in the prototype; keep the visual quarantine pattern for any future simulated feature. |
| Reports | Defer | Replace with the AI analyst stub in MVP (the report generator is the natural AI-analyst surface later). |
| API & export | Defer (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
- 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.
- 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.
- T&C capture diffing (red/green line diff between dated captures of issuer terms) — unique, defensible, and demo-magic.
- Issuer-level Tightening/Loosening direction derived purely from counting good/bad changes in a 90-day window — a headline insight at near-zero cost.
- 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.