earnrate 0.2.5 → 0.2.7

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 (112) hide show
  1. package/README.md +25 -18
  2. package/data/CHANGELOG.md +56 -0
  3. package/data/VERSION +1 -1
  4. package/data/cards/amex-blue-business-plus.yaml +14 -6
  5. package/data/cards/amex-blue-cash-everyday.yaml +12 -3
  6. package/data/cards/amex-blue-cash-preferred.yaml +17 -3
  7. package/data/cards/amex-gold.yaml +19 -3
  8. package/data/cards/amex-platinum.yaml +19 -3
  9. package/data/cards/bank-of-america-atmos-rewards-ascent.yaml +10 -2
  10. package/data/cards/bank-of-america-atmos-rewards-business.yaml +14 -3
  11. package/data/cards/bank-of-america-atmos-rewards-summit.yaml +4 -1
  12. package/data/cards/bank-of-america-customized-cash-rewards.yaml +10 -5
  13. package/data/cards/bilt-obsidian.yaml +4 -3
  14. package/data/cards/capital-one-kohls-card.yaml +11 -2
  15. package/data/cards/capital-one-kohls-rewards-visa.yaml +8 -2
  16. package/data/cards/capital-one-savor.yaml +32 -10
  17. package/data/cards/capital-one-venture-x.yaml +19 -4
  18. package/data/cards/capital-one-venture.yaml +19 -5
  19. package/data/cards/chase-aeroplan.yaml +15 -3
  20. package/data/cards/chase-freedom-flex.yaml +49 -10
  21. package/data/cards/chase-freedom-unlimited.yaml +6 -2
  22. package/data/cards/chase-ihg-one-rewards-business.yaml +18 -2
  23. package/data/cards/chase-ihg-one-rewards-premier-select.yaml +14 -2
  24. package/data/cards/chase-ihg-one-rewards-premier.yaml +15 -2
  25. package/data/cards/chase-ihg-one-rewards.yaml +8 -2
  26. package/data/cards/chase-ink-business-cash.yaml +7 -3
  27. package/data/cards/chase-ink-business-preferred.yaml +9 -2
  28. package/data/cards/chase-prime-visa.yaml +24 -5
  29. package/data/cards/chase-sapphire-preferred.yaml +59 -6
  30. package/data/cards/chase-sapphire-reserve.yaml +12 -2
  31. package/data/cards/chase-southwest-performance-business.yaml +22 -4
  32. package/data/cards/chase-southwest-plus.yaml +13 -3
  33. package/data/cards/chase-southwest-premier-business.yaml +22 -5
  34. package/data/cards/chase-southwest-premier.yaml +13 -3
  35. package/data/cards/chase-southwest-priority.yaml +18 -2
  36. package/data/cards/chase-united-business.yaml +5 -2
  37. package/data/cards/chase-united-club-business.yaml +2 -2
  38. package/data/cards/chase-united-club.yaml +16 -3
  39. package/data/cards/chase-united-explorer.yaml +5 -2
  40. package/data/cards/chase-united-gateway.yaml +2 -2
  41. package/data/cards/chase-united-quest.yaml +12 -3
  42. package/data/cards/citi-aadvantage-business.yaml +10 -4
  43. package/data/cards/citi-aadvantage-executive.yaml +8 -8
  44. package/data/cards/citi-aadvantage-mileup.yaml +7 -4
  45. package/data/cards/citi-aadvantage-platinum-select.yaml +1 -1
  46. package/data/cards/citi-costco-anywhere-visa-business.yaml +5 -2
  47. package/data/cards/citi-costco-anywhere-visa.yaml +6 -3
  48. package/data/cards/citi-custom-cash.yaml +43 -12
  49. package/data/cards/citi-double-cash.yaml +9 -3
  50. package/data/cards/citi-my-best-buy-visa.yaml +6 -3
  51. package/data/cards/citi-strata-elite.yaml +12 -2
  52. package/data/cards/citi-strata-premier.yaml +24 -2
  53. package/data/cards/sofi-unlimited-2.yaml +17 -3
  54. package/data/cards/td-bank-nordstrom-credit-card.yaml +12 -3
  55. package/data/cards/td-bank-nordstrom-visa.yaml +19 -3
  56. package/data/cards/td-bank-target-circle-card.yaml +13 -4
  57. package/data/cards/usbank-cash-plus.yaml +29 -7
  58. package/data/cards/wells-fargo-active-cash.yaml +9 -2
  59. package/data/cards/wells-fargo-autograph.yaml +31 -7
  60. package/data/issuers.yaml +32 -0
  61. package/data/merchants/seed.yaml +7 -0
  62. package/dist/brand.js +1 -0
  63. package/dist/cli/bin.js +10 -1
  64. package/dist/cli/cheatsheet.js +10 -5
  65. package/dist/cli/explore.js +6 -13
  66. package/dist/cli/import.js +2 -2
  67. package/dist/cli/missed.js +65 -0
  68. package/dist/cli/offers.js +17 -7
  69. package/dist/cli/run.js +29 -17
  70. package/dist/cli/todo.js +62 -0
  71. package/dist/cli/wallet.js +9 -6
  72. package/dist/data/file-store.js +6 -7
  73. package/dist/data/integrity.js +4 -0
  74. package/dist/data/load.js +4 -1
  75. package/dist/data/store.js +6 -22
  76. package/dist/data/wallet-edit.js +38 -13
  77. package/dist/engine/cheat-sheet.js +14 -6
  78. package/dist/engine/checklist.js +152 -0
  79. package/dist/engine/compare.js +2 -0
  80. package/dist/engine/evaluate.js +16 -11
  81. package/dist/engine/explore.js +6 -38
  82. package/dist/engine/memberships.js +1 -1
  83. package/dist/engine/offers.js +2 -1
  84. package/dist/engine/periods.js +18 -0
  85. package/dist/engine/recommend.js +21 -20
  86. package/dist/engine/rule-text.js +47 -0
  87. package/dist/engine/setup.js +14 -24
  88. package/dist/engine/simulate.js +7 -6
  89. package/dist/engine/text.js +13 -1
  90. package/dist/engine/wallet-change.js +10 -12
  91. package/dist/engine/wallet-summary.js +80 -65
  92. package/dist/format/card-text.js +7 -20
  93. package/dist/format/explore.js +2 -2
  94. package/dist/format/offers.js +2 -2
  95. package/dist/format/recommendation.js +2 -2
  96. package/dist/format/wallet.js +7 -1
  97. package/dist/import/analyze.js +2 -1
  98. package/dist/mcp/context.js +38 -13
  99. package/dist/mcp/inputs.js +1 -1
  100. package/dist/mcp/schemas.js +9 -0
  101. package/dist/mcp/serve.js +4 -0
  102. package/dist/mcp/server.js +8 -2
  103. package/dist/mcp/tools/account.js +44 -16
  104. package/dist/mcp/tools/answers.js +58 -19
  105. package/dist/mcp/tools/wallet.js +64 -25
  106. package/dist/merchants/cache.js +0 -10
  107. package/dist/merchants/missed.js +78 -0
  108. package/dist/merchants/resolve.js +81 -4
  109. package/dist/schema/card.js +4 -1
  110. package/dist/schema/issuer.js +8 -0
  111. package/dist/schema/wallet.js +1 -1
  112. package/package.json +1 -1
@@ -3,8 +3,8 @@ name: Nordstrom Visa Credit Card
3
3
  issuer: td-bank
4
4
  network: visa
5
5
  currency: nordy-club
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-11-01
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://www.nordstrom.com/browse/nordy-club
10
10
  - https://www.nordstrom.com/browse/nordy-club/faq
@@ -25,7 +25,7 @@ rules:
25
25
  - id: nordstrom
26
26
  match: { merchants: [nordstrom] }
27
27
  rate: 1
28
- note: Card's own share only. Cardmembers start at Influencer status and earn 2 points per dollar at Nordstrom and Nordstrom.com, 1 of which any member earns paying any way. Paid through Apple Pay, Google Pay or PayPal it's 1 point per dollar, so nothing extra.
28
+ note: Card's own share only. Cardmembers start at Influencer status and earn 2 points per dollar at Nordstrom and Nordstrom.com, 1 of which any member earns paying any way. Apple Pay and Google Pay with the card still earn the full rate; through PayPal or another third-party processor, or another contactless method, it's 1 point per dollar, so nothing extra.
29
29
  - id: nordstrom-ambassador
30
30
  match: { merchants: [nordstrom] }
31
31
  rate: 2
@@ -51,6 +51,10 @@ rules:
51
51
  match: { categories: [dining] }
52
52
  rate: 2
53
53
  note: Includes fast food, bars, caterers and third-party delivery. Restaurants inside Nordstrom earn as Nordstrom purchases.
54
+ - id: caterers
55
+ match: { mccs: [5811] }
56
+ rate: 2
57
+ note: Caterers count as dining.
54
58
  - id: streaming
55
59
  match: { categories: [streaming, tv_internet] }
56
60
  rate: 2
@@ -107,3 +111,15 @@ evidence:
107
111
  - rule: not_with
108
112
  url: https://www.nordstrom.com/becomecardmember
109
113
  quote: 'Current and previous Nordstrom credit cardmembers and Nordstrom employees are not eligible for this offer'
114
+ - rule: nordstrom
115
+ url: https://www.nordstrom.com/browse/nordy-club/faq
116
+ quote: "If you use your Nordstrom card via Google Pay or Apple Pay, you'll earn your standard Nordy Club base points"
117
+ - rule: nordstrom
118
+ url: https://www.nordstrom.com/browse/nordy-club/terms-conditions
119
+ quote: 'For purchases made with a third-party payment processor, including those linked to a Nordstrom credit or debit card'
120
+ - rule: nordstrom-rack
121
+ url: https://www.nordstrom.com/browse/nordy-club/terms-conditions
122
+ quote: 'Eligible purchases exclude gift cards, alterations, charitable donations, taxes, shipping fees'
123
+ - rule: caterers
124
+ url: https://www.nordstrom.com/browse/nordy-club/terms-conditions
125
+ quote: 'cocktail lounges, caterers, fast-food locations, and third-party restaurant delivery and takeout services'
@@ -4,8 +4,8 @@ issuer: td-bank
4
4
  network: store
5
5
  only_at: [target]
6
6
  currency: cash
7
- last_verified: 2026-10-26
8
- verified_by: collin
7
+ last_verified: 2026-11-01
8
+ verified_by: claude
9
9
  source_urls:
10
10
  - https://www.target.com/circlecard
11
11
  - https://www.target.com/c/target-circle-card-program-rules/-/N-4tgj0
@@ -17,12 +17,12 @@ welcome_offer:
17
17
  dollars: 50
18
18
  spend: 50
19
19
  months: 2
20
- note: Paid as $50 in Target Circle Rewards, redeemable through Target Circle, not cash. Spend $50 or more on qualifying purchases in-store or at Target.com with the Target Circle Credit Card within 60 days of approval. Not for the Target Circle Debit Card or mail-in applications. Qualifying purchases exclude alcohol and gift cards, among others.
20
+ note: Paid as $50 in Target Circle Rewards, redeemable through Target Circle, not cash. Spend $50 or more on qualifying purchases in-store or at Target.com with the Target Circle Credit Card within 60 days of approval. Not for the Target Circle Debit Card or mail-in applications. Qualifying purchases exclude alcohol, Target GiftCards, prepaid Visa/Mastercard/Amex cards, CVS pharmacy, clinic and Target Optical.
21
21
  rules:
22
22
  - id: target
23
23
  match: { merchants: [target] }
24
24
  rate: 5
25
- note: An instant 5% discount at checkout in Target stores and on Target.com, after other discounts, on the part of the purchase paid with the card. Not on prescriptions, pharmacy-counter items or clinic services, Target Optical eye exams and protection plans (glasses and contacts do count), Target GiftCards or prepaid Visa/Mastercard/Amex cards, Starbucks gift cards bought at in-store Starbucks, Shipt or Target Circle 360 fees, taxes, shipping or other fees.
25
+ note: An instant 5% discount at checkout in Target stores and on Target.com, after other discounts, on the part of the purchase paid with the card. Not on prescriptions, pharmacy-counter items or clinic services, Target Optical eye exams and protection plans (glasses and contacts do count), Target GiftCards, prepaid Visa/Mastercard/Amex cards or sports betting gift cards, Starbucks gift cards bought at in-store Starbucks, Shipt or Target Circle 360 fees, wireless protection plans and carrier deposits, gift wrapping, alcohol in Indiana, taxes, shipping or other fees.
26
26
  todo:
27
27
  - The debit version (Target Circle Debit Card) gets the same 5% discount but isn't modeled as a separate card.
28
28
  - Target considers credit applicants for the Target Mastercard first, which also earns 2% on dining and gas and 1% elsewhere. Not modeled; target.com doesn't say what form those earnings take.
@@ -49,3 +49,12 @@ evidence:
49
49
  - rule: welcome_offer
50
50
  url: https://www.target.com/circlecard
51
51
  quote: 'within 60 days of approval'
52
+ - rule: target
53
+ url: https://www.target.com/c/target-circle-card-program-rules/-/N-4tgj0
54
+ quote: 'Game On, DraftsKings, and other sports betting gift cards'
55
+ - rule: target
56
+ url: https://www.target.com/c/target-circle-card-program-rules/-/N-4tgj0
57
+ quote: 'Wireless protection program purchases and deposits required by mobile carrier'
58
+ - rule: welcome_offer
59
+ url: https://www.target.com/circlecard
60
+ quote: 'Qualifying purchases exclude alcohol, Target GiftCards, Visa, American Express and Mastercard gift cards/prepaid cards'
@@ -3,8 +3,8 @@ name: U.S. Bank Cash+ Visa Signature Card
3
3
  issuer: usbank
4
4
  network: visa
5
5
  currency: cash
6
- last_verified: 2026-10-24
7
- verified_by: collin
6
+ last_verified: 2026-11-01
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
10
10
  - https://cashplus.usbank.com/cash-plus/samplemerchants
@@ -64,14 +64,16 @@ choices:
64
64
  rules:
65
65
  - id: five-percent
66
66
  match: { choice: five-percent }
67
+ # Ground transportation leaves out parking and tolls (also cruises and
68
+ # tickets bought through travel agencies); no other 5% option covers them.
69
+ exclude: { mccs: [7523, 4784] }
67
70
  rate: 5
68
71
  cap: five-percent-combined
69
72
  - id: two-percent
70
73
  match: { choice: two-percent }
71
- # Gas and grocery exclude discount stores/supercenters and wholesale clubs.
72
- # Grocery-store gas stations may still count as gas, per U.S. Bank's
73
- # sample merchants (Kroger Fuel Center is listed).
74
- exclude: { categories: [gas.superstore] }
74
+ # Fast food is its own 5% option, not part of Restaurants. Gas at grocery
75
+ # stores is left in until the todo below is settled.
76
+ exclude: { categories: [gas.superstore, dining.fast_food] }
75
77
  rate: 2
76
78
  - id: travel-center
77
79
  match: { merchants: [usbank-travel-center] }
@@ -94,9 +96,29 @@ evidence:
94
96
  - rule: five-percent
95
97
  url: https://cashplus.usbank.com/cash-plus/samplemerchants
96
98
  quote: 'This category includes only the merchants listed below'
99
+ - rule: five-percent
100
+ url: https://cashplus.usbank.com/cash-plus/samplemerchants
101
+ quote: 'steam ships, cruise lines, freight trains, parking meters, parking garages and toll stations'
102
+ - rule: five-percent
103
+ url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
104
+ quote: 'Purchases over $2,000 in the 5% categories will earn 1% cash back.'
105
+ - rule: two-percent
106
+ url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
107
+ quote: 'Gas station and electric vehicle charging station purchases at discount stores/supercenters, wholesale clubs and grocery stores/supermarkets'
108
+ - rule: two-percent
109
+ url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
110
+ quote: 'Excludes discount stores/supercenters and wholesale clubs'
111
+ - rule: two-percent
112
+ url: https://cashplus.usbank.com/cash-plus/samplemerchants
113
+ quote: "Excludes most fast food restaurants like McDonald's"
114
+ - rule: travel-center
115
+ url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
116
+ quote: 'additional 4% cash back for every $1 in eligible Net Purchases spent on prepaid air, car and hotel reservations'
97
117
  - rule: annual_fee
98
118
  url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
99
- quote: 'ANNUAL FEE $0 Annual fee'
119
+ quote: '$0 Annual fee'
100
120
  - rule: welcome_offer
101
121
  url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
102
122
  quote: 'Earn a $200 rewards bonus after you spend $1,000 in eligible purchases within the first 90 days of account opening'
123
+ todo:
124
+ - "Gas at grocery stores (Kroger, Hy-Vee, Meijer) is modeled at 2%, as U.S. Bank's sample merchants page says it may qualify, varying by location; the card page's footnote says 1%, so it may earn 1% at some stations."
@@ -3,10 +3,11 @@ name: Wells Fargo Active Cash Card
3
3
  issuer: wells-fargo
4
4
  network: visa
5
5
  currency: cash
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-11-01
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://creditcards.wellsfargo.com/active-cash-credit-card/
10
+ - https://www.wellsfargo.com/credit-cards/active-cash/terms/
10
11
  base_rate: 2
11
12
  annual_fee: 0
12
13
  welcome_offer:
@@ -23,6 +24,9 @@ evidence:
23
24
  - rule: base_rate
24
25
  url: https://creditcards.wellsfargo.com/active-cash-credit-card/
25
26
  quote: '2% cash rewards are earned for every $1 spent in net purchases'
27
+ - rule: base_rate
28
+ url: https://www.wellsfargo.com/credit-cards/active-cash/terms/
29
+ quote: 'For every $1.00 you spend with this Credit Card for net purchases, you earn two cents ($0.02) in Cash Rewards.'
26
30
  - rule: annual_fee
27
31
  url: https://creditcards.wellsfargo.com/active-cash-credit-card/
28
32
  quote: '$0 annual fee'
@@ -32,3 +36,6 @@ evidence:
32
36
  - rule: welcome_offer
33
37
  url: https://creditcards.wellsfargo.com/active-cash-credit-card/
34
38
  quote: 'To qualify for the $100 cash rewards bonus, a total of at least $500 in net purchases'
39
+ - rule: welcome_offer
40
+ url: https://www.wellsfargo.com/credit-cards/active-cash/terms/
41
+ quote: 'opened one within the last 48 months from the date of this application'
@@ -3,11 +3,12 @@ name: Wells Fargo Autograph Card
3
3
  issuer: wells-fargo
4
4
  network: visa
5
5
  currency: WFR
6
- last_verified: 2026-10-26
7
- verified_by: collin
6
+ last_verified: 2026-11-01
7
+ verified_by: claude
8
8
  source_urls:
9
9
  - https://creditcards.wellsfargo.com/autograph-visa-credit-card/
10
10
  - https://www.wellsfargo.com/credit-cards/autograph-visa/streaming/
11
+ - https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
11
12
  base_rate: 1
12
13
  annual_fee: 0
13
14
  welcome_offer:
@@ -18,8 +19,9 @@ welcome_offer:
18
19
  rules:
19
20
  - id: dining
20
21
  match: { categories: [dining] }
22
+ exclude: { merchants: [doordash, uber-eats] }
21
23
  rate: 3
22
- note: Restaurants, bars (drinking places), fast food and caterers, by Visa merchant code.
24
+ note: Restaurants, bars (drinking places), fast food and caterers, by Visa merchant code. Wells Fargo says bakeries, food at grocery stores and third-party delivery services may not earn 3X.
23
25
  - id: travel
24
26
  match: { categories: [travel] }
25
27
  rate: 3
@@ -31,43 +33,65 @@ rules:
31
33
  note: Passenger rail, taxis, limousines, ferries, tolls and parking. Intercity bus lines aren't on Wells Fargo's list.
32
34
  - id: gas
33
35
  match: { categories: [gas, ev_charging] }
36
+ exclude: { categories: [gas.superstore, gas.grocery_store] }
34
37
  rate: 3
35
- note: Gas stations, automated fuel dispensers and EV charging stations, by Visa merchant code.
38
+ note: Gas stations, automated fuel dispensers and EV charging stations, by Visa merchant code. Fuel or charging at superstores, warehouse clubs, grocery stores, car washes and auto repair shops may not earn 3X.
36
39
  - id: streaming
37
40
  match: { categories: [streaming, tv_internet] }
38
41
  rate: 3
39
- note: Cable and pay TV, digital goods (books, movies, music) and subscription services. Subscriptions bundled with another product or billed by a third party (an app store, a car) may not count. An internet-only bill may not code as pay TV.
42
+ note: Cable and pay TV, digital goods (books, movies, music) and subscription services. Subscriptions bundled with another product or billed by a third party (an app store, a cable, phone or internet provider, an online retailer, a car) may not count. An internet-only bill may not code as pay TV.
40
43
  - id: phone
41
44
  match: { categories: [phone] }
42
45
  rate: 3
43
- note: Landline and cell phone providers.
46
+ note: Landline and cell phone providers. Phones, accessories, add-ons like insurance or extra data, and plans bundled or billed by a third party (an internet provider, a car) may not count.
44
47
  # Not modeled: cell phone protection.
45
48
  # Issuer text backing each rule; `yarn check-sources` checks it weekly.
46
49
  evidence:
47
50
  - rule: base_rate
48
51
  url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
49
52
  quote: '1 rewards point will be earned per $1 spent on other net purchases'
53
+ - rule: travel
54
+ url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
55
+ quote: 'Airlines Hotel/motels Campgrounds Timeshares Cruise lines Travel agencies Discount travel sites Vehicle/auto rentals'
50
56
  - rule: dining
51
57
  url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
52
58
  quote: 'Dining: eating places and restaurants, drinking places, fast food restaurants'
59
+ - rule: dining
60
+ url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
61
+ quote: 'Grocery stores and other miscellaneous places that serve food or operate restaurants on site Third-party delivery services'
53
62
  - rule: travel
54
63
  url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
55
64
  quote: 'Travel: airline, hotel/motel, timeshare, or vehicle/auto rental, cruise lines'
56
65
  - rule: transit
57
66
  url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
58
- quote: 'Transit: passenger railway, taxis, limousines, ferries, toll bridges and highways'
67
+ quote: 'Transit: passenger railway, taxis, limousines, ferries, toll bridges and highways, parking lots and garages'
68
+ - rule: transit
69
+ url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
70
+ quote: 'Ferries Parking lots and garages Limousines Taxis Passenger railway Toll bridges and highways'
59
71
  - rule: gas
60
72
  url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
61
73
  quote: 'Gas: gas stations, automated fuel dispensers, and electric vehicle charging stations'
74
+ - rule: gas
75
+ url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
76
+ quote: 'Gas or electric vehicle charging purchases made at the following locations may not earn bonus Points'
77
+ - rule: gas
78
+ url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
79
+ quote: 'Auto repair stores Superstores Car washes Warehouse/membership clubs Grocery stores'
62
80
  - rule: streaming
63
81
  url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
64
82
  quote: 'Popular streaming services: cable and other pay television, digital goods, books'
65
83
  - rule: streaming
66
84
  url: https://www.wellsfargo.com/credit-cards/autograph-visa/streaming/
67
85
  quote: 'You may earn 3X on these streaming services'
86
+ - rule: streaming
87
+ url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
88
+ quote: 'Streaming services that a third party bills, such as a digital platform; a cable, telecommunications, or internet provider'
68
89
  - rule: phone
69
90
  url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
70
91
  quote: 'Phone plans: landline and cell phone providers considered telecommunication services'
92
+ - rule: phone
93
+ url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
94
+ quote: 'Phone-plan services that a third party bundles or bills, such as an internet provider or in-vehicle service'
71
95
  - rule: annual_fee
72
96
  url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
73
97
  quote: '$0 Annual Fee'
@@ -0,0 +1,32 @@
1
+ # The banks behind the cards: the name shown for each, and other words people
2
+ # search with ("bofa", "amex"). Every card's `issuer` must be one of these.
3
+
4
+ - id: amex
5
+ name: American Express
6
+ search: [amex, american express]
7
+ - id: bank-of-america
8
+ name: Bank of America
9
+ search: [bofa, boa, bankofamerica]
10
+ - id: capital-one
11
+ name: Capital One
12
+ search: [capitalone]
13
+ - id: chase
14
+ name: Chase
15
+ - id: citi
16
+ name: Citi
17
+ search: [citibank]
18
+ - id: column
19
+ # Column N.A. issues the Bilt card; people know it as Bilt.
20
+ name: Bilt
21
+ search: [bilt]
22
+ - id: sofi
23
+ name: SoFi
24
+ - id: td-bank
25
+ name: TD Bank
26
+ search: [td]
27
+ - id: usbank
28
+ name: U.S. Bank
29
+ search: [us bank]
30
+ - id: wells-fargo
31
+ name: Wells Fargo
32
+ search: [wf, wellsfargo]
@@ -104,6 +104,13 @@
104
104
  category: streaming,
105
105
  }
106
106
  - { id: max, name: Max, aliases: [hbo max], category: streaming }
107
+ - { id: peacock, name: Peacock, aliases: [peacock tv], category: streaming }
108
+ - {
109
+ id: showtime,
110
+ name: Showtime,
111
+ aliases: [paramount plus with showtime],
112
+ category: streaming,
113
+ }
107
114
  - { id: spotify, name: Spotify, category: streaming }
108
115
  - {
109
116
  id: apple-tv,
package/dist/brand.js CHANGED
@@ -5,6 +5,7 @@ export const MCP_SERVER_NAME = 'earnrate';
5
5
  export const CONFIG_DIR = '.earnrate'; // under the home directory
6
6
  export const WALLET_ENV_VAR = 'EARNRATE_WALLET';
7
7
  export const MERCHANT_CACHE_ENV_VAR = 'EARNRATE_MERCHANT_CACHE';
8
+ export const MISSED_ENV_VAR = 'EARNRATE_MISSED_MERCHANTS';
8
9
  export const LLM_ENV_VAR = 'EARNRATE_LLM';
9
10
  export const LLM_MODEL_ENV_VAR = 'EARNRATE_LLM_MODEL';
10
11
  export const LLM_MAX_CALLS_ENV_VAR = 'EARNRATE_LLM_MAX_CALLS';
package/dist/cli/bin.js CHANGED
@@ -1,8 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { styleText } from 'node:util';
3
+ import { CLI_NAME, WALLET_ENV_VAR } from "../brand.js";
3
4
  import { localToday } from "../engine/periods.js";
4
5
  import { run } from "./run.js";
5
- if (process.argv[2] === 'mcp') {
6
+ if (process.argv[2] === 'mcp' && /^(-h|--help)$/.test(process.argv[3] ?? '')) {
7
+ process.stdout.write(`Usage: ${CLI_NAME} mcp
8
+
9
+ Runs earnrate as an MCP server over stdio, for an AI assistant to start. It
10
+ uses the same wallet file as the command line (${WALLET_ENV_VAR}, or
11
+ ~/.earnrate/wallet.yaml).
12
+ `);
13
+ }
14
+ else if (process.argv[2] === 'mcp') {
6
15
  const { serveMcp } = await import("../mcp/serve.js");
7
16
  await serveMcp(process.env);
8
17
  }
@@ -15,6 +15,7 @@ name with --cards, or else from every card earnrate knows.
15
15
 
16
16
  Options:
17
17
  --cards <ids> Use these cards instead of your wallet, comma separated.
18
+ --every-card Pick from every card earnrate knows, even with a wallet.
18
19
  --member <name> A membership you have, like amazon-prime or costco. Repeatable.
19
20
  --valuation <profile> flat or points-heavy, instead of your wallet's values.
20
21
  --date <YYYY-MM-DD> The day to check. Default: today.
@@ -30,6 +31,7 @@ export async function runCheatSheet(argv, io) {
30
31
  valuation: { type: 'string' },
31
32
  date: { type: 'string' },
32
33
  cards: { type: 'string' },
34
+ 'every-card': { type: 'boolean' },
33
35
  member: { type: 'string', multiple: true },
34
36
  json: { type: 'boolean' },
35
37
  help: { type: 'boolean', short: 'h' },
@@ -45,7 +47,9 @@ export async function runCheatSheet(argv, io) {
45
47
  io.stdout(CHEATSHEET_HELP);
46
48
  return 0;
47
49
  }
48
- const problem = valuationProblem(values.valuation) ?? dateProblem(values.date);
50
+ const problem = values['every-card'] && values.cards !== undefined
51
+ ? 'Pick --cards or --every-card, not both.'
52
+ : (valuationProblem(values.valuation) ?? dateProblem(values.date));
49
53
  if (problem !== null) {
50
54
  io.stderr(`${problem}\n`);
51
55
  return USAGE_ERROR;
@@ -58,17 +62,18 @@ export async function runCheatSheet(argv, io) {
58
62
  if (loaded === null)
59
63
  return RUNTIME_ERROR;
60
64
  const date = values.date ?? io.today;
61
- if (loaded === 'none') {
65
+ if (loaded === 'none' || values['every-card']) {
66
+ const saved = await savedSettings(io);
62
67
  const sheet = catalogCheatSheet(catalog, date, {
63
68
  filters: ALL_PERSONAL_CARDS,
64
- memberships: values.member ?? [],
69
+ memberships: values.member ?? saved.memberships,
65
70
  valuations: values.valuation !== undefined && isValuationProfile(values.valuation)
66
71
  ? VALUATION_PROFILES[values.valuation]
67
- : (await savedSettings(io)).valuations,
72
+ : saved.valuations,
68
73
  });
69
74
  io.stdout(values.json
70
75
  ? `${JSON.stringify(sheet, null, 2)}\n`
71
- : `${formatCheatSheet(sheet)}\n${NO_WALLET_HINT}\n`);
76
+ : `${formatCheatSheet(sheet)}${loaded === 'none' ? `\n${NO_WALLET_HINT}\n` : ''}`);
72
77
  return 0;
73
78
  }
74
79
  const wallet = values.valuation !== undefined && isValuationProfile(values.valuation)
@@ -1,9 +1,9 @@
1
1
  import { parseArgs } from 'node:util';
2
2
  import { CLI_NAME } from "../brand.js";
3
3
  import { loadCatalog, walletPath } from "../data/load.js";
4
- import { ALL_PERSONAL_CARDS, exploreCards, findCategory, } from "../engine/explore.js";
4
+ import { ALL_PERSONAL_CARDS, exploreCards, } from "../engine/explore.js";
5
5
  import { isValuationProfile, profileText, VALUATION_PROFILES, withValuationProfile, } from "../engine/profiles.js";
6
- import { resolveSpendLines, simulateAdditions, simulateCatalog, spendLineFor, TYPICAL_MONTHLY_SPEND, TYPICAL_SPEND_NOTE, } from "../engine/simulate.js";
6
+ import { resolveSpendLines, simulateAdditions, simulateCatalog, spendLineFor, typicalSpend, TYPICAL_SPEND_NOTE, } from "../engine/simulate.js";
7
7
  import { formatAdditions, formatCatalogSimulation, formatExploration, } from "../format/explore.js";
8
8
  import { BOTH_CHANNELS, cardList, channelOf, dateProblem, membershipProblem, splitAmount, valuationProblem, } from "./args.js";
9
9
  import { cardsToUse, RUNTIME_ERROR, savedSettings, USAGE_ERROR, } from "./run.js";
@@ -141,10 +141,9 @@ export async function runBest(argv, io) {
141
141
  io.stderr(`${settings}\n`);
142
142
  return USAGE_ERROR;
143
143
  }
144
- const category = findCategory(catalog, text);
145
144
  const result = exploreCards(catalog, {
146
- merchant: category === null ? text : null,
147
- category,
145
+ merchant: text,
146
+ category: null,
148
147
  amount,
149
148
  channel: channelOf(values),
150
149
  date: values.date ?? io.today,
@@ -160,13 +159,7 @@ function parseSpend(catalog, args) {
160
159
  const match = /^(.+)=\$?(\d+(?:\.\d+)?)$/.exec(arg);
161
160
  if (!match?.[1] || match[2] === undefined)
162
161
  return `"${arg}" isn't <what>=<dollars a month>, e.g. groceries=600.`;
163
- const what = match[1].replace(/[-_]/g, ' ');
164
- const category = findCategory(catalog, what);
165
- lines.push({
166
- merchant: category === null ? what : null,
167
- category,
168
- monthly: Number(match[2]),
169
- });
162
+ lines.push(spendLineFor(catalog, match[1].replace(/[-_]/g, ' '), Number(match[2])));
170
163
  }
171
164
  if (lines.length === 0)
172
165
  return `Give your monthly spending, e.g. ${CLI_NAME} simulate groceries=600 dining=300 other=1000.`;
@@ -203,7 +196,7 @@ export async function runSimulate(argv, io) {
203
196
  }
204
197
  const typical = positionals.length === 0;
205
198
  const spend = typical
206
- ? TYPICAL_MONTHLY_SPEND.map((t) => spendLineFor(catalog, t.what, t.monthly))
199
+ ? typicalSpend(catalog)
207
200
  : parseSpend(catalog, positionals);
208
201
  if (typeof spend === 'string') {
209
202
  io.stderr(`${spend}\n`);
@@ -12,7 +12,7 @@ import { cardValuation } from "../engine/valuation.js";
12
12
  import { analyzeStatement, } from "../import/analyze.js";
13
13
  import { parseStatement } from "../import/statement.js";
14
14
  import { displayPath, readWallet, RUNTIME_ERROR, USAGE_ERROR, } from "./run.js";
15
- import { dollarsAndCents } from "../engine/text.js";
15
+ import { currencyName, dollarsAndCents } from "../engine/text.js";
16
16
  const IMPORT_HELP = `Usage: ${CLI_NAME} import <statement.csv> --card <card-id> [--apply]
17
17
  ${CLI_NAME} import <statement.csv>:<card-id> [<statement.csv>:<card-id> ...]
18
18
 
@@ -64,7 +64,7 @@ function valuationNote(catalog, wallet, cardIds, profile) {
64
64
  }
65
65
  if (values.size === 0)
66
66
  return 'Cash back counted as cash.';
67
- const parts = [...values].map(([id, cents]) => `${String(cents)}¢ for ${catalog.currencies.find((c) => c.id === id)?.name ?? id}`);
67
+ const parts = [...values].map(([id, cents]) => `${String(cents)}¢ for ${currencyName(catalog, id)}`);
68
68
  const list = parts.length > 1
69
69
  ? `${parts.slice(0, -1).join(', ')} and ${parts.at(-1) ?? ''}`
70
70
  : (parts[0] ?? '');
@@ -0,0 +1,65 @@
1
+ import { rm } from 'node:fs/promises';
2
+ import { parseArgs } from 'node:util';
3
+ import { CLI_NAME, MISSED_ENV_VAR } from "../brand.js";
4
+ import { loadCatalog } from "../data/load.js";
5
+ import { categoryPhrase, shortDate } from "../engine/text.js";
6
+ import { missedPath, readMissed } from "../merchants/missed.js";
7
+ import { displayPath, USAGE_ERROR } from "./run.js";
8
+ const MISSED_HELP = `Usage: ${CLI_NAME} missed [--clear]
9
+
10
+ Stores earnrate didn't recognize when you asked about them here (the command
11
+ line or the local MCP server), most asked about first. They're kept only on
12
+ this computer. To suggest one for earnrate's data, open an issue at
13
+ https://github.com/cwasley/earnrate/issues.
14
+
15
+ Set ${MISSED_ENV_VAR}=off to stop keeping the list, or to a path to keep it
16
+ somewhere else.
17
+
18
+ Options:
19
+ --clear Delete the list.
20
+ -h, --help Show this help.
21
+ `;
22
+ export async function runMissed(argv, io) {
23
+ let parsed;
24
+ try {
25
+ parsed = parseArgs({
26
+ args: argv,
27
+ options: {
28
+ clear: { type: 'boolean' },
29
+ help: { type: 'boolean', short: 'h' },
30
+ },
31
+ });
32
+ }
33
+ catch (e) {
34
+ io.stderr(`${e instanceof Error ? e.message : String(e)}\n${MISSED_HELP}`);
35
+ return USAGE_ERROR;
36
+ }
37
+ if (parsed.values.help) {
38
+ io.stdout(MISSED_HELP);
39
+ return 0;
40
+ }
41
+ const file = missedPath(io.env);
42
+ if (file === null) {
43
+ io.stdout(`The list is off (${MISSED_ENV_VAR}=off).\n`);
44
+ return 0;
45
+ }
46
+ if (parsed.values.clear) {
47
+ await rm(file, { force: true });
48
+ io.stdout(`Deleted ${displayPath(file)}.\n`);
49
+ return 0;
50
+ }
51
+ const missed = await readMissed(file);
52
+ if (missed.length === 0) {
53
+ io.stdout('No unrecognized stores yet.\n');
54
+ return 0;
55
+ }
56
+ const catalog = await loadCatalog();
57
+ const lines = missed
58
+ .sort((a, b) => b.count - a.count || b.last_seen.localeCompare(a.last_seen))
59
+ .map((m) => {
60
+ const kinds = m.categories.map((c) => categoryPhrase(catalog, c));
61
+ return `- ${m.name}: ${String(m.count)} ${m.count === 1 ? 'time' : 'times'}, last ${shortDate(m.last_seen)}${kinds.length === 0 ? '' : ` (taken as ${kinds.join(', ')})`}`;
62
+ });
63
+ io.stdout(`${lines.join('\n')}\n\nKept in ${displayPath(file)}. Suggest additions at https://github.com/cwasley/earnrate/issues.\n`);
64
+ return 0;
65
+ }
@@ -3,9 +3,10 @@ import { CLI_NAME } from "../brand.js";
3
3
  import { loadCatalog, walletPath } from "../data/load.js";
4
4
  import { compareOffers } from "../engine/offers.js";
5
5
  import { isValuationProfile, withValuationProfile } from "../engine/profiles.js";
6
+ import { currencyName } from "../engine/text.js";
6
7
  import { formatOffers } from "../format/offers.js";
7
- import { valuationProblem } from "./args.js";
8
- import { cardsToUse, RUNTIME_ERROR, USAGE_ERROR } from "./run.js";
8
+ import { cardList, valuationProblem } from "./args.js";
9
+ import { cardsToUse, RUNTIME_ERROR, savedSettings, USAGE_ERROR, } from "./run.js";
9
10
  const OFFERS_HELP = `Usage: ${CLI_NAME} offers [--cash | --points] [--no-fee] [--business] [options]
10
11
 
11
12
  Ranks the welcome offers on cards you don't have by first-year value: the
@@ -18,6 +19,8 @@ Options:
18
19
  --no-fee Cards with no annual fee.
19
20
  --business Business cards instead of personal ones.
20
21
  --held Include cards already in your wallet.
22
+ --cards <ids> Cards you have, instead of your wallet, comma separated.
23
+ --member <name> A membership you have, in place of the saved ones.
21
24
  --valuation <profile> flat or points-heavy, instead of your wallet's values.
22
25
  --all List every offer, not just the top 10.
23
26
  --json Print the full comparison as JSON.
@@ -34,6 +37,8 @@ export async function runOffers(argv, io) {
34
37
  'no-fee': { type: 'boolean' },
35
38
  business: { type: 'boolean' },
36
39
  held: { type: 'boolean' },
40
+ cards: { type: 'string' },
41
+ member: { type: 'string', multiple: true },
37
42
  valuation: { type: 'string' },
38
43
  all: { type: 'boolean' },
39
44
  json: { type: 'boolean' },
@@ -61,12 +66,18 @@ export async function runOffers(argv, io) {
61
66
  }
62
67
  const catalog = await loadCatalog();
63
68
  const found = await cardsToUse(catalog, walletPath(io.env), io, {
64
- cards: null,
65
- members: [],
69
+ cards: cardList(values.cards),
70
+ members: values.member ?? [],
66
71
  });
67
72
  if (found === null)
68
73
  return RUNTIME_ERROR;
69
- const loaded = found === 'none' ? { valuations: {}, memberships: [], cards: [] } : found;
74
+ const loaded = found === 'none'
75
+ ? await savedSettings(io).then((saved) => ({
76
+ valuations: saved.valuations,
77
+ memberships: values.member ?? saved.memberships,
78
+ cards: [],
79
+ }))
80
+ : found;
70
81
  const wallet = values.valuation && isValuationProfile(values.valuation)
71
82
  ? withValuationProfile(loaded, values.valuation)
72
83
  : loaded;
@@ -80,7 +91,6 @@ export async function runOffers(argv, io) {
80
91
  io.stdout(`${JSON.stringify(comparison, null, 2)}\n`);
81
92
  return 0;
82
93
  }
83
- const currencyName = (id) => catalog.currencies.find((c) => c.id === id)?.name ?? id;
84
- io.stdout(formatOffers(comparison, currencyName, values.all ?? false));
94
+ io.stdout(formatOffers(comparison, (id) => currencyName(catalog, id), values.all ?? false));
85
95
  return 0;
86
96
  }