earnrate 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +2 -1
  2. package/data/cards/amex-blue-business-plus.yaml +1 -0
  3. package/data/cards/amex-blue-cash-everyday.yaml +4 -3
  4. package/data/cards/amex-blue-cash-preferred.yaml +5 -7
  5. package/data/cards/amex-gold.yaml +1 -0
  6. package/data/cards/amex-platinum.yaml +1 -0
  7. package/data/cards/bank-of-america-atmos-rewards-ascent.yaml +6 -6
  8. package/data/cards/bank-of-america-atmos-rewards-summit.yaml +6 -6
  9. package/data/cards/chase-freedom-flex.yaml +5 -1
  10. package/data/cards/chase-southwest-plus.yaml +5 -5
  11. package/data/cards/chase-southwest-premier-business.yaml +8 -5
  12. package/data/cards/chase-southwest-premier.yaml +6 -6
  13. package/data/cards/chase-southwest-priority.yaml +6 -6
  14. package/data/cards/citi-aadvantage-business.yaml +6 -6
  15. package/data/cards/citi-aadvantage-platinum-select.yaml +10 -7
  16. package/data/cards/citi-costco-anywhere-visa-business.yaml +4 -4
  17. package/data/merchants/overrides.yaml +4 -1
  18. package/dist/data/file-store.js +3 -1
  19. package/dist/data/store.js +9 -1
  20. package/dist/data/wallet-edit.js +10 -1
  21. package/dist/engine/evaluate.js +22 -3
  22. package/dist/engine/offers.js +2 -1
  23. package/dist/engine/recommend.js +14 -1
  24. package/dist/engine/setup.js +3 -3
  25. package/dist/engine/wallet-change.js +40 -8
  26. package/dist/engine/wallet-summary.js +105 -3
  27. package/dist/format/offers.js +11 -5
  28. package/dist/format/wallet.js +48 -19
  29. package/dist/mcp/context.js +7 -3
  30. package/dist/mcp/inputs.js +12 -5
  31. package/dist/mcp/schemas.js +6 -0
  32. package/dist/mcp/server.js +4 -4
  33. package/dist/mcp/tools/account.js +42 -5
  34. package/dist/mcp/tools/answers.js +13 -4
  35. package/dist/schema/card.js +4 -0
  36. package/package.json +2 -1
package/README.md CHANGED
@@ -167,7 +167,7 @@ Spotted a wrong rate, or want a card added? [Open an issue](https://github.com/c
167
167
 
168
168
  ## Use it in an AI assistant (MCP)
169
169
 
170
- **In Claude, nothing to install:** add a custom connector (Settings → Connectors) with the address `https://mcp.earnrate.dev`. It works in Claude on the web, desktop and phone. Questions need no sign-in. If you ask it to save your cards, it opens an earnrate page where you start an anonymous wallet: no email, just a recovery code you keep. The wallet tracks caps and welcome bonuses across your Claude apps, and you can export it for the command line or delete it from the chat. In Claude Code:
170
+ **In Claude, nothing to install:** add a custom connector (Settings → Connectors) with the address `https://mcp.earnrate.dev`. It works in Claude on the web, desktop and phone. When you add it, an earnrate page opens once, where you start an anonymous wallet: no email, just a recovery code you keep. Then ask anything: with no cards saved it answers from every card. Name your cards for answers about yours, and save them to track caps and welcome bonuses across your Claude apps. From the chat you can export the wallet for the command line, switch to another one, or delete it. In Claude Code:
171
171
 
172
172
  ```sh
173
173
  claude mcp add --transport http earnrate https://mcp.earnrate.dev
@@ -250,6 +250,7 @@ earnrate needs no account. The local version has no telemetry; the hosted server
250
250
  - **AI assistants:** when you use earnrate through Claude or another assistant, that assistant's provider sees the conversation, including earnrate's answers, under its own privacy policy.
251
251
  - **earnrate.dev:** everything you enter on the site stays in your browser. The site counts visits with Cloudflare Web Analytics, without cookies.
252
252
  - **Retention:** locally, your data lives only in files you control, for as long as you keep them. Delete the wallet file, or clear the site's data in your browser, to remove it. A saved wallet on mcp.earnrate.dev lasts until you delete it or leave it unused for 13 months.
253
+ - **Terms:** [earnrate.dev/terms](https://earnrate.dev/terms).
253
254
  - **Contact:** [hello@earnrate.dev](mailto:hello@earnrate.dev).
254
255
 
255
256
  ## Working on earnrate
@@ -14,6 +14,7 @@ welcome_offer:
14
14
  points: 50000
15
15
  spend: 5000
16
16
  months: 3
17
+ varies: true
17
18
  note: 'The public offer is "as high as" 50,000 points; Amex shows the exact amount after you apply. "Welcome offers vary and you may not be eligible for an offer." The card-specific eligibility rules are in the offer terms, which the headless browser can''t load.'
18
19
  rules:
19
20
  - id: amex-travel
@@ -3,8 +3,8 @@ name: American Express Blue Cash Everyday Card
3
3
  issuer: amex
4
4
  network: amex
5
5
  currency: cash
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-10-31
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://www.americanexpress.com/us/credit-cards/card/blue-cash-everyday/
10
10
  - https://www.americanexpress.com/en-us/credit-cards/apply/terms/personal/blue-cash-everyday-credit-card/25330-10-0
@@ -14,6 +14,7 @@ welcome_offer:
14
14
  dollars: 200
15
15
  spend: 2000
16
16
  months: 6
17
+ varies: true
17
18
  note: 'Paid as Reward Dollars, redeemable for a statement credit or at Amazon.com checkout. The card page says "Earn $200"; the offer terms say "as high as $200", with the exact amount shown before you accept. Not available if you have or have had this Card, the Cash Magnet Card, the Blue Cash Preferred Card (including the Morgan Stanley version) or previous versions of these Cards.'
18
19
  rules:
19
20
  - id: us-supermarkets
@@ -62,7 +63,7 @@ evidence:
62
63
  quote: 'at supermarkets located in the U.S. (superstores, convenience stores, warehouse clubs,'
63
64
  - rule: us-online-retail
64
65
  url: https://www.americanexpress.com/en-us/credit-cards/apply/terms/personal/blue-cash-everyday-credit-card/25330-10-0
65
- quote: '3% cash back on the first of $6,000 of U.S. online retail purchases'
66
+ quote: '3% cash back on the first $6,000 of U.S. online retail purchases'
66
67
  - rule: us-online-retail
67
68
  url: https://www.americanexpress.com/en-us/credit-cards/apply/terms/personal/blue-cash-everyday-credit-card/25330-10-0
68
69
  quote: 'from a U.S. retail merchant that sells physical goods or merchandise directly to consumers'
@@ -3,8 +3,8 @@ name: American Express Blue Cash Preferred Card
3
3
  issuer: amex
4
4
  network: amex
5
5
  currency: cash
6
- last_verified: 2026-10-24
7
- verified_by: collin
6
+ last_verified: 2026-10-31
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://www.americanexpress.com/us/credit-cards/card/blue-cash-preferred/
10
10
  - https://www.americanexpress.com/en-us/credit-cards/apply/terms/personal/blue-cash-preferred-credit-card/25330-10-0#offer-terms
@@ -15,7 +15,8 @@ welcome_offer:
15
15
  dollars: 300
16
16
  spend: 3000
17
17
  months: 6
18
- note: 'Paid as Reward Dollars, redeemable for a statement credit or at Amazon.com checkout. The card page says "Earn $300"; the offer terms say "as high as $300", with the exact amount shown before you accept. Not available if you have or have had this Card, the Morgan Stanley Blue Cash Preferred or previous versions of these Cards.'
18
+ varies: true
19
+ note: 'Paid as Reward Dollars, redeemable for a statement credit or at Amazon.com checkout. Up to $300: the offer terms say "as high as $300", with the exact amount shown after you apply and before you accept. The card page''s headline has shown both "Earn $300" and "As High As $300". Not available if you have or have had this Card, the Morgan Stanley Blue Cash Preferred or previous versions of these Cards.'
19
20
  rules:
20
21
  - id: us-supermarkets
21
22
  match: { categories: [grocery.supermarket] }
@@ -67,10 +68,7 @@ evidence:
67
68
  quote: 'will be $0 for the first year of Card Membership'
68
69
  - rule: welcome_offer
69
70
  url: https://www.americanexpress.com/us/credit-cards/card/blue-cash-preferred/
70
- quote: 'Earn $300 Cash Back'
71
- - rule: welcome_offer
72
- url: https://www.americanexpress.com/us/credit-cards/card/blue-cash-preferred/
73
- quote: 'after you spend $3,000 in purchases on your new Card in the first 6 months of Card Membership'
71
+ quote: 'after you spend $3,000 in purchases on your new Card within the first 6 months of Card Membership'
74
72
  - rule: welcome_offer
75
73
  url: https://www.americanexpress.com/en-us/credit-cards/apply/terms/personal/blue-cash-preferred-credit-card/25330-10-0#offer-terms
76
74
  quote: 'As High As $300 Cash Back Welcome Offer'
@@ -14,6 +14,7 @@ welcome_offer:
14
14
  points: 100000
15
15
  spend: 8000
16
16
  months: 6
17
+ varies: true
17
18
  note: 'The public offer is "as high as" 100,000 points; Amex shows the exact amount (which may be lower, or no offer) after you apply and before you accept. Not available if you have or have had this Card, the Premier Rewards Gold Card, the Platinum Card (including the Schwab and Morgan Stanley versions) or previous versions of these Cards; Amex may also decline based on your card history.'
18
19
  rules:
19
20
  - id: restaurants
@@ -14,6 +14,7 @@ welcome_offer:
14
14
  points: 175000
15
15
  spend: 12000
16
16
  months: 6
17
+ varies: true
17
18
  note: 'The public offer is "as high as" 175,000 points; Amex shows the exact amount (which may be lower, or no offer) after you apply and before you accept. Not available if you have or have had this Card, the Schwab or Morgan Stanley Platinum Card, or previous versions of these Cards; Amex may also decline based on your card history.'
18
19
  # One cap covers flights booked directly and through Amex Travel.
19
20
  caps:
@@ -3,15 +3,15 @@ name: Atmos Rewards Ascent Visa Signature Credit Card
3
3
  issuer: bank-of-america
4
4
  network: visa
5
5
  currency: atmos-points
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-10-31
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://www.bankofamerica.com/credit-cards/products/alaska-airlines-credit-card/
10
10
  base_rate: 1
11
11
  annual_fee: 95
12
12
  welcome_offer:
13
- points: 50000
14
- spend: 1500
13
+ points: 70000
14
+ spend: 2500
15
15
  months: 3
16
16
  note: Limited-time online offer, no end date stated; it may not be available elsewhere after leaving the page. The window is 90 days from account opening. The offer also includes a $99 Companion Fare (plus taxes and fees from $23).
17
17
  rules:
@@ -62,10 +62,10 @@ evidence:
62
62
  quote: '$95 annual fee'
63
63
  - rule: welcome_offer
64
64
  url: https://www.bankofamerica.com/credit-cards/products/alaska-airlines-credit-card/
65
- quote: 'Get 50,000 bonus points and a $99 Companion Fare (plus taxes and fees from $23) with this offer.'
65
+ quote: 'Get 70,000 bonus points and a $99 Companion Fare (plus taxes and fees from $23) with this offer.'
66
66
  - rule: welcome_offer
67
67
  url: https://www.bankofamerica.com/credit-cards/products/alaska-airlines-credit-card/
68
- quote: 'spend $1,500 or more on purchases within the first 90 days of opening your account'
68
+ quote: 'spend $2,500 or more on purchases within the first 90 days of opening your account'
69
69
  todo:
70
70
  - 'The 10% rewards bonus rules use a proposed membership id, bank-of-america-account. Bank of America does not say on this page which accounts are "eligible".'
71
71
  - 20% back on Alaska and Hawaiian inflight purchases is a rebate, not points, and isn't modeled.
@@ -3,15 +3,15 @@ name: Atmos Rewards Summit Visa Infinite Credit Card
3
3
  issuer: bank-of-america
4
4
  network: visa
5
5
  currency: atmos-points
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-10-31
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://www.bankofamerica.com/credit-cards/products/alaska-airlines-infinite-credit-card/
10
10
  base_rate: 1
11
11
  annual_fee: 395
12
12
  welcome_offer:
13
- points: 70000
14
- spend: 3000
13
+ points: 80000
14
+ spend: 4000
15
15
  months: 3
16
16
  note: Limited-time online offer, no end date stated; it may not be available elsewhere after leaving the page. The window is 90 days from account opening. The offer also includes a 25,000-point Global Companion Award.
17
17
  rules:
@@ -61,10 +61,10 @@ evidence:
61
61
  quote: '$395 annual fee and no foreign transaction fees'
62
62
  - rule: welcome_offer
63
63
  url: https://www.bankofamerica.com/credit-cards/products/alaska-airlines-infinite-credit-card/
64
- quote: 'Earn 70,000 bonus points and a 25,000-point Global Companion Award after spending $3,000 or more on purchases'
64
+ quote: 'Earn 80,000 bonus points and a 25,000-point Global Companion Award after spending $4,000 or more on purchases'
65
65
  - rule: welcome_offer
66
66
  url: https://www.bankofamerica.com/credit-cards/products/alaska-airlines-infinite-credit-card/
67
- quote: '$3,000 or more on purchases within the first 90 days of opening your account'
67
+ quote: '$4,000 or more on purchases within the first 90 days of opening your account'
68
68
  todo:
69
69
  - '3x on "foreign transaction" purchases (anything charged in a foreign currency or processed abroad) is not modeled: rules have no foreign-only geography. It would make this card the pick for most purchases abroad.'
70
70
  - 'The 10% rewards bonus rules use a proposed membership id, bank-of-america-account. Bank of America does not say on this page which accounts are "eligible".'
@@ -66,7 +66,7 @@ rules:
66
66
  requires: { activation: true }
67
67
  valid_from: 2026-10-01
68
68
  valid_to: 2026-12-31
69
- note: Dining already earns 3%, so this adds 2% until the $1,500 quarterly cap.
69
+ note: Chase says activated categories earn 5% total (4% on top of the 1% base) and doesn't say whether that stacks with the 3% dining rate, so earnrate shows 5%, not 7%, until the $1,500 quarterly cap.
70
70
  - id: q4-2026-american-red-cross
71
71
  match: { merchants: [american-red-cross] }
72
72
  rate: 5
@@ -120,6 +120,9 @@ evidence:
120
120
  - rule: q4-2026-dining
121
121
  url: https://creditcards.chase.com/cash-back-credit-cards/freedom/flex
122
122
  quote: 'Dining American Red Cross'
123
+ - rule: q4-2026-dining
124
+ url: https://creditcards.chase.com/cash-back-credit-cards/freedom/flex
125
+ quote: '5% Cash Back rewards total for each $1 spent until $1,500 is spent'
123
126
  - rule: q4-2026-american-red-cross
124
127
  url: https://creditcards.chase.com/cash-back-credit-cards/freedom/flex
125
128
  quote: 'American Red Cross'
@@ -144,4 +147,5 @@ evidence:
144
147
  todo:
145
148
  - "Chase's offer-details overlays for the Q4 2026 and Q1 2027 categories (exact grocery exclusions, what counts for American Red Cross) didn't render in the page dump; rules follow the calendar labels and the rewards-category FAQ."
146
149
  - 'q4-2026-american-red-cross: what counts (presumably donations) is unconfirmed.'
150
+ - 'q4-2026-dining: whether the 4% additional stacks on the 3% dining rate (7% total) is unstated; showing 5% until Chase says.'
147
151
  - 'Q2 2027 (Apr-Jun) shows "Coming Soon"; add it after Chase posts it (activation opens March 15, 2027).'
@@ -3,18 +3,18 @@ name: Southwest Rapid Rewards Plus Credit Card
3
3
  issuer: chase
4
4
  network: visa
5
5
  currency: southwest-rapid-rewards
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-10-31
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://creditcards.chase.com/travel-credit-cards/southwest/plus
10
10
  - https://www.chase.com/personal/credit-cards/rewards-category-faq
11
11
  base_rate: 1
12
12
  annual_fee: 99
13
13
  welcome_offer:
14
- points: 50000
14
+ points: 80000
15
15
  spend: 1000
16
16
  months: 3
17
- note: 'Not available to current cardmembers of any Southwest Rapid Rewards Credit Card, or previous cardmembers of any Southwest Rapid Rewards Credit Card who received a new Cardmember bonus within the last 24 months (this does not apply to business cards).'
17
+ note: 'Chase labels it a limited-time offer and states no end date. Not available to current cardmembers of any Southwest Rapid Rewards Credit Card, or previous cardmembers of any Southwest Rapid Rewards Credit Card who received a new Cardmember bonus within the last 24 months (this does not apply to business cards).'
18
18
  caps:
19
19
  - id: gas-and-grocery
20
20
  amount: 5000
@@ -48,7 +48,7 @@ evidence:
48
48
  quote: '$99 applied to first billing statement.'
49
49
  - rule: welcome_offer
50
50
  url: https://creditcards.chase.com/travel-credit-cards/southwest/plus
51
- quote: 'Earn 50,000 bonus points after you spend $1,000 on purchases in the first 3 months from account opening'
51
+ quote: 'Earn 80,000 bonus points after you spend $1,000 on purchases in the first 3 months from account opening'
52
52
  - rule: base_rate
53
53
  url: https://creditcards.chase.com/travel-credit-cards/southwest/plus
54
54
  quote: 'Earn 1 point for each dollar spent on all other purchases.'
@@ -3,8 +3,8 @@ name: Southwest Rapid Rewards Premier Business Credit Card
3
3
  issuer: chase
4
4
  network: visa
5
5
  currency: southwest-rapid-rewards
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-10-31
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://creditcards.chase.com/southwest/premier-business-credit-card
10
10
  - https://creditcards.chase.com/business-credit-cards/southwest/premier-business
@@ -12,10 +12,10 @@ base_rate: 1
12
12
  business: true
13
13
  annual_fee: 149
14
14
  welcome_offer:
15
- points: 80000
15
+ points: 60000
16
16
  spend: 3000
17
17
  months: 3
18
- note: 'Marked a limited-time offer, with 60,000 struck through; the page states no end date. Not available to current cardmembers of this business card, or previous cardmembers who received a new Cardmember bonus for this business card within the last 24 months.'
18
+ note: 'Not available to current cardmembers of this business card, or previous cardmembers who received a new Cardmember bonus for this business card within the last 24 months.'
19
19
  caps:
20
20
  - id: gas-and-dining
21
21
  amount: 8000
@@ -48,7 +48,10 @@ evidence:
48
48
  quote: '$149 applied to first billing statement.'
49
49
  - rule: welcome_offer
50
50
  url: https://creditcards.chase.com/business-credit-cards/southwest/premier-business
51
- quote: '80,000 points after you spend $3,000 on purchases in the first 3 months from account opening'
51
+ quote: 'Earn 60,000 points'
52
+ - rule: welcome_offer
53
+ url: https://creditcards.chase.com/business-credit-cards/southwest/premier-business
54
+ quote: 'after you spend $3,000 on purchases in the first 3 months from account opening'
52
55
  - rule: base_rate
53
56
  url: https://creditcards.chase.com/southwest/premier-business-credit-card
54
57
  quote: 'You’ll earn 1 point for each $1 spent on all other purchases'
@@ -3,18 +3,18 @@ name: Southwest Rapid Rewards Premier Credit Card
3
3
  issuer: chase
4
4
  network: visa
5
5
  currency: southwest-rapid-rewards
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-10-31
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://creditcards.chase.com/travel-credit-cards/southwest/premier
10
10
  - https://www.chase.com/personal/credit-cards/rewards-category-faq
11
11
  base_rate: 1
12
12
  annual_fee: 149
13
13
  welcome_offer:
14
- points: 55000
15
- spend: 1500
14
+ points: 85000
15
+ spend: 2000
16
16
  months: 3
17
- note: 'Not available to current cardmembers of any Southwest Rapid Rewards Credit Card, or previous cardmembers of any Southwest Rapid Rewards Credit Card who received a new Cardmember bonus within the last 24 months (this does not apply to business cards).'
17
+ note: 'Chase labels it a limited-time offer and states no end date. Not available to current cardmembers of any Southwest Rapid Rewards Credit Card, or previous cardmembers of any Southwest Rapid Rewards Credit Card who received a new Cardmember bonus within the last 24 months (this does not apply to business cards).'
18
18
  caps:
19
19
  - id: grocery-and-dining
20
20
  amount: 8000
@@ -48,7 +48,7 @@ evidence:
48
48
  quote: '$149 applied to first billing statement.'
49
49
  - rule: welcome_offer
50
50
  url: https://creditcards.chase.com/travel-credit-cards/southwest/premier
51
- quote: 'Earn 55,000 bonus points after you spend $1,500 on purchases in the first 3 months from account opening'
51
+ quote: 'Earn 85,000 bonus points after you spend $2,000 on purchases in the first 3 months from account opening'
52
52
  - rule: base_rate
53
53
  url: https://creditcards.chase.com/travel-credit-cards/southwest/premier
54
54
  quote: 'Earn 1 point for each dollar spent on all other purchases.'
@@ -3,17 +3,17 @@ name: Southwest Rapid Rewards Priority Credit Card
3
3
  issuer: chase
4
4
  network: visa
5
5
  currency: southwest-rapid-rewards
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-10-31
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://creditcards.chase.com/travel-credit-cards/southwest/priority
10
10
  base_rate: 1
11
11
  annual_fee: 229
12
12
  welcome_offer:
13
- points: 60000
14
- spend: 2000
13
+ points: 90000
14
+ spend: 3000
15
15
  months: 3
16
- note: 'Not available to current cardmembers of any Southwest Rapid Rewards Credit Card, or previous cardmembers of any Southwest Rapid Rewards Credit Card who received a new Cardmember bonus within the last 24 months (this does not apply to business cards).'
16
+ note: 'Chase labels it a limited-time offer and states no end date. Not available to current cardmembers of any Southwest Rapid Rewards Credit Card, or previous cardmembers of any Southwest Rapid Rewards Credit Card who received a new Cardmember bonus within the last 24 months (this does not apply to business cards).'
17
17
  rules:
18
18
  - id: southwest
19
19
  match: { merchants: [southwest] }
@@ -37,7 +37,7 @@ evidence:
37
37
  quote: '$229 applied to first billing statement.'
38
38
  - rule: welcome_offer
39
39
  url: https://creditcards.chase.com/travel-credit-cards/southwest/priority
40
- quote: 'Earn 60,000 bonus points after you spend $2,000 on purchases in the first 3 months from account opening'
40
+ quote: 'Earn 90,000 bonus points after you spend $3,000 on purchases in the first 3 months from account opening'
41
41
  - rule: base_rate
42
42
  url: https://creditcards.chase.com/travel-credit-cards/southwest/priority
43
43
  quote: 'Earn 1 point for each dollar spent on all other purchases.'
@@ -3,8 +3,8 @@ name: Citi / AAdvantage Business World Elite Mastercard
3
3
  issuer: citi
4
4
  network: mastercard
5
5
  currency: aadvantage-miles
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-10-31
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://www.citi.com/credit-cards/citi-aadvantage-business-credit-card
10
10
  # Miles post to the company's AAdvantage Business account, not a personal
@@ -14,9 +14,9 @@ business: true
14
14
  annual_fee: 99
15
15
  first_year_fee: 0
16
16
  welcome_offer:
17
- points: 65000
18
- spend: 4000
19
- months: 4
17
+ points: 75000
18
+ spend: 5000
19
+ months: 5
20
20
  note: "Miles post to the business's AAdvantage Business account. Not available if you received a new account bonus for a Citi / AAdvantage Business account in the past 48 months, or converted another Citi card on which you earned a new account bonus in the last 48 months into one."
21
21
  rules:
22
22
  - id: american-airlines
@@ -72,7 +72,7 @@ evidence:
72
72
  quote: 'The annual fee is $99. However, this fee is waived for the first 12 months.'
73
73
  - rule: welcome_offer
74
74
  url: https://www.citi.com/credit-cards/citi-aadvantage-business-credit-card
75
- quote: '65,000 AAdvantage® bonus miles For your business after $4,000 in purchases within the first 4 months of account opening'
75
+ quote: '75,000 AAdvantage® bonus miles For your business after $5,000 in purchases within the first 5 months of account opening'
76
76
  - rule: welcome_offer
77
77
  url: https://www.citi.com/credit-cards/citi-aadvantage-business-credit-card
78
78
  quote: 'new account bonus for a Citi® / AAdvantage® Business account in the past 48 months'
@@ -3,18 +3,18 @@ name: Citi / AAdvantage Platinum Select World Elite Mastercard
3
3
  issuer: citi
4
4
  network: mastercard
5
5
  currency: aadvantage-miles
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-10-31
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://www.citi.com/credit-cards/citi-aadvantage-platinum-elite-credit-card
10
10
  base_rate: 1
11
11
  annual_fee: 99
12
12
  first_year_fee: 0
13
13
  welcome_offer:
14
- points: 80000
15
- spend: 3500
16
- months: 4
17
- note: 'Citi labels it a limited time offer but states no end date. Not available if you received a new account bonus for a Citi / AAdvantage Platinum Select account in the past 48 months, or converted another Citi card on which you earned a new account bonus in the last 48 months into one.'
14
+ points: 50000
15
+ spend: 2500
16
+ months: 3
17
+ note: 'Not available if you received a new account bonus for a Citi / AAdvantage Platinum Select account in the past 48 months, or converted another Citi card on which you earned a new account bonus in the last 48 months into one.'
18
18
  rules:
19
19
  - id: american-airlines
20
20
  match: { merchants: [american-airlines] }
@@ -62,7 +62,10 @@ evidence:
62
62
  quote: 'Annual fee - $99, waived for the first 12 months.'
63
63
  - rule: welcome_offer
64
64
  url: https://www.citi.com/credit-cards/citi-aadvantage-platinum-elite-credit-card
65
- quote: 'Earn 80,000 AAdvantage® bonus miles after $3,500 in purchases in the first 4 months.'
65
+ quote: 'Earn 50,000 AAdvantage® bonus miles'
66
+ - rule: welcome_offer
67
+ url: https://www.citi.com/credit-cards/citi-aadvantage-platinum-elite-credit-card
68
+ quote: 'after $2,500 in purchases in the first 3 months'
66
69
  - rule: welcome_offer
67
70
  url: https://www.citi.com/credit-cards/citi-aadvantage-platinum-elite-credit-card
68
71
  quote: 'new account bonus for a Citi® / AAdvantage® Platinum Select® account in the past 48 months'
@@ -3,8 +3,8 @@ name: Costco Anywhere Visa Business Card by Citi
3
3
  issuer: citi
4
4
  network: visa
5
5
  currency: cash
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-10-31
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://www.citi.com/credit-cards/costco-anywhere-visa-business-card
10
10
  # Paid once a year as a Costco reward certificate after the February statement,
@@ -30,7 +30,7 @@ rules:
30
30
  exclude: { categories: [gas.superstore, gas.grocery_store] }
31
31
  rate: 4
32
32
  cap: gas-and-ev-combined
33
- note: Other eligible gas and EV charging worldwide. Gas or EV charging at superstores, supermarkets, convenience stores and warehouse clubs other than Costco, and fuel for non-automobile use, earn 1%. Citi keeps a list of excluded merchants at citi.com/Costco_Exclusions.
33
+ note: Other eligible gas and EV charging worldwide. Gas or EV charging at some superstores, supermarkets and warehouse clubs other than Costco, and fuel for non-automobile use, earn 1%. Citi keeps a list of excluded merchants at citi.com/Costco_Exclusions.
34
34
  - id: restaurants
35
35
  match: { categories: [dining] }
36
36
  rate: 3
@@ -63,7 +63,7 @@ evidence:
63
63
  quote: '4% cash back on other eligible gas and electric vehicle (EV) charging purchases worldwide'
64
64
  - rule: gas-and-ev-charging
65
65
  url: https://www.citi.com/credit-cards/costco-anywhere-visa-business-card
66
- quote: 'for gas or EV charging purchased at some superstores, supermarkets, convenience stores and warehouse clubs other than Costco'
66
+ quote: 'for gas or EV charging purchased at some superstores, supermarkets and warehouse clubs other than Costco'
67
67
  - rule: restaurants
68
68
  url: https://www.citi.com/credit-cards/costco-anywhere-visa-business-card
69
69
  quote: '3% cash back on restaurant purchases (including cafes, bars, lounges, and fast food restaurants)'
@@ -113,7 +113,10 @@
113
113
  category: warehouse_club
114
114
  in_store:
115
115
  accepts: [visa]
116
- note: Warehouse registers take Visa only for credit. Costco.com takes other networks.
116
+ online:
117
+ accepts: [visa, mastercard]
118
+ # customerservice.costco.com/app/answers/answer_view/a_id/719 (2026-10-31)
119
+ note: Warehouse registers take Visa only for credit; Costco.com takes Visa and Mastercard.
117
120
  - id: costco-gas
118
121
  name: Costco Gas
119
122
  aliases: [costco gas station]
@@ -69,7 +69,9 @@ export class FileWalletStore {
69
69
  return this.change(this.existing((w) => updateInWallet(this.catalog, w, input, today)), (text, change) => updateCardText(text ?? '', input.id, change.fields));
70
70
  }
71
71
  removeCard(cardId) {
72
- return this.change(this.existing((w) => removeFromWallet(w, cardId)), (text) => removeCardText(text ?? '', cardId));
72
+ return this.change(this.existing((w) => w.cards.length === 1 && w.cards[0]?.id === cardId
73
+ ? failed(`${cardId} is the last card in ${this.where}. To start over, delete that file; to swap cards, add the new one first.`)
74
+ : removeFromWallet(w, cardId)), (text) => removeCardText(text ?? '', cardId));
73
75
  }
74
76
  setMemberships(memberships) {
75
77
  return this.change(this.existing((w) => withMemberships(this.catalog, w, memberships)), (text, change) => setMembershipsText(text ?? '', change.wallet.memberships));
@@ -30,7 +30,11 @@ export class JsonWalletStore {
30
30
  const change = apply(before);
31
31
  if (!change.ok)
32
32
  return failed(change.error);
33
- await this.backend.write(JSON.stringify(change.wallet));
33
+ // An empty wallet is stored as none: the schema needs a card.
34
+ if (change.wallet.cards.length === 0)
35
+ await this.backend.remove();
36
+ else
37
+ await this.backend.write(JSON.stringify(change.wallet));
34
38
  return {
35
39
  ok: true,
36
40
  wallet: change.wallet,
@@ -62,6 +66,10 @@ export class JsonWalletStore {
62
66
  erase() {
63
67
  return this.serialized(() => this.backend.remove());
64
68
  }
69
+ // Nothing to sign out of: the backend is the caller's own storage.
70
+ disconnect() {
71
+ return Promise.resolve();
72
+ }
65
73
  record(purchase, merchant) {
66
74
  return this.serialized(async () => {
67
75
  const wallet = await this.load();
@@ -33,12 +33,20 @@ function fieldLines(field, f, indent) {
33
33
  case 'checkout_label':
34
34
  case 'anniversary': {
35
35
  const value = f[field];
36
- return value === undefined ? '' : `${indent}${field}: ${scalar(value)}\n`;
36
+ return value == null ? '' : `${indent}${field}: ${scalar(value)}\n`;
37
37
  }
38
38
  case 'activated':
39
39
  return f.activated === undefined
40
40
  ? ''
41
41
  : `${indent}activated: ${flowList(f.activated)}\n`;
42
+ case 'cap_usage': {
43
+ if (f.cap_usage === undefined)
44
+ return '';
45
+ const caps = Object.entries(f.cap_usage)
46
+ .map(([key, u]) => `${indent} ${key}: { period: ${scalar(u.period)}, spent: ${String(u.spent)} }\n`)
47
+ .join('');
48
+ return caps === '' ? '' : `${indent}cap_usage:\n${caps}`;
49
+ }
42
50
  case 'bonus': {
43
51
  if (!f.bonus)
44
52
  return '';
@@ -66,6 +74,7 @@ const FIELDS = [
66
74
  'anniversary',
67
75
  'chosen',
68
76
  'activated',
77
+ 'cap_usage',
69
78
  'bonus',
70
79
  ];
71
80
  function cardsSeq(text) {
@@ -56,7 +56,26 @@ export function pickedCategories(card, held, date) {
56
56
  }
57
57
  return { picked, staleChoice };
58
58
  }
59
- function applicability(rule, ctx, held, picked) {
59
+ // Activating a rotating quarter (Freedom Flex) turns on all its categories:
60
+ // rules on the same rotating cap with the same dates share one activation.
61
+ export function activationGroup(card, ruleId) {
62
+ const rule = card.rules.find((r) => r.id === ruleId);
63
+ const cap = typeof rule?.cap === 'string'
64
+ ? card.caps.find((c) => c.id === rule.cap)
65
+ : undefined;
66
+ if (!rule || !cap?.rotating)
67
+ return [ruleId];
68
+ return card.rules
69
+ .filter((r) => r.requires?.activation === true &&
70
+ r.cap === rule.cap &&
71
+ r.valid_from === rule.valid_from &&
72
+ r.valid_to === rule.valid_to)
73
+ .map((r) => r.id);
74
+ }
75
+ export function isActivated(card, held, ruleId) {
76
+ return activationGroup(card, ruleId).some((id) => held.activated.includes(id));
77
+ }
78
+ function applicability(card, rule, ctx, held, picked) {
60
79
  const { match, exclude } = rule;
61
80
  const inCategory = (cats) => cats.some((c) => categoryMatches(c, ctx.category));
62
81
  if (match.categories?.length && !inCategory(match.categories))
@@ -98,7 +117,7 @@ function applicability(rule, ctx, held, picked) {
98
117
  const membership = rule.requires?.membership;
99
118
  if (membership !== undefined && !ctx.memberships.has(membership))
100
119
  return 'no';
101
- if (rule.requires?.activation && !held.activated.includes(rule.id))
120
+ if (rule.requires?.activation && !isActivated(card, held, rule.id))
102
121
  return 'needs-activation';
103
122
  return 'applies';
104
123
  }
@@ -158,7 +177,7 @@ export function evaluateCard(card, held, ctx) {
158
177
  let nominal = base;
159
178
  const blocked = [];
160
179
  for (const rule of card.rules) {
161
- const status = applicability(rule, ctx, held, picked);
180
+ const status = applicability(card, rule, ctx, held, picked);
162
181
  if (status === 'no')
163
182
  continue;
164
183
  if (status === 'needs-activation') {
@@ -34,6 +34,7 @@ export function compareOffers(catalog, wallet, filters, today) {
34
34
  spend: offer.spend,
35
35
  months: offer.months,
36
36
  validTo: offer.valid_to ?? null,
37
+ varies: offer.varies === true,
37
38
  note: offer.note ?? null,
38
39
  annualFee: card.annual_fee ?? null,
39
40
  firstYearFee,
@@ -47,7 +48,7 @@ export function compareOffers(catalog, wallet, filters, today) {
47
48
  offers.sort((a, b) => b.firstYearValue - a.firstYearValue || a.cardId.localeCompare(b.cardId));
48
49
  const notes = [
49
50
  `Compares the ${String(catalog.cards.length)} cards earnrate knows, not every card on the market.`,
50
- "Each offer is the public one on the issuer's page, checked weekly. Offers can differ by person and by how you apply, and issuers limit who can get a bonus; each card's note says how.",
51
+ "Each offer is the public one on the issuer's page on the date shown, checked weekly. Yours may differ: issuers test and target offers, so use the one shown to you when you apply. Issuers also limit who can get a bonus; each card's note says how.",
51
52
  "First-year value is the bonus at your point values minus the first year's annual fee. It leaves out what the card earns on your spending and any credits or perks.",
52
53
  ];
53
54
  return { offers, withoutOffer, notes };
@@ -23,6 +23,11 @@ const PERIOD_WORDS = {
23
23
  const displayName = (e) => e.held.checkout_label ?? e.card.name;
24
24
  // "Worth X or more" must hold at X itself, so round up; "less than X" rounds down.
25
25
  const roundUp = (n) => String(Math.ceil(n * 100 - EPSILON) / 100);
26
+ // At exactly the break-even value the two cards tie, so only an exact
27
+ // threshold reads "more than".
28
+ const worthAbove = (n) => Math.abs(n * 100 - Math.round(n * 100)) < EPSILON
29
+ ? `more than ${num(n)}¢`
30
+ : `${roundUp(n)}¢ or more`;
26
31
  const roundDown = (n) => String(Math.floor(n * 100 + EPSILON) / 100);
27
32
  function compareEntries(a, b) {
28
33
  const byReturn = b.effectiveReturnPct - a.effectiveReturnPct;
@@ -218,7 +223,7 @@ function sensitivity(catalog, entries) {
218
223
  if (o.range && oRate > 0) {
219
224
  const threshold = winner.effectiveReturnPct / oRate;
220
225
  if (threshold > o.centsPerUnit && threshold <= o.range[1]) {
221
- consider((threshold - o.centsPerUnit) / o.centsPerUnit, `${displayName(other)} beats ${displayName(winner)} here if ${currencyLabel(catalog, o.currency)} points are worth ${roundUp(threshold)}¢ or more (you value them at ${num(o.centsPerUnit)}¢).`);
226
+ consider((threshold - o.centsPerUnit) / o.centsPerUnit, `${displayName(other)} beats ${displayName(winner)} here if ${currencyLabel(catalog, o.currency)} points are worth ${worthAbove(threshold)} (you value them at ${num(o.centsPerUnit)}¢).`);
222
227
  }
223
228
  }
224
229
  }
@@ -283,6 +288,14 @@ export function recommend(catalog, wallet, purchase, merchant = resolveMerchant(
283
288
  }
284
289
  if (!winner) {
285
290
  warnings.push(`None of your cards work at ${merchant.resolvedName}.`);
291
+ const online = merchant.channel === 'in_store' && merchant.merchantId !== null
292
+ ? resolveMerchantById(catalog, merchant.merchantId, 'online')
293
+ : null;
294
+ const alt = online
295
+ ? rankEntries(catalog, wallet, { ...purchase, channel: 'online' }, online, online.category).entries[0]
296
+ : undefined;
297
+ if (alt)
298
+ warnings.push(`Ordering online instead? ${displayName(alt)} works there (${num(alt.effectiveReturnPct)}%).`);
286
299
  }
287
300
  else if (merchant.accepts === null &&
288
301
  (winner.card.network === 'amex' || winner.card.network === 'discover')) {