nerdwallet.com

compare-credit-cards

Installation

Adds this website's skill for your agents

 

Summary

Search and compare credit cards on NerdWallet (category, card name, full URL, or free-form criteria) and return structured per-card data — rating, fees, intro APR, welcome bonus, rewards, pros/cons, key benefits, and the affiliate Apply Now URL (captured, never followed). Read-only.

SKILL.md
277 lines

NerdWallet Credit Card Comparison

Purpose

Search and compare credit cards on NerdWallet given (a) a full NerdWallet credit-card URL, (b) a category intent like "best travel cards" / "cash back cards" / "0% intro APR cards", (c) a specific card name (returns that one card's detail), or (d) free-form criteria ("travel card with no foreign transaction fee"). Returns a list of cards — name, issuer, network, star rating, fees, intro APR, welcome bonus, rewards structure, pros/cons, key benefits, "Apply Now" affiliate URL (captured, not followed), and canonical review URL — plus editorial context (NerdWallet's "Why we like it" blurb, "Best for…" tag, last-reviewed timestamp). Read-only — never clicks Apply Now, Apply, Get Started, Sign In, or submits an application form.

When to Use

  • "Show me the best travel credit cards on NerdWallet."
  • "Compare NerdWallet's top no-annual-fee cards."
  • "What's the NerdWallet rating + intro APR offer on Chase Sapphire Preferred?"
  • A scheduled rescore that diffs today's NerdWallet category page vs. yesterday's snapshot.
  • Aggregating editorial pros/cons across issuers to feed a card-recommendation pipeline.
  • Anywhere a downstream agent needs structured card data + editorial commentary without re-implementing the NerdWallet review schema by hand.

Workflow

NerdWallet's category pages render the card grid into static-after-hydration HTML — the data is there, but the document is large (typical category page > 1 MB, occasionally 2 MB+) and there is no public JSON or GraphQL API: every internal endpoint hinted at by robots.txt (/CreditCardDetailAJAX, /compareajax, /structured-content-renderer, /api/*) is locked to internal callers or returns 405/404 from the public surface (verified 2026-05-18 — see Site-Specific Gotchas). JSON-LD blocks on category pages describe the page (Organization, VideoObject) not the cards, so the only path to per-card data is to parse the rendered card grid. Therefore the recommended method is browser via Browserbase — bb fetch is unsuitable because every category page exceeds the 1 MB fetch ceiling.

  1. Resolve the input to a canonical URL.

    • Full URL given — if it starts with https://www.nerdwallet.com/credit-cards/best/... or https://www.nerdwallet.com/credit-cards/reviews/..., use as-is. If it starts with the deprecated https://www.nerdwallet.com/best/credit-cards/... form, don't rewrite it manually — NerdWallet's CDN emits a 301 to the new canonical (/credit-cards/best/{slug}); pass --allow-redirects (or let the browser follow) and read the final URL.

    • Category intent — map to the canonical slug under /credit-cards/best/{slug}:

      IntentSlug
      best travel cardstravel
      cash back cardscash-back
      0% intro APR / low-interest cardslow-interest
      balance-transfer cardsbalance-transfer
      student / college cardscollege-student
      business cards(use /credit-cards/small-business/... — different tree)
      secured cardssecured
      no-annual-fee cardsno-annual-fee
      no foreign-transaction-fee cardsno-foreign-transaction-fee
      rewards cardsrewards
      premium / luxury cardspremium
      bonus-offer / welcome-bonus cardsbonus-offers
      airline (generic)united-airlines-cards / delta-airlines-cards / american-airlines-cards / southwest-airlines-cards / alaska-airlines-cards (issuer-cobrand-specific — pick by issuer)
      hotel (generic)hotel (or marriott-bonvoy-cards / hilton-hotels for chain-specific)
      excellent creditexcellent-credit
      good creditgood-credit
      fair creditfair-credit
      bad / limited creditbad-credit / no-credit
      groceries / dining / gas / streaminggroceries / restaurants / gas / streaming-services
      lounge access / TSA PreCheckairport-lounge-access / tsa-precheck-global-entry
      Chase / Amex / Capital One / Citi / Discover / BofA / Wells Fargo / US Bank / Navy Federalchase-cards / american-express-cards / capital-one-cards / citi-cards / discover-cards / bank-of-america-cards / wells-fargo-cards / us-bank-cards / navy-federal-cards
      Visa / Mastercardvisa-cards / mastercard-cards

      The full enumeration of valid slugs is published in NerdWallet's WordPress sitemap at https://www.nerdwallet.com/sitemaps/us/wp-sitemap-posts-credit-cards-pages-1.xml (~80 category slugs as of 2026-05-18). If the intent doesn't obviously map, fetch the sitemap with bb fetch (XML, ~13 KB, well under the fetch ceiling), grep <loc>...best/...</loc> for the closest slug, and use it.

    • Specific card name — convert to the review-page slug https://www.nerdwallet.com/credit-cards/reviews/{slug} where {slug} is the canonical NerdWallet review slug (kebab-case, e.g. chase-sapphire-preferred, citi-double-cash, american-express-platinum, discover-it-cash-back). The full set is enumerated in the same credit-cards sitemap above (~180 review slugs as of 2026-05-18). If the name is ambiguous (e.g. "Chase Freedom" matches chase-freedom, chase-freedom-unlimited, chase-freedom-flex), return all matches and let the caller disambiguate.

    • Free-form criteria — pick the closest canonical category slug from the table above, then post-filter the parsed result list client-side by the explicit criteria. (NerdWallet's UI exposes filter chips but they're query-param-driven only on a handful of pages — see Site-Specific Gotchas.)

  2. Open a remote Browserbase session with proxies. --proxies is generally sufficient — Cloudflare on NerdWallet is light. Add --verified only after a confirmed 403 on the canonical URL:

    SID=$(bb sessions create --keep-alive --proxies | jq -r .id)
    export BROWSE_SESSION="$SID"
    browse open "https://www.nerdwallet.com/credit-cards/best/{slug}" --remote
    browse wait load --remote
    browse wait timeout 2500 --remote   # card-grid hydration after `load`
    
  3. Parse the card grid. Use browse get markdown body for fast structured extraction, or browse snapshot if you need ref-driven interaction (e.g. expanding a "Show details" accordion). Per-card extraction targets, in priority order:

    • Card nameh2/h3 inside each card row; also data-testid="product-card-title" on most templates.
    • Issuer — derived from the card name (e.g. "Chase ...", "American Express ...", "Capital One ...") or from the review-page slug prefix.
    • Network — surfaces in the card-detail rates table near the bottom of each card row ("Network: Visa" / "Mastercard" / "American Express" / "Discover"). On a few cobrand pages it's absent — fall back to inferring from the issuer + card art.
    • NerdWallet star rating — the decimal next to "★" or aria-label="Rated X out of 5 stars" near the card title. Always parse the aria-label rather than the visible text — visible text is rendered as a sprite at certain breakpoints.
    • Editorial blurb / "Why we like it" — first paragraph under the rating, usually 1–2 sentences. There is also a longer "Why we don't" / cons-rationale paragraph further down. Capture both.
    • "Best for…" tag — short pill above the card title (e.g. "Best for travel rewards", "Best for cash back"). Optional — only present on category pages, not review pages.
    • Card art URL<img src="..."> on the card image. NerdWallet hosts on www.nerdwallet.com/cdn/... and www.nerdwallet.com/tachyon/.... Capture full absolute URL.
    • Annual fee, Intro APR (purchases / balance transfers), Regular APR, Welcome bonus, Rewards rate, Foreign transaction fee, Balance transfer fee, Late payment fee — all live in the card's "Rates & Fees" / "Quick Facts" table. Field labels are stable across the site; values are free-form (string with $, %, intro qualifier, etc.). Don't try to coerce to numeric in the parser — return both raw (verbatim NerdWallet text) and a parsed numeric where unambiguous.
    • Rewards structure — bullet list inside the card row ("5x on travel through Chase, 3x on dining, 2x on all other travel, 1x on everything else"). Decompose into [{ category, rate, cap }]. Rate is multiplier-style (5x, 3%) or flat percentage. Cap is usually null; some cards list up to $1,500 in combined purchases each quarter. Keep the raw bullet alongside the decomposed structure.
    • Welcome bonus — typically "Earn {N} points/{$X cash back} after you spend {$Y} on purchases in the first {Z} months" + an "estimated dollar value" annotation from NerdWallet. Capture as { amount, currency: 'points'|'miles'|'usd', spend_required, spend_window_months, estimated_value_usd }.
    • Credit score required — surfaces as a "Recommended Credit Score" pill or table row. Values are NerdWallet's tier strings: Excellent (720-850), Good (690-719), Fair (630-689), Bad (300-629), Limited / No credit.
    • Key benefits / perks — bullet list further down each card ("Cell phone protection", "Primary rental car coverage", "Priority Pass Select lounge access", "No foreign transaction fees", "Trip cancellation insurance"). Always parse the bullet list — there's no fixed enum.
    • Pros / Cons — explicit Pros: / Cons: bullet blocks inside the card row. Capture verbatim — NerdWallet's editorial voice is part of the value.
    • "Apply Now" affiliate URL<a class="...apply-now..." href="...">Apply Now</a>. The href is typically a nerdwallet.com/redirect/... or nerdwallet.com/cct/... short-link that 302-redirects through partner tracking before landing on the issuer's application page. Capture the href, but DO NOT follow it. Tag it as is_affiliate: true in the output.
    • Canonical NerdWallet review URL — the "Read full review" link or the card-name-as-anchor inside the card. Typically https://www.nerdwallet.com/credit-cards/reviews/{slug}. This is the URL to follow if the caller asks for deeper detail on one card.
    • Last-reviewed / "Card details last updated" timestamps — small grey-text line at the bottom of each card or in the page footer ("Last updated May 6, 2026", "Reviewed by …"). Capture as ISO-8601 if parseable.
  4. For "specific card name" inputs (review-page mode), the same fields apply but laid out across a single page rather than a grid. The "Pros" / "Cons" sections are longer, the rewards structure is broken into a dedicated card-specific table, and there's a "Compare to similar cards" section near the bottom with thumbnails of 2–3 related cards — capture their names + slugs as related_cards if useful for the caller.

  5. Honor sort + limit at parse time. NerdWallet's category-page query-param filter surface is inconsistent across slugs: a handful of pages accept ?sort=annual_fee_asc / ?sort=intro_apr_length / ?sort=rewards_rate / ?sort=welcome_bonus_value, but most ignore unknown params and render the editorial default ranking. Don't trust the URL to filter for you — fetch the full grid and sort/filter client-side. NerdWallet typically renders 10–20 cards per category page; if the caller asks for a specific count, truncate after parsing.

  6. Release the session and emit JSON.

    bb sessions update "$SID" --status REQUEST_RELEASE
    

Static-HTML fallback (works for sub-1 MB pages only)

The category-finder discovery surface — the /credit-cards root, /sitemaps/us/wp-sitemap-posts-credit-cards-pages-1.xml, and /robots.txt — all fetch cleanly under the 1 MB ceiling via bb fetch --proxies --allow-redirects --output <path>. Use this path for slug discovery and category enumeration (no browser needed). The card grids themselves do not — every /credit-cards/best/{slug} and /credit-cards/reviews/{slug} page exceeds 1 MB on first load and must be retrieved through a real session (browse open --remote).

Site-Specific Gotchas

  • READ-ONLY. Never click Apply Now, Apply, Get Started, Sign In, or any form-submit on the page. NerdWallet's affiliate clickout is logged on click and starts a tracked redirect chain through partner ad networks before landing on the issuer's application page; clicking taints the user-state and is a category-prohibited action under the marketplace's read-only rule. Capture the href and flag it is_affiliate: true in the output — the caller decides whether to surface it.
  • URL canonicalization & 301s. The marketing-friendly URL https://www.nerdwallet.com/best/credit-cards/{slug} 301-redirects to https://www.nerdwallet.com/credit-cards/best/{slug} at Cloudflare (Server: cloudflare, Location: /credit-cards/best/...). The new path is canonical; the legacy form still works only if you let redirects flow. Direct fetches without --allow-redirects return the bare 301 page and you'll have a confusing time. Always follow redirects.
  • Trailing-slash and .html-suffix variants trigger Cloudflare 403. Verified 2026-05-18: /credit-cards/best/travel/ (trailing slash) and /credit-cards/best/travel.html both return 403 Forbidden with a freshly-issued __cf_bm cookie (CF bot challenge). The bare canonical /credit-cards/best/travel returns 200. Stick to the no-trailing-slash, no-suffix canonical form. Don't programmatically append / or .html.
  • Page size > 1 MB → bb fetch returns 502 The response body exceeded the maximum allowed size of 1MB. This is a Browserbase Fetch-API ceiling, not a NerdWallet limit. Every /credit-cards/best/{slug} and /credit-cards/reviews/{slug} page hits this. Use browse open --remote (real session) for those; reserve bb fetch for sitemap, robots.txt, and the /credit-cards root (~850 KB, OK).
  • JSON-LD on category pages is page-level, not card-level. Only two <script type="application/ld+json"> blocks on /credit-cards: a VideoObject (the embedded explainer video) and the site-wide Organization block. There is no FinancialProduct, CreditCard, or Product schema embedded — verified 2026-05-18. Do not try to read card data from JSON-LD. Parse the rendered card grid.
  • No public JSON or GraphQL API. Robots.txt explicitly disallows /CreditCardDetailAJAX, /compareajax, /structured-content-renderer, /api/, /cc-prequal-service/, /janitor/, /identity/, and /redirect/ — these endpoints exist internally but are locked. Direct probes (2026-05-18): /wp-json/wp/v2/pages?slug=... → 404, /api/credit-cards → 404, /compareajax?ids=... → 404, /structured-content-renderer?path=... → 405 (POST-only and likely auth-gated). Don't waste cycles on API-discovery — there isn't a usable one.
  • Card-finder quiz at /card-finder-jump is a separate surface. It's a multi-step questionnaire (credit-score → spend categories → annual-fee tolerance → reward preference) that produces a personalized recommendation page rather than the editorial category page. It overlaps the filter surface in the task spec but is not the comparison surface — different data shape (one ranked recommendation + 2–3 alternates rather than a sorted grid of 10–20). If the caller's input includes both a category intent and personalization signals ("rebuild credit, low income, no annual fee, monthly rent payments to build score"), the quiz path is more accurate; but it's interaction-heavy and not described here. Default to the category-page path.
  • Cloudflare cookies (__cf_bm, _cfuvid, nws4) issued on first request. A bare-cookie second request to the same domain in the same session is fine — CF sees the cookies and waves you through. If you rotate sessions per-card, expect a fresh challenge each time; pooling card fetches in a single session is significantly faster.
  • bb sessions create returns a connect URL on connect.usw2.browserbase.com (or regional equivalent). This is the WebSocket endpoint your browse --remote driver attaches to — it is not reachable from sandboxes that allowlist only api.browserbase.com for outbound. If browse open --remote returns getaddrinfo ENOTFOUND connect.usw2.browserbase.com, your runtime's network policy is too restrictive and you have to use bb fetch only (which means you're limited to the sub-1MB pages — see fallback section above).
  • ?sort=... query params are inconsistently honored. A handful of category pages respect ?sort=annual_fee_asc, ?sort=intro_apr_length, etc., but most ignore unknown params and render the editorial default. The visible UI sort dropdown drives a client-side rerender via JS state — it does NOT update the URL on most templates. Sort and filter client-side after parsing.
  • m.nerdwallet.com mobile-subdomain returns 500. Don't try the mobile variant as a "lighter" fetch — the route is broken or removed.
  • AMP variants don't exist for credit-card pages. /credit-cards/best/travel/amp returns 404. Don't waste time looking for an AMP path.
  • Apply Now hrefs are NerdWallet-hosted short-links, not direct issuer URLs. Format: https://www.nerdwallet.com/redirect/...?... or https://www.nerdwallet.com/cct/.... They 302 through partners.nerdwallet.com to the issuer's actual application URL. Capture the NerdWallet-hosted href in the output — that's the de-facto canonical "Apply" link for the card; don't try to resolve it to the underlying issuer URL by following the chain (a) it's affiliate-tracked and (b) following the chain triggers ad-network beacons.
  • "Estimated dollar value" of the welcome bonus is NerdWallet-editorial, not the issuer's. It's an apples-to-apples valuation computed by NerdWallet (e.g. Chase Ultimate Rewards ≈ 2 cents/point in their model). Surface it but tag it explicitly as estimated_value_usd_source: 'nerdwallet_editorial' so the caller knows it's not from the issuer.
  • Sitemap is the canonical slug list. https://www.nerdwallet.com/sitemaps/us/wp-sitemap-posts-credit-cards-pages-1.xml enumerates every /credit-cards/best/{slug} and /credit-cards/reviews/{slug} URL NerdWallet considers canonical. ~13 KB. Re-fetch periodically (slugs do change — NerdWallet retires deprecated categories and adds new co-branded slugs at the start of each year).
  • NerdWallet's editorial team can hold a card off the live page mid-update. When you see "Card details last updated: [date]" with a date older than 90 days and the issuer recently changed terms, the card is in an editorial-stale state; surface the timestamp so downstream consumers can see freshness.
  • Card art images live at www.nerdwallet.com/cdn/... and www.nerdwallet.com/tachyon/... — both are CDN paths. They serve raster + WebP via srcset; pick the first 1x raster src for the primary URL.

Expected Output

Two output shapes — list (category-page input) and single (review-page input):

// Category / list mode — input was a category intent, full /credit-cards/best/{slug} URL, or free-form criteria
{
  "mode": "list",
  "source_url": "https://www.nerdwallet.com/credit-cards/best/travel",
  "category_slug": "travel",
  "category_label": "Best Travel Credit Cards",
  "last_reviewed_page_level": "2026-05-06",
  "applied_filters": {
    "annual_fee_max": null,
    "credit_score_required": null,
    "issuer": null,
    "network": null,
    "rewards_type": null,
    "foreign_transaction_fee_none": false,
    "intro_apr_min_months": null
  },
  "sort": "editorial_default",
  "count": 12,
  "cards": [
    {
      "name": "Chase Sapphire Preferred® Card",
      "issuer": "Chase",
      "network": "Visa",
      "review_url": "https://www.nerdwallet.com/credit-cards/reviews/chase-sapphire-preferred",
      "card_art_url": "https://www.nerdwallet.com/cdn/.../chase-sapphire-preferred.png",
      "nerdwallet_rating": 5.0,
      "rating_label": "Rated 5 out of 5 stars",
      "best_for_tag": "Best for travel rewards",
      "why_we_like_it": "If you want a great mix of …",
      "why_we_dont": "If you'd rather pay zero …",
      "annual_fee": { "raw": "$95", "amount_usd": 95 },
      "intro_apr": {
        "purchases":         { "raw": "None", "rate_percent": null, "duration_months": null },
        "balance_transfers": { "raw": "None", "rate_percent": null, "duration_months": null }
      },
      "regular_apr": { "raw": "20.49% – 27.49% Variable", "min_percent": 20.49, "max_percent": 27.49 },
      "welcome_bonus": {
        "raw": "Earn 60,000 bonus points after you spend $4,000 on purchases in the first 3 months from account opening",
        "amount": 60000,
        "currency": "points",
        "spend_required_usd": 4000,
        "spend_window_months": 3,
        "estimated_value_usd": 750,
        "estimated_value_usd_source": "nerdwallet_editorial"
      },
      "rewards": [
        { "category": "Travel purchased through Chase Travel", "rate": "5x",  "cap": null, "raw": "5x on travel purchased through Chase Travel" },
        { "category": "Dining",                                "rate": "3x",  "cap": null, "raw": "3x on dining" },
        { "category": "Online grocery",                        "rate": "3x",  "cap": null, "raw": "3x on online grocery (excluding Target/Walmart/wholesale)" },
        { "category": "Streaming",                             "rate": "3x",  "cap": null, "raw": "3x on select streaming services" },
        { "category": "Other travel",                          "rate": "2x",  "cap": null, "raw": "2x on all other travel" },
        { "category": "Everything else",                       "rate": "1x",  "cap": null, "raw": "1x on everything else" }
      ],
      "foreign_transaction_fee": { "raw": "None", "percent": 0 },
      "balance_transfer_fee":    { "raw": "Either $5 or 5% of the amount of each transfer, whichever is greater" },
      "late_payment_fee":        { "raw": "Up to $40" },
      "credit_score_required":   { "raw": "Excellent, Good", "tier_min": "Good (690-719)" },
      "key_benefits": [
        "Trip cancellation/interruption insurance",
        "Primary rental car coverage (within US)",
        "$50 annual Chase Travel hotel credit",
        "10% anniversary points boost",
        "No foreign transaction fees"
      ],
      "pros": [
        "Bonus categories include both popular and niche spending",
        "Generous and flexible travel rewards",
        "Reasonable annual fee"
      ],
      "cons": [
        "Has annual fee",
        "Requires good/excellent credit"
      ],
      "apply_url":   "https://www.nerdwallet.com/redirect/...",
      "is_affiliate": true,
      "card_last_updated": "2026-04-22"
    }
    // ...more cards
  ]
}

// Single card / review mode — input was a specific card name or a /credit-cards/reviews/{slug} URL
{
  "mode": "single",
  "source_url": "https://www.nerdwallet.com/credit-cards/reviews/chase-sapphire-preferred",
  "card": { /* same shape as one entry in `cards[]` above, plus: */
    "related_cards": [
      { "name": "Chase Sapphire Reserve®",     "review_url": "https://www.nerdwallet.com/credit-cards/reviews/chase-sapphire-reserve" },
      { "name": "Capital One Venture Rewards", "review_url": "https://www.nerdwallet.com/credit-cards/reviews/capital-one-venture" }
    ]
  }
}

// Disambiguation mode — name input matched multiple review-page slugs
{
  "mode": "ambiguous",
  "query": "chase freedom",
  "matches": [
    { "name": "Chase Freedom Unlimited®",   "review_url": "https://www.nerdwallet.com/credit-cards/reviews/chase-freedom-unlimited" },
    { "name": "Chase Freedom Flex℠",        "review_url": "https://www.nerdwallet.com/credit-cards/reviews/chase-freedom-flex" },
    { "name": "Chase Freedom (legacy)",     "review_url": "https://www.nerdwallet.com/credit-cards/reviews/chase-freedom" }
  ]
}

// Not-found mode — input didn't resolve to any category or review slug
{
  "mode": "not_found",
  "query": "foo",
  "reason": "no_matching_slug",
  "hint": "Try a NerdWallet category slug from /sitemaps/us/wp-sitemap-posts-credit-cards-pages-1.xml or a card name from /credit-cards/reviews/*"
}

Numeric fields are nullable — when NerdWallet's text says "None" or doesn't quote a specific value, leave the parsed numeric null and keep the verbatim string in raw. Always include raw alongside any parsed numeric so the caller can audit the extraction.

NerdWallet Credit Card Comparison · browse.sh