earnrate 0.2.1 → 0.2.2

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.
package/README.md CHANGED
@@ -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)'
@@ -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 };
@@ -1,4 +1,4 @@
1
- import { pickedCategories } from "./evaluate.js";
1
+ import { isActivated, pickedCategories } from "./evaluate.js";
2
2
  import { daysBetween } from "./periods.js";
3
3
  import { rotationGap } from "./rotation.js";
4
4
  // Rules that start within this many days are worth activating now.
@@ -144,7 +144,7 @@ export function setupNotes(catalog, wallet, held, today) {
144
144
  text: `Some of ${name}'s rates need a membership: ${list(missing)}. Add any that apply to the wallet's memberships.`,
145
145
  });
146
146
  }
147
- const toActivate = activationRules(card, today).filter((id) => !held.activated.includes(id));
147
+ const toActivate = activationRules(card, today).filter((id) => !isActivated(card, held, id));
148
148
  if (toActivate.length > 0) {
149
149
  notes.push({
150
150
  kind: 'activation',
@@ -1,3 +1,4 @@
1
+ import { capByKey } from "./evaluate.js";
1
2
  import { choicePeriod, periodKey } from "./periods.js";
2
3
  import { knownMemberships, searchCards, setupNotes, } from "./setup.js";
3
4
  const failed = (error) => ({ ok: false, error });
@@ -25,7 +26,9 @@ function pickKey(card, today) {
25
26
  export function cardFields(card, held, input, today) {
26
27
  const problems = [];
27
28
  const fields = {};
28
- if (input.checkoutLabel !== undefined) {
29
+ if (input.checkoutLabel === null)
30
+ fields.checkout_label = null;
31
+ else if (input.checkoutLabel !== undefined) {
29
32
  if (input.checkoutLabel.trim() === '')
30
33
  problems.push('The checkout label is empty.');
31
34
  // earnrate never stores card numbers; a last four is fine.
@@ -83,6 +86,34 @@ export function cardFields(card, held, input, today) {
83
86
  };
84
87
  }
85
88
  }
89
+ if (input.capSpent !== undefined) {
90
+ const usage = { ...held?.cap_usage };
91
+ const anniversary = fields.anniversary ?? held?.anniversary ?? null;
92
+ for (const [key, spent] of Object.entries(input.capSpent)) {
93
+ const cap = capByKey(card, key);
94
+ if (!cap) {
95
+ const keys = [
96
+ ...card.caps.map((c) => c.id),
97
+ ...card.rules
98
+ .filter((r) => typeof r.cap === 'object')
99
+ .map((r) => r.id),
100
+ ];
101
+ problems.push(keys.length > 0
102
+ ? `${card.id} has no cap '${key}'. Its caps are: ${list(keys)}.`
103
+ : `${card.id} has no spending caps.`);
104
+ continue;
105
+ }
106
+ if (!(spent >= 0))
107
+ problems.push('Spending on a cap must be $0 or more.');
108
+ const period = periodKey(cap.period, today, anniversary);
109
+ if (period === null) {
110
+ problems.push(`The '${key}' cap resets on the card's anniversary; give the anniversary (MM-DD) too.`);
111
+ continue;
112
+ }
113
+ usage[key] = { period, spent };
114
+ }
115
+ fields.cap_usage = usage;
116
+ }
86
117
  if (input.bonus === null)
87
118
  fields.bonus = null;
88
119
  else if (input.bonus !== undefined) {
@@ -115,15 +146,17 @@ const newHolding = (id) => ({
115
146
  cap_usage: {},
116
147
  activated: [],
117
148
  });
118
- const withFields = ({ bonus, ...held }, f) => ({
149
+ const withFields = ({ bonus, checkout_label, ...held }, f) => ({
119
150
  ...held,
120
151
  ...(bonus && f.bonus === undefined ? { bonus } : {}),
121
- ...(f.checkout_label === undefined
122
- ? {}
123
- : { checkout_label: f.checkout_label }),
152
+ ...(checkout_label !== undefined && f.checkout_label === undefined
153
+ ? { checkout_label }
154
+ : {}),
155
+ ...(f.checkout_label ? { checkout_label: f.checkout_label } : {}),
124
156
  ...(f.anniversary === undefined ? {} : { anniversary: f.anniversary }),
125
157
  ...(f.chosen === undefined ? {} : { chosen: f.chosen }),
126
158
  ...(f.activated === undefined ? {} : { activated: f.activated }),
159
+ ...(f.cap_usage === undefined ? {} : { cap_usage: f.cap_usage }),
127
160
  ...(f.bonus ? { bonus: f.bonus } : {}),
128
161
  });
129
162
  // `wallet` is null when there isn't one yet; adding a card starts it.
@@ -1,10 +1,17 @@
1
1
  import { feeText, money } from "../engine/text.js";
2
2
  const TOP = 10;
3
3
  function reward(o, currencyName) {
4
- if (o.dollars !== null)
5
- return money(o.dollars);
6
- return `${(o.points ?? 0).toLocaleString('en-US')} ${currencyName}`;
4
+ const amount = o.dollars !== null
5
+ ? money(o.dollars)
6
+ : `${(o.points ?? 0).toLocaleString('en-US')} ${currencyName}`;
7
+ return o.varies ? `Up to ${amount}` : amount;
7
8
  }
9
+ const asOf = (date) => new Date(`${date}T12:00:00Z`).toLocaleDateString('en-US', {
10
+ month: 'short',
11
+ day: 'numeric',
12
+ year: 'numeric',
13
+ timeZone: 'UTC',
14
+ });
8
15
  export function formatOffers(c, currencyName, all) {
9
16
  if (c.offers.length === 0) {
10
17
  return `No published welcome offers match.${c.withoutOffer.length > 0 ? ` ${String(c.withoutOffer.length)} matching card(s) have none on file.` : ''}\n`;
@@ -19,8 +26,7 @@ export function formatOffers(c, currencyName, all) {
19
26
  ? `after ${money(o.spend)} in ${String(o.months)} month${o.months === 1 ? '' : 's'}${pace}`
20
27
  : 'with no spending required';
21
28
  lines.push(`${String(i + 1).padStart(2)}. ${o.cardName.padEnd(width)} about ${money(Math.round(o.firstYearValue))} the first year`, ` ${reward(o, currencyName(o.currency))} ${spend}. ${feeText(o)[0]?.toUpperCase() ?? ''}${feeText(o).slice(1)}.${o.held ? ' In your wallet.' : ''}`);
22
- if (o.validTo)
23
- lines.push(` Offer ends ${o.validTo}.`);
29
+ lines.push(` ${o.validTo ? `Offer ends ${o.validTo}. ` : ''}Public offer as of ${asOf(o.lastVerified)}; yours may differ.`);
24
30
  if (o.note)
25
31
  lines.push(` ${o.note}`);
26
32
  });
@@ -1,26 +1,45 @@
1
1
  import { money } from "../engine/text.js";
2
+ const NETWORKS = {
3
+ visa: 'Visa',
4
+ mastercard: 'Mastercard',
5
+ amex: 'American Express',
6
+ discover: 'Discover',
7
+ store: 'store card',
8
+ };
9
+ const words = (id) => id.replace(/[-_.]/g, ' ');
2
10
  export function formatWalletSummary(summary) {
3
- return summary.cards
4
- .map((c) => {
5
- const caps = c.caps
6
- .filter((cap) => cap.remaining !== null)
7
- .map((cap) => `${cap.key} ${money(cap.spent)} of ${money(cap.amount)} used for ${cap.currentPeriod ?? ''}`);
8
- const chosen = c.chosen
9
- ? `; chosen categories for ${c.chosen.period}${c.chosen.current ? '' : ' (out of date)'}`
10
- : '';
11
- const onlyAt = c.onlyAt ? `; works only at ${c.onlyAt.join(', ')}` : '';
12
- const rotation = c.rotationGap === null
13
- ? ''
14
- : `; rotating categories for ${c.rotationGap} not in earnrate yet`;
11
+ const cards = summary.cards.map((c) => {
12
+ const lines = [
13
+ `- ${c.checkoutLabel ?? c.cardName} (${c.cardId}): ${NETWORKS[c.network] ?? c.network}, ${c.currency} at ${String(c.centsPerUnit)}¢. Data checked ${c.lastVerified}${c.dataIsStale ? ' (old; check the issuer)' : ''}.`,
14
+ ];
15
+ if (c.onlyAt)
16
+ lines.push(` Works only at ${c.onlyAt.join(', ')}.`);
17
+ if (c.chosen) {
18
+ const picks = Object.entries(c.chosen.picks)
19
+ .map(([choice, options]) => `${words(choice)}: ${options.map(words).join(', ')}`)
20
+ .join('; ');
21
+ lines.push(c.chosen.current
22
+ ? ` Picks for ${c.chosen.period}: ${picks}. They need choosing again when the period ends.`
23
+ : ` Picks are for ${c.chosen.period}, which is over: ask for this period's picks.`);
24
+ }
25
+ if (c.activated.length > 0)
26
+ lines.push(` Activated: ${c.activated.join(', ')}.`);
27
+ if (c.rotationGap !== null)
28
+ lines.push(` Rotating categories for ${c.rotationGap} aren't in earnrate yet.`);
29
+ for (const cap of c.caps)
30
+ if (cap.remaining !== null)
31
+ lines.push(` Cap ${cap.key}: ${money(cap.spent)} of ${money(cap.amount)} used for ${cap.currentPeriod ?? ''}.`);
15
32
  const b = c.bonus;
16
- const bonus = b?.state === 'open' && b.spend !== null
17
- ? `; welcome bonus ${money(b.spent)} of ${money(b.spend)} spent, due ${b.deadline ?? ''}`
18
- : b
19
- ? `; welcome bonus ${b.state}`
20
- : '';
21
- return `- ${c.checkoutLabel ?? c.cardName} (${c.cardId}): ${c.currency} at ${String(c.centsPerUnit)}¢${caps.length > 0 ? `; ${caps.join(', ')}` : ''}${chosen}${rotation}${onlyAt}${bonus}`;
22
- })
23
- .join('\n');
33
+ if (b?.state === 'open' && b.spend !== null)
34
+ lines.push(` Welcome bonus: ${money(b.spent)} of ${money(b.spend)} spent, due ${b.deadline ?? ''}.`);
35
+ else if (b)
36
+ lines.push(` Welcome bonus: ${b.state}.`);
37
+ return lines.join('\n');
38
+ });
39
+ const memberships = summary.memberships.length > 0
40
+ ? `Memberships: ${summary.memberships.join(', ')}.`
41
+ : 'Memberships: none saved.';
42
+ return [...cards, memberships].join('\n');
24
43
  }
25
44
  export function formatBooking(amount, cardId, cap, bonus) {
26
45
  const messages = [];
@@ -46,8 +46,9 @@ const cardFields = {
46
46
  checkoutLabel: z
47
47
  .string()
48
48
  .min(1)
49
+ .nullable()
49
50
  .optional()
50
- .describe('How the card appears at checkout or in a browser wallet, e.g. "Amex Gold". Optional.'),
51
+ .describe('How the card appears at checkout or in a browser wallet, e.g. "Amex Gold". Optional; null goes back to the card\'s own name.'),
51
52
  picks: z
52
53
  .record(z.string(), z.array(z.string()).min(1))
53
54
  .optional()
@@ -61,6 +62,10 @@ const cardFields = {
61
62
  .array(z.string())
62
63
  .optional()
63
64
  .describe('Rule ids the user has activated with the issuer, such as rotating categories. Replaces the current list.'),
65
+ capSpent: z
66
+ .record(z.string(), z.number().nonnegative())
67
+ .optional()
68
+ .describe('Dollars already spent this period on a capped bonus, for a card the user starts tracking partway through: cap key (as list_wallet shows it) → dollars, e.g. {"five-percent-quarterly": 400}. Ask; recorded purchases add to it from then on.'),
64
69
  bonus: z
65
70
  .object({
66
71
  opened: IsoDate.describe('The day the account opened, YYYY-MM-DD.'),
@@ -95,10 +100,12 @@ export function cardInput({ cardId, bonus, ...rest }) {
95
100
  }
96
101
  // Inputs that list the catalog's categories, issuers and memberships.
97
102
  export function makeInputs(catalog) {
98
- const categoryIds = catalog.categories.map((c) => c.id);
99
- const categoryList = catalog.categories
100
- .map((c) => `${c.id} (${c.name})`)
101
- .join(', ');
103
+ // A parent with subcategories and no merchant codes of its own ("Grocery")
104
+ // isn't something a purchase codes as, so it's left out.
105
+ const categories = catalog.categories.filter((c) => c.mccs.length > 0 ||
106
+ !catalog.categories.some((k) => k.id.startsWith(`${c.id}.`)));
107
+ const categoryIds = categories.map((c) => c.id);
108
+ const categoryList = categories.map((c) => `${c.id} (${c.name})`).join(', ');
102
109
  const membershipIds = knownMemberships(catalog);
103
110
  const issuers = [...new Set(catalog.cards.map((c) => c.issuer))].sort();
104
111
  const CategoryInput = z
@@ -161,6 +161,7 @@ export const OfferComparisonSchema = z.object({
161
161
  spend: z.number(),
162
162
  months: z.number(),
163
163
  validTo: z.string().nullable(),
164
+ varies: z.boolean(), // the amount is the most the issuer shows
164
165
  note: z.string().nullable(),
165
166
  annualFee: z.number().nullable(),
166
167
  firstYearFee: z.number().nullable(),
@@ -34,7 +34,7 @@ const SAVING = {
34
34
  };
35
35
  const instructions = (savedWallets) => `earnrate answers which credit card earns the most for a purchase, from each issuer's own terms. Anyone can use it without entering anything: with no cards given, best_card_for_purchase, best_cards_for, cheat_sheet, simulate_spending and compare_welcome_offers answer from every card earnrate knows, and that's a complete answer. Never require the user's cards, and don't ask what they spend unless the answer depends on it; simulate_spending assumes typical amounts when none are given and says so.
36
36
 
37
- Personalizing is optional. If the user names their cards, pass them as \`cards\` (ids from search_cards) for that answer; nothing is saved. ${SAVING[savedWallets]} When a card needs category picks, memberships like Amazon Prime, or activations, ask the user rather than guessing.
37
+ Personalizing is optional. If the user names their cards, pass them as \`cards\` (ids from search_cards) for that answer; nothing is saved. After answering, offer once to save those cards so earnrate remembers them, and save them with add_card only if the user says yes. ${SAVING[savedWallets]} When a card needs category picks, memberships like Amazon Prime, or activations, ask the user rather than guessing.
38
38
 
39
39
  The wallet lists cards by product id, never card numbers; never ask for card numbers, expiry dates or security codes.`;
40
40
  export function createEarnrateServer(deps) {
@@ -23,13 +23,17 @@ export function registerAccountTools(server, catalog, store, ctx) {
23
23
  });
24
24
  server.registerTool('import_wallet', {
25
25
  title: 'Import a wallet',
26
- description: 'Replaces the saved wallet with a wallet.yaml from the earnrate command line or earnrate.dev, when the user pastes or attaches one. Wallet files only: never statements or card numbers. Confirm with the user first if they already have cards saved.',
26
+ description: 'Replaces the saved wallet with a wallet.yaml from the earnrate command line or earnrate.dev, when the user pastes or attaches one. Wallet files only: never statements or card numbers. If cards are already saved, it refuses unless replace is true: confirm with the user first.',
27
27
  inputSchema: z.object({
28
28
  yaml: z
29
29
  .string()
30
30
  .min(1)
31
31
  .max(100_000)
32
32
  .describe('The wallet.yaml text, as the user gave it.'),
33
+ replace: z
34
+ .boolean()
35
+ .optional()
36
+ .describe('true to replace a wallet that already has cards, after the user confirms.'),
33
37
  }),
34
38
  outputSchema: WalletChangeSchema,
35
39
  annotations: {
@@ -38,10 +42,13 @@ export function registerAccountTools(server, catalog, store, ctx) {
38
42
  idempotentHint: true,
39
43
  openWorldHint: false,
40
44
  },
41
- }, async ({ yaml }) => {
45
+ }, async ({ yaml, replace }) => {
42
46
  const parsed = parseWalletYaml(yaml, catalog);
43
47
  if (typeof parsed === 'string')
44
48
  return fail(parsed);
49
+ const current = await store.load().catch(() => null);
50
+ if (current !== null && current.cards.length > 0 && replace !== true)
51
+ return fail(`This would replace the ${String(current.cards.length)} ${current.cards.length === 1 ? 'card' : 'cards'} already saved (${current.cards.map((c) => c.id).join(', ')}). Ask the user, then call again with replace: true.`);
45
52
  const count = parsed.cards.length;
46
53
  return ctx.changed(await store.replace(parsed), `Imported a wallet with ${String(count)} ${count === 1 ? 'card' : 'cards'}.`, { notes: false });
47
54
  });
@@ -125,6 +125,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
125
125
  description: "Ranks the public welcome (sign-up) offers on cards the user doesn't have by first-year value: the bonus at the user's point values minus the first year's annual fee. Use it when the user asks which new card to get or which has the best sign-up bonus. It covers only the cards earnrate knows, offers vary by person, and each offer's note gives the issuer's eligibility rules; pass those caveats on. It doesn't account for what a card would earn on the user's spending.",
126
126
  inputSchema: z.object({
127
127
  ...OfferFilters,
128
+ valuation: CatalogFilters.valuation,
128
129
  includeHeld: z
129
130
  .boolean()
130
131
  .optional()
@@ -137,7 +138,8 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
137
138
  const wallet = await ctx.cardsFor(args.cards, undefined);
138
139
  if (typeof wallet === 'string')
139
140
  return fail(wallet);
140
- const comparison = compareOffers(catalog, wallet ?? EMPTY_WALLET, {
141
+ const base = wallet ?? EMPTY_WALLET;
142
+ const comparison = compareOffers(catalog, args.valuation ? withValuationProfile(base, args.valuation) : base, {
141
143
  kind: args.kind ?? 'any',
142
144
  noAnnualFee: args.noAnnualFee ?? false,
143
145
  business: args.business ?? false,
@@ -273,11 +275,16 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
273
275
  });
274
276
  server.registerTool('cheat_sheet', {
275
277
  title: 'Cheat sheet',
276
- description: "The card to use for each everyday kind of purchase right now: groceries, dining, gas, Amazon, Costco, travel, streaming, drugstores and everything else. With no cards given and no saved wallet, it picks from every card earnrate knows (`scope: all cards`, each row noting the card's fee and conditions). With cards named in the conversation or a saved wallet, it picks from the user's own cards, with caps, picks and activations as they are. For one specific store, call best_card_for_purchase instead. Pass on the notes.",
278
+ description: "The card to use for each everyday kind of purchase right now: groceries, dining, gas, Amazon, Costco, travel, streaming, drugstores and everything else. With no cards given and no saved wallet, it picks from every card earnrate knows (`scope: all cards`, each row noting the card's fee and conditions). With cards named in the conversation or a saved wallet, it picks from the user's own cards, with caps, picks and activations as they are. The kind, noAnnualFee, business and issuer filters apply only when it picks from every card. For one specific store, call best_card_for_purchase instead. Pass on the notes.",
277
279
  inputSchema: z.object({
278
280
  date: DateInput,
279
281
  cards: NamedCards,
280
282
  memberships: NamedMemberships,
283
+ kind: CatalogFilters.kind,
284
+ noAnnualFee: CatalogFilters.noAnnualFee,
285
+ business: CatalogFilters.business,
286
+ issuer: CatalogFilters.issuer,
287
+ valuation: CatalogFilters.valuation,
281
288
  }),
282
289
  outputSchema: CheatSheetSchema,
283
290
  annotations: { readOnlyHint: true, openWorldHint: false },
@@ -288,10 +295,12 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
288
295
  const date = args.date ?? deps.today();
289
296
  const sheet = wallet === null
290
297
  ? {
291
- ...catalogCheatSheet(catalog, date, settings({ memberships: args.memberships })),
298
+ ...catalogCheatSheet(catalog, date, settings(args)),
292
299
  notes: [ALL_CARDS_NOTE],
293
300
  }
294
- : cheatSheet(catalog, wallet, date);
301
+ : cheatSheet(catalog, args.valuation
302
+ ? withValuationProfile(wallet, args.valuation)
303
+ : wallet, date);
295
304
  return {
296
305
  content: [{ type: 'text', text: formatCheatSheet(sheet) }],
297
306
  structuredContent: sheet,
@@ -75,6 +75,10 @@ export const WelcomeOfferSchema = z
75
75
  spend: z.number().nonnegative(), // dollars of purchases required
76
76
  months: z.number().int().positive(), // from account opening
77
77
  valid_to: IsoDate.optional(), // when the issuer says the offer ends
78
+ // The issuer shows each applicant their own amount ("as high as"), or
79
+ // the page showed different offers on different visits. The amount
80
+ // above is then the most it shows.
81
+ varies: z.literal(true).optional(),
78
82
  note: z.string().optional(),
79
83
  })
80
84
  .refine((o) => (o.points === undefined) !== (o.dollars === undefined), 'a welcome offer needs points or dollars, not both');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "earnrate",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Which credit card earns the most at a merchant, from your cards or every card, and why",
5
5
  "keywords": [
6
6
  "credit cards",
@@ -37,6 +37,7 @@
37
37
  "earnrate": "node src/cli/bin.ts",
38
38
  "mcp": "node src/mcp/bin.ts",
39
39
  "check-sources": "node src/sources/bin.ts",
40
+ "usage": "node scripts/usage.ts",
40
41
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
41
42
  "prepack": "yarn build",
42
43
  "smoke:pack": "node scripts/smoke-pack.ts",