The data model, in full

What is in the card catalog

Every number on this site comes out of one hand-maintained file. This is that file, field by field — including the fields that exist only to stop the site claiming something it cannot back.

Reviewed: Data verified 2026-09-15 · Credit Upside editorial · Updated 2026-09-16 · How we rank

The card catalog, field by field

comes out of: 286 cards, 907 earn rules, 234 statement credits, maintained by hand. This document describes it completely, including the fields that exist only to stop the site claiming something it cannot back.

It is published for the same reason the point values are. The moat here is not the data — anyone can read an issuer's page — it is knowing *when each row was last checked and against what*. A schema you can read is how you judge whether to believe the numbers.

Counts below are as of 2026-09-14 and move with the catalog; the shapes do not.


The file

{
  "schemaVersion": "1.1",
  "generatedAt": "…",
  "source": "…",
  "pointValues": { "MR": 0.011, "UR": 0.0125, "CASH": 0.01, … },
  "cards": [ … ]
}

dollar figure on the site is points × pointValues[currency]. The values are deliberately mid-range and published at /points; npm run points-check compares them monthly against One Mile at a Time and reports, never applies.

The file is stored ASCII-escaped (\uXXXX, not literal UTF-8). Anything that rewrites it must keep that, or a one-field edit churns 274 unrelated lines.


A card

Every card carries all of these:

fieldtypemeaning
idstringStable slug, issuer_card_name. Also the URL: /cards/<id>. Never reused.
namestringThe issuer's own name for the card, including ®/.
issuerstringWho underwrites it, not who it is branded for.
networkstringVisa / Mastercard / American Express / Discover.
currencystringReward currency key into pointValues (MR, UR, CASH, HILTON…).
annualFeenumberIn feeCurrency, or USD when absent. 0 means no fee.
fxFeenumberForeign transaction fee as a fraction: 0.027 is 2.7%. 0 is none.
colorstring#rrggbb for the card chip. Validated before it reaches any style.
cardTypestringpersonal or business.
statusstringopen (accepting applications) or discontinued (still scored for people who hold one, never recommended).
countrystringUS, CA, MX. Guides are per-country; scoring is not.
earnarrayThe earn rules. See below.
creditsarrayStatement credits. See below.
perksarray of stringsNon-cash benefits, in the issuer's words. Never valued in dollars.
verifiedDatedateThe day the full terms were last re-read. The card page prints it.
sourcesarray of URLsWhere the rows came from. Checked for link rot by npm run source-check.

Optional, and each one exists to prevent a specific wrong claim:

fieldwhen presentwhy it exists
officialUrl275 cardsThe issuer's own page. offer-check reads it.
annualFeeUnknownflagThe fee is not published. Better than guessing zero.
isRetailCobrandflagStore cards are excluded from general rankings.
baseRateUnknown38 cardsThe issuer does not publish a base rate. Charges falling through to it are scored as unknown, not as zero — otherwise every ordinary purchase on such a card looks like a total loss.
feeCurrency37 cardsNon-USD fee (CAD, MXN). The fee is not silently converted.
shortName22 cardsA shorter name only where the issuer itself uses one. Never invented.
conversionNotes102 cardsWhat a downgrade/upgrade path actually does.
welcomeBonusText96 cardsThe offer in the issuer's words, kept alongside the parsed figure.
signup98 cardsThe parsed welcome offer. See below.
conditionalCredits4 cardsCredits that depend on spend or status; worth $0 until the holder says otherwise.
offerVaries3 cardsThe bonus is targeted and differs per applicant.
membershipRequired2 cardsNeeds a separate membership (AAA, a credit union).
correctedOndate, when setThe day our own earn or credit rows were fixed without a full re-read (so verifiedDate did not move). When it changes, the /changes/ diff records none of that card's earn or credit differences, which were our error rather than the issuer's move. A change to verifiedDate does the same. Fees, offers and status are still recorded. Set it on every such fix. Change alerts email cardholders from that log.

earn — 907 rules

{ "category": "groceries", "rate": 4, "cap": 25000, "capPeriod": "year",
  "note": "…", "portalOnly": false, "brand": null }
fieldmeaning
categoryOne of the taxonomy's keys, or * for the catch-all base rate, or ROTATING / CHOICE.
ratePoints (or percent, for cash cards) per dollar.
capSpend ceiling in dollars before the rate drops to base.
capPeriodyear, quarter, month, anniversary.
noteThe issuer's qualification, verbatim where it matters.
portalOnlyThe rate applies only through the issuer's travel portal. 98 rules.
brandThe rate applies only at this brand.
choice / choiceOptionsThe holder picks the category (Custom Cash, Venmo). 17 rules.
rotating / quarters5%-style quarterly categories, keyed 2026Q4 → list of categories. 5 rules.
quartersAnnouncedThroughThe last quarter the issuer has announced. Past it, the rate is unknown, not base.

portalOnly and brand are load-bearing. A 5x rule that only pays through a portal, ranked as if it paid everywhere, puts the wrong card at #1. That has shipped three times; npm run catalog-check now fails the deploy on a rule of 3x or more that names a brand or portal in its note without the flag.


credits — 234 statement credits

{ "id": "amex_platinum-airline-fee-credit", "label": "Airline Fee Credit",
  "amount": 200, "period": "year", "cadence": "quarterly",
  "categories": ["flights"], "enrollmentRequired": true,
  "automatic": false, "resetsOn": "anniversary", "endsOn": "2027-12-31" }
fieldmeaning
amountFace value per period.
periodAlmost always year — the denominator for amount.
cadenceHow it is actually released: monthly, quarterly, semiannual, annual, or a multi-year cadence. This is what decides its real worth: a $10/month credit is twelve separate $10 chances, and one December dinner cannot backfill eleven missed months.
categoriesWhere it can be spent, in taxonomy keys.
enrollmentRequiredThe holder must opt in, or it pays nothing. 93 credits.
automaticPosts without a matching purchase. 40 credits.
resetsOnanniversary when the year runs from the open date, not the calendar. 32 credits — these are excluded from /credits-reset/, because the reset date depends on when *you* opened the card.
endsOnA promotional credit with a published end date. 35 credits. Past it the engine counts it as zero and catalog-check fails the deploy.

Credits are never summed and presented as value. The app allocates them against the holder's own charges, narrowest first, so two credits cannot be paid out of the same dollar — which is how a $795 card ends up "worth" $1,800 elsewhere.


signup — 98 welcome offers

{ "bonusPoints": 100000, "spendReq": 8000, "months": 6,
  "offerVerifiedDate": "2026-09-14", "offerEnds": "2026-11-04",
  "note": "…", "checkNote": "…" }
fieldmeaning
bonusPointsThe bonus in the card's own currency. Cash offers are stored in cents-as-points: a $300 bonus is 30000 with currency: "CASH".
spendReq / monthsWhat it takes to earn it.
offerVerifiedDateThe day the figure was confirmed on the issuer's page. A smaller claim than verifiedDate and deliberately a separate field — conflating them once made 41 cards publish a verification nobody had performed. catalog-check fails a deploy on an offer older than 60 days.
offerEndsThe issuer's own end date. 13 offers. Past it the bonus counts as zero, the guides stop ranking it, and the deploy gate fails.
noteThe caveat that travels with the number: "as high as", "plus a Free Night Reward", "first tier of up to 115,000".
checkNoteWhy an automated check cannot read this one — Amex renders client-side, Citi injects from an API, AAA is behind a bot wall. 26 offers.

A bonus is never counted in savings math. It is a year-one event, stated on its own, and a bonus you do not clear is worth zero.


What is deliberately not in the file

ranking code cannot read it. Today no link pays anything and the footer says so.

not have the data, and inventing it is how a comparison site becomes an ad.


Keeping it honest

commandwhat it checks
npm run catalog-checkFreshness, portalOnly violations, offers past 60 days, credits and offers past endsOn/offerEnds. Gates every deploy.
npm run offer-checkIs each welcome bonus still on the issuer's page?
npm run offer-check-browserThe same, for pages only a browser can read.
npm run source-checkLink rot across every card's sources.
npm run points-checkOur point values against One Mile at a Time.
npm run offers-checkThe card-linked offers feed.
npm run monthlyAll of the above, into one dated report.

Adding a card does not need a deploy: insert a row into D1 catalog_extra and the Worker serves it merged within about five minutes.


*Corrections are welcome and land in the same place everything else does: the report link at the foot of any card page.*

How we value points · What changed · The card database · Open data and feeds