earnrate 0.2.10 → 0.2.11
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 +14 -6
- package/data/CHANGELOG.md +44 -0
- package/data/VERSION +1 -1
- package/data/cards/amex-delta-skymiles-gold.yaml +120 -0
- package/data/cards/amex-delta-skymiles-platinum.yaml +118 -0
- package/data/cards/amex-delta-skymiles-reserve.yaml +80 -0
- package/data/cards/amex-hilton-honors-aspire.yaml +140 -0
- package/data/cards/amex-hilton-honors-surpass.yaml +151 -0
- package/data/cards/amex-hilton-honors.yaml +117 -0
- package/data/cards/amex-marriott-bonvoy-bevy.yaml +116 -0
- package/data/cards/amex-marriott-bonvoy-brilliant.yaml +91 -0
- package/data/cards/amex-platinum.yaml +14 -0
- package/data/cards/bank-of-america-atmos-rewards-ascent.yaml +13 -0
- package/data/cards/bank-of-america-atmos-rewards-business.yaml +13 -0
- package/data/cards/bank-of-america-atmos-rewards-summit.yaml +16 -0
- package/data/cards/capital-one-quicksilver.yaml +80 -0
- package/data/cards/chase-aeroplan.yaml +28 -0
- package/data/cards/chase-marriott-bonvoy-bold.yaml +123 -0
- package/data/cards/chase-marriott-bonvoy-boundless.yaml +76 -0
- package/data/cards/chase-sapphire-reserve.yaml +29 -0
- package/data/cards/chase-united-business.yaml +10 -0
- package/data/cards/chase-united-club-business.yaml +19 -0
- package/data/cards/chase-united-club.yaml +31 -0
- package/data/cards/chase-united-explorer.yaml +19 -0
- package/data/cards/chase-united-gateway.yaml +18 -0
- package/data/cards/chase-united-quest.yaml +19 -0
- package/data/cards/citi-aadvantage-business.yaml +12 -1
- package/data/cards/citi-aadvantage-platinum-select.yaml +12 -1
- package/data/cards/discover-it-cash-back.yaml +169 -0
- package/data/currencies.yaml +26 -0
- package/data/issuers.yaml +3 -0
- package/data/merchants/overrides.yaml +16 -0
- package/data/merchants/seed.yaml +40 -0
- package/dist/engine/cheat-sheet.js +3 -0
- package/dist/engine/recommend.js +10 -8
- package/dist/mcp/prompts.js +90 -0
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/tools/account.js +5 -4
- package/dist/mcp/tools/answers.js +7 -0
- package/dist/mcp/tools/wallet.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
id: discover-it-cash-back
|
|
2
|
+
name: Discover it Cash Back Credit Card
|
|
3
|
+
# Discover merged into Capital One, N.A.; the application terms are headed
|
|
4
|
+
# "Discover by Capital One" and served from disclosures.capitalone.com.
|
|
5
|
+
issuer: discover
|
|
6
|
+
network: discover
|
|
7
|
+
currency: cash
|
|
8
|
+
last_verified: 2026-11-03
|
|
9
|
+
verified_by: claude
|
|
10
|
+
source_urls:
|
|
11
|
+
- https://www.discover.com/credit-cards/cash-back/it-card/
|
|
12
|
+
# The card page's "See rates, rewards, fees, and other cost information"
|
|
13
|
+
# (pricing and rewards FAQ); the number is the page's offer id and changes
|
|
14
|
+
# when Capital One changes it.
|
|
15
|
+
- https://disclosures.capitalone.com/disclosure.42300.en-US.html
|
|
16
|
+
- https://www.discover.com/credit-cards/cash-back/cashback-calendar.html
|
|
17
|
+
- https://www.discover.com/credit-cards/cash-back/cashback-bonus.html
|
|
18
|
+
# The calendar page's "See our FAQs".
|
|
19
|
+
- https://www.discover.com/credit-cards/card-smarts/using-cashback-bonus/
|
|
20
|
+
base_rate: 1
|
|
21
|
+
annual_fee: 0
|
|
22
|
+
# No page states it: the application terms' fee table lists no foreign
|
|
23
|
+
# transaction fee row, but nothing says "None".
|
|
24
|
+
foreign_transaction_fee: null
|
|
25
|
+
welcome_offer:
|
|
26
|
+
dollars: 200
|
|
27
|
+
spend: 500
|
|
28
|
+
months: 3
|
|
29
|
+
note: One-time $200 statement credit, available by applying through the card page. Existing or previous cardmembers may not be eligible if they received a new cardmember bonus for this product in the past 48 months. New cardmembers also get Cashback Match (all cash back earned in the first year matched), which isn't counted here.
|
|
30
|
+
caps:
|
|
31
|
+
# Discover's quarterly maximum on the rotating 5% categories.
|
|
32
|
+
- id: five-percent-quarterly
|
|
33
|
+
amount: 1500
|
|
34
|
+
period: quarter
|
|
35
|
+
then_rate: 1
|
|
36
|
+
rotating: true
|
|
37
|
+
rules:
|
|
38
|
+
- id: capital-one-travel
|
|
39
|
+
match:
|
|
40
|
+
{
|
|
41
|
+
merchants:
|
|
42
|
+
[
|
|
43
|
+
capital-one-travel-hotels,
|
|
44
|
+
capital-one-travel-vacation-rentals,
|
|
45
|
+
capital-one-travel-car-rentals,
|
|
46
|
+
capital-one-travel-activities,
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
rate: 5
|
|
50
|
+
note: Hotels, vacation rentals, rental cars and activities booked through Capital One Travel only. Flights booked there, and hotel incidentals or upgrades paid outside Capital One Travel, earn the base 1%.
|
|
51
|
+
- id: capital-one-entertainment
|
|
52
|
+
match: { merchants: [capital-one-entertainment] }
|
|
53
|
+
rate: 5
|
|
54
|
+
note: Tickets bought on the Capital One Entertainment ticketing platform only. Cardholder-exclusive pre-sales, tickets bought directly from Capital One Hall or Capital One Arena, and Capital One Dining are excluded.
|
|
55
|
+
# Rotating 5% categories. Activation is per quarter and isn't retroactive:
|
|
56
|
+
# only purchases from the day you activate earn 5%.
|
|
57
|
+
- id: q4-2026-restaurants
|
|
58
|
+
match: { categories: [dining] }
|
|
59
|
+
rate: 5
|
|
60
|
+
geography: us_only
|
|
61
|
+
cap: five-percent-quarterly
|
|
62
|
+
requires: { activation: true }
|
|
63
|
+
valid_from: 2026-10-01
|
|
64
|
+
valid_to: 2026-12-31
|
|
65
|
+
activate_by: 2026-12-31
|
|
66
|
+
note: Full-service restaurants, cafes, cafeterias, fast-food locations and caterers. Restaurants inside hotels or retail stores, and bakeries, may not qualify. Earns 5% only from the day you activate (any time through December 31, 2026).
|
|
67
|
+
- id: q4-2026-entertainment
|
|
68
|
+
match: { categories: [entertainment, movie_theaters] }
|
|
69
|
+
rate: 5
|
|
70
|
+
geography: us_only
|
|
71
|
+
cap: five-percent-quarterly
|
|
72
|
+
requires: { activation: true }
|
|
73
|
+
valid_from: 2026-10-01
|
|
74
|
+
valid_to: 2026-12-31
|
|
75
|
+
activate_by: 2026-12-31
|
|
76
|
+
note: Tickets at movie theaters, professional and semi-professional sports promoters, theatrical and concert promoters, amusement parks, tourist attractions, aquariums, zoos, dance halls, record stores, video rental locations, pool halls and bowling alleys. Golf courses, country clubs, some collegiate sporting events, charitable events, cable and streaming subscriptions, amusement park tickets bought on third-party sites and packages coded as travel or hotel don't count. Earns 5% only from the day you activate.
|
|
77
|
+
- id: q4-2026-utilities
|
|
78
|
+
match: { categories: [utilities, tv_internet, phone] }
|
|
79
|
+
rate: 5
|
|
80
|
+
geography: us_only
|
|
81
|
+
cap: five-percent-quarterly
|
|
82
|
+
requires: { activation: true }
|
|
83
|
+
valid_from: 2026-10-01
|
|
84
|
+
valid_to: 2026-12-31
|
|
85
|
+
activate_by: 2026-12-31
|
|
86
|
+
note: Residential electricity, natural gas and water, plus internet, cable, satellite TV and mobile and landline phone service paid by phone or online. Paying for those at a physical store, through a landlord, property manager or HOA, home fuel delivery and standalone streaming services may not qualify. Earns 5% only from the day you activate.
|
|
87
|
+
todo:
|
|
88
|
+
- 'Discover has published only 2026 on its 5% calendar (Oct-Dec 2026 is the last quarter shown). Add 2027 quarters when Discover posts them.'
|
|
89
|
+
- Discover's 5% activation isn't retroactive (5% applies "from 10/1/26 (or the date on which you activate 5%, whichever is later)"), and you can activate until the quarter's last day. The schema's activate_by can't say that purchases before activation earn 1%; it's set to the quarter's last day and the rule notes say so.
|
|
90
|
+
- Cashback Match (Discover matches all cash back a new cardmember earns in the first year, no limit) can't be expressed as a fixed welcome offer; it roughly doubles first-year earnings on every rule. Not modeled.
|
|
91
|
+
- The card page's Cashback Match terms say "through your first 365 days"; the Cashback Bonus page says "your first 12 consecutive billing periods or 365 days, whichever is longer". Which one current applicants get is unclear; it doesn't change any rate here.
|
|
92
|
+
- Foreign transaction fee isn't stated on any Discover page or in the application terms (their fee table has no foreign transaction row). Left null; confirm with the Cardmember Agreement or Capital One.
|
|
93
|
+
- Discover says 5% purchases "must be made with merchants in the U.S." The application terms put that sentence in the general earning paragraph, so it may also apply to the 1% base and Capital One Travel; only the rotating rules are marked us_only.
|
|
94
|
+
- Amusement parks, tourist attractions, aquariums, zoos, record stores, video rental, pool halls and bowling alleys earn 5% as Q4 2026 entertainment but have no earnrate category. Golf courses, country clubs and collegiate sporting events are excluded but can't be told apart from the entertainment category.
|
|
95
|
+
- Third-party payment accounts and digital wallet transactions may not earn 5% if they don't carry a qualifying MCC. Not modeled.
|
|
96
|
+
# Issuer text backing each rule; `yarn check-sources` checks it weekly.
|
|
97
|
+
evidence:
|
|
98
|
+
- rule: base_rate
|
|
99
|
+
url: https://www.discover.com/credit-cards/cash-back/it-card/
|
|
100
|
+
quote: 'Plus, earn 1% cash back on all other purchases.'
|
|
101
|
+
- rule: base_rate
|
|
102
|
+
url: https://disclosures.capitalone.com/disclosure.42300.en-US.html
|
|
103
|
+
quote: 'There is no cap to the amount of 1% rewards you can earn on purchases.'
|
|
104
|
+
- rule: capital-one-travel
|
|
105
|
+
url: https://disclosures.capitalone.com/disclosure.42300.en-US.html
|
|
106
|
+
quote: 'earn 5% cash back on purchases (hotels, vacation rentals, rental cars and activities only) made on Capital One Travel'
|
|
107
|
+
- rule: capital-one-travel
|
|
108
|
+
url: https://disclosures.capitalone.com/disclosure.42300.en-US.html
|
|
109
|
+
quote: 'such as hotel incidentals, upgrades or other expenses, will not earn 5% cash back'
|
|
110
|
+
- rule: capital-one-entertainment
|
|
111
|
+
url: https://disclosures.capitalone.com/disclosure.42300.en-US.html
|
|
112
|
+
quote: 'You will earn 5% cash back on Capital One Entertainment purchases.'
|
|
113
|
+
- rule: capital-one-entertainment
|
|
114
|
+
url: https://disclosures.capitalone.com/disclosure.42300.en-US.html
|
|
115
|
+
quote: 'and dining reservations or purchases through Capital One Dining are excluded'
|
|
116
|
+
- rule: q4-2026-restaurants
|
|
117
|
+
url: https://www.discover.com/credit-cards/cash-back/cashback-calendar.html
|
|
118
|
+
quote: 'Earn 5% Cashback Bonus at Restaurants, Entertainment and Utilities, now to December 31, 2026'
|
|
119
|
+
- rule: q4-2026-restaurants
|
|
120
|
+
url: https://www.discover.com/credit-cards/cash-back/cashback-calendar.html
|
|
121
|
+
quote: 'from 10/1/26 (or the date on which you activate 5%, whichever is later) through 12/31/26'
|
|
122
|
+
- rule: q4-2026-restaurants
|
|
123
|
+
url: https://www.discover.com/credit-cards/cash-back/cashback-calendar.html
|
|
124
|
+
quote: 'Restaurant purchases include those made at merchants classified as full-service restaurants, cafes, cafeterias, fast-food locations, and caterers.'
|
|
125
|
+
- rule: q4-2026-restaurants
|
|
126
|
+
url: https://www.discover.com/credit-cards/cash-back/cashback-calendar.html
|
|
127
|
+
quote: 'Purchases must be made with merchants in the U.S.'
|
|
128
|
+
- rule: q4-2026-restaurants
|
|
129
|
+
url: https://disclosures.capitalone.com/disclosure.42300.en-US.html
|
|
130
|
+
quote: '5% category purchases over the quarterly maximum, and all other purchases, except as noted below, earn 1% cash back.'
|
|
131
|
+
- rule: q4-2026-restaurants
|
|
132
|
+
url: https://www.discover.com/credit-cards/cash-back/cashback-bonus.html
|
|
133
|
+
quote: 'NOW: Earn 5% Cashback Bonus at Restaurants, Entertainment and Utilities'
|
|
134
|
+
- rule: q4-2026-entertainment
|
|
135
|
+
url: https://www.discover.com/credit-cards/cash-back/cashback-calendar.html
|
|
136
|
+
quote: 'on up to $1,500 in purchases when you activate'
|
|
137
|
+
- rule: q4-2026-entertainment
|
|
138
|
+
url: https://www.discover.com/credit-cards/cash-back/cashback-calendar.html
|
|
139
|
+
quote: 'Entertainment purchases include ticket purchases at movie theaters, sports promoters (professional and semi-professional live events)'
|
|
140
|
+
- rule: q4-2026-entertainment
|
|
141
|
+
url: https://www.discover.com/credit-cards/cash-back/cashback-calendar.html
|
|
142
|
+
quote: 'Tickets for or fees at golf courses, country clubs, some collegiate sporting events, charitable events'
|
|
143
|
+
- rule: q4-2026-utilities
|
|
144
|
+
url: https://www.discover.com/credit-cards/cash-back/cashback-calendar.html
|
|
145
|
+
quote: 'Utilities purchases include purchases for residential electricity, natural gas, and water service.'
|
|
146
|
+
- rule: q4-2026-utilities
|
|
147
|
+
url: https://www.discover.com/credit-cards/cash-back/cashback-calendar.html
|
|
148
|
+
quote: 'purchases made over the phone or online for internet, cable, satellite TV, and mobile and landline phone service'
|
|
149
|
+
- rule: q4-2026-utilities
|
|
150
|
+
url: https://www.discover.com/credit-cards/cash-back/cashback-calendar.html
|
|
151
|
+
quote: 'Purchases at physical retail stores for internet, cable, satellite, and mobile and landline phone service do not qualify.'
|
|
152
|
+
- rule: q4-2026-utilities
|
|
153
|
+
url: https://www.discover.com/credit-cards/card-smarts/using-cashback-bonus/
|
|
154
|
+
quote: 'you need to activate to earn each quarter'
|
|
155
|
+
- rule: annual_fee
|
|
156
|
+
url: https://disclosures.capitalone.com/disclosure.42300.en-US.html
|
|
157
|
+
quote: 'Annual Fee None.'
|
|
158
|
+
- rule: annual_fee
|
|
159
|
+
url: https://www.discover.com/credit-cards/cash-back/it-card/
|
|
160
|
+
quote: 'No, there is no annual fee for the Discover it Card.'
|
|
161
|
+
- rule: welcome_offer
|
|
162
|
+
url: https://www.discover.com/credit-cards/cash-back/it-card/
|
|
163
|
+
quote: 'Earn a $200 bonus statement credit when you spend $500 in the first 3 months.'
|
|
164
|
+
- rule: welcome_offer
|
|
165
|
+
url: https://disclosures.capitalone.com/disclosure.42300.en-US.html
|
|
166
|
+
quote: 'You will receive a $200 statement credit if you spend at least $500 within 3 months'
|
|
167
|
+
- rule: welcome_offer
|
|
168
|
+
url: https://www.discover.com/credit-cards/cash-back/it-card/
|
|
169
|
+
quote: 'Get an unlimited dollar-for-dollar match of all the cash back you earn at the end of your first year'
|
package/data/currencies.yaml
CHANGED
|
@@ -131,6 +131,32 @@
|
|
|
131
131
|
sources:
|
|
132
132
|
- https://thepointsguy.com/loyalty-programs/monthly-valuations/
|
|
133
133
|
|
|
134
|
+
- id: delta-skymiles
|
|
135
|
+
name: Delta SkyMiles
|
|
136
|
+
value_cents: 1.0
|
|
137
|
+
range: [1.0, 1.2]
|
|
138
|
+
note: Delta's Pay with Miles lets Delta Amex cardholders take $50 off a Delta fare for every 5,000 miles (1.0¢ a mile). 1.2¢ is TPG's November 2026 valuation.
|
|
139
|
+
sources:
|
|
140
|
+
- https://www.delta.com/paywithmiles
|
|
141
|
+
- https://thepointsguy.com/loyalty-programs/monthly-valuations/
|
|
142
|
+
|
|
143
|
+
- id: hilton-honors
|
|
144
|
+
name: Hilton Honors points
|
|
145
|
+
value_cents: 0.4
|
|
146
|
+
note: Hilton publishes no general cash value for points; its terms say rewards can't be redeemed for cash, and the only fixed rate is for Event Credits on group bills (25,000 points for $50, 0.2¢). 0.4¢ is TPG's November 2026 valuation, below the 0.5¢ default used for IHG, so it's the default here.
|
|
147
|
+
sources:
|
|
148
|
+
- https://www.hilton.com/en/hilton-honors/terms/
|
|
149
|
+
- https://thepointsguy.com/loyalty-programs/monthly-valuations/
|
|
150
|
+
|
|
151
|
+
- id: marriott-bonvoy
|
|
152
|
+
name: Marriott Bonvoy points
|
|
153
|
+
value_cents: 0.5
|
|
154
|
+
range: [0.5, 0.75]
|
|
155
|
+
note: Marriott publishes no general cash value for points. The only fixed rate is Chase's Pay Yourself Back on the Bonvoy Bold card, $100 for every 12,500 points (0.8¢), limited to airline and Marriott purchases on that card. 0.5¢ is the conservative default IHG uses too; 0.75¢ is TPG's November 2026 valuation.
|
|
156
|
+
sources:
|
|
157
|
+
- https://creditcards.chase.com/travel-credit-cards/marriott-bonvoy/bold
|
|
158
|
+
- https://thepointsguy.com/loyalty-programs/monthly-valuations/
|
|
159
|
+
|
|
134
160
|
- id: kohls-rewards
|
|
135
161
|
name: Kohl's Rewards
|
|
136
162
|
value_cents: 1.0
|
package/data/issuers.yaml
CHANGED
|
@@ -212,6 +212,11 @@
|
|
|
212
212
|
aliases: [capital one travel car rental],
|
|
213
213
|
category: travel,
|
|
214
214
|
}
|
|
215
|
+
- id: capital-one-travel-activities
|
|
216
|
+
kind: booking
|
|
217
|
+
name: Activity through Capital One Travel
|
|
218
|
+
aliases: [capital one travel activity]
|
|
219
|
+
category: travel
|
|
215
220
|
- {
|
|
216
221
|
id: capital-one-travel-flights,
|
|
217
222
|
kind: booking,
|
|
@@ -445,6 +450,17 @@
|
|
|
445
450
|
domains: [postmates.com]
|
|
446
451
|
category: dining
|
|
447
452
|
note: A delivery app that codes as a restaurant for most issuers. Bonuses limited to buying directly from a restaurant don't count it.
|
|
453
|
+
- id: caviar
|
|
454
|
+
name: Caviar
|
|
455
|
+
domains: [trycaviar.com]
|
|
456
|
+
category: dining
|
|
457
|
+
note: A delivery app that codes as a restaurant for most issuers. Bonuses limited to buying directly from a restaurant don't count it.
|
|
458
|
+
- id: delivery-com
|
|
459
|
+
name: Delivery.com
|
|
460
|
+
aliases: [delivery com]
|
|
461
|
+
domains: [delivery.com]
|
|
462
|
+
category: dining
|
|
463
|
+
note: A delivery app that codes as a restaurant for most issuers. Bonuses limited to buying directly from a restaurant don't count it.
|
|
448
464
|
|
|
449
465
|
# A dinner theater: food ordered at your seat may land on the same check.
|
|
450
466
|
- id: alamo-drafthouse
|
package/data/merchants/seed.yaml
CHANGED
|
@@ -1036,6 +1036,18 @@
|
|
|
1036
1036
|
domains: [delta.com],
|
|
1037
1037
|
category: travel.flights,
|
|
1038
1038
|
}
|
|
1039
|
+
- {
|
|
1040
|
+
id: delta-vacations,
|
|
1041
|
+
name: Delta Vacations,
|
|
1042
|
+
domains: [deltavacations.com],
|
|
1043
|
+
category: travel,
|
|
1044
|
+
}
|
|
1045
|
+
- {
|
|
1046
|
+
id: delta-stays,
|
|
1047
|
+
name: Delta Stays,
|
|
1048
|
+
aliases: [delta stays, delta hotels],
|
|
1049
|
+
category: travel.hotels,
|
|
1050
|
+
}
|
|
1039
1051
|
- {
|
|
1040
1052
|
id: united,
|
|
1041
1053
|
name: United Airlines,
|
|
@@ -1332,6 +1344,34 @@
|
|
|
1332
1344
|
domains: [thrifty.com],
|
|
1333
1345
|
category: travel.car_rental,
|
|
1334
1346
|
}
|
|
1347
|
+
- {
|
|
1348
|
+
id: ez-rent-a-car,
|
|
1349
|
+
name: E-Z Rent-A-Car,
|
|
1350
|
+
aliases: [ez rent a car],
|
|
1351
|
+
domains: [e-zrentacar.com],
|
|
1352
|
+
category: travel.car_rental,
|
|
1353
|
+
}
|
|
1354
|
+
- {
|
|
1355
|
+
id: fox-rent-a-car,
|
|
1356
|
+
name: Fox Rent A Car,
|
|
1357
|
+
aliases: [fox car rental],
|
|
1358
|
+
domains: [foxrentacar.com],
|
|
1359
|
+
category: travel.car_rental,
|
|
1360
|
+
}
|
|
1361
|
+
- {
|
|
1362
|
+
id: payless-car-rental,
|
|
1363
|
+
name: Payless Car Rental,
|
|
1364
|
+
aliases: [payless car],
|
|
1365
|
+
domains: [paylesscar.com],
|
|
1366
|
+
category: travel.car_rental,
|
|
1367
|
+
}
|
|
1368
|
+
- {
|
|
1369
|
+
id: sixt,
|
|
1370
|
+
name: Sixt,
|
|
1371
|
+
aliases: [sixt rent a car],
|
|
1372
|
+
domains: [sixt.com],
|
|
1373
|
+
category: travel.car_rental,
|
|
1374
|
+
}
|
|
1335
1375
|
- {
|
|
1336
1376
|
id: dollar-car-rental,
|
|
1337
1377
|
name: Dollar Rent A Car,
|
|
@@ -20,6 +20,9 @@ export const EVERYDAY_PURCHASES = [
|
|
|
20
20
|
{ label: 'Drugstores', category: 'drugstore' },
|
|
21
21
|
{ label: 'Everything else', category: 'other' },
|
|
22
22
|
];
|
|
23
|
+
// What a quick pick searches for: "Streaming (Netflix)" searches "Netflix",
|
|
24
|
+
// since streaming bonuses follow each issuer's list of services.
|
|
25
|
+
export const quickPickText = (item) => /\((.+)\)$/.exec(item.label)?.[1] ?? item.label;
|
|
23
26
|
// The best card in the wallet for each everyday kind of purchase on `date`,
|
|
24
27
|
// with caps, picks and activations as the wallet has them.
|
|
25
28
|
export function cheatSheet(catalog, wallet, date, items = EVERYDAY_PURCHASES) {
|
package/dist/engine/recommend.js
CHANGED
|
@@ -133,7 +133,7 @@ function warningsFor(catalog, entry, purchase) {
|
|
|
133
133
|
const { best, nominal } = evaluation;
|
|
134
134
|
const warnings = [];
|
|
135
135
|
if (entry.fee > 0)
|
|
136
|
-
warnings.push(`Charges a ${pct(entry.fee)} foreign transaction fee here, so it nets ${pct(entry.
|
|
136
|
+
warnings.push(`Charges a ${pct(entry.fee)} foreign transaction fee here, so it nets ${pct(entry.rateReturnPct)}.`);
|
|
137
137
|
if (isForeign(purchase) && card.foreign_transaction_fee === null)
|
|
138
138
|
warnings.push("Its foreign transaction fee isn't on file, so none is subtracted. Check the issuer's terms before using it abroad.");
|
|
139
139
|
const capLabel = (e) => e.cap
|
|
@@ -220,7 +220,7 @@ function tieNote(entries) {
|
|
|
220
220
|
tied.some((e) => e.evaluation.best.cap !== null)
|
|
221
221
|
? 'because its rate has no cap'
|
|
222
222
|
: 'by a fixed order; either works';
|
|
223
|
-
return `${names} also ${verb} ${pct(winner.
|
|
223
|
+
return `${names} also ${verb} ${pct(winner.rateReturnPct)} here. ${displayName(winner)} wins the tie ${why}.`;
|
|
224
224
|
}
|
|
225
225
|
// Finds the closest point inside a currency's value range where the winner
|
|
226
226
|
// stops winning, varying one currency at a time. Returns are linear in the
|
|
@@ -338,9 +338,9 @@ export function recommend(catalog, wallet, purchase, merchant = resolveMerchant(
|
|
|
338
338
|
const twin = alts.find((e) => e.card.id !== winner.card.id &&
|
|
339
339
|
alt.effectiveReturnPct - e.effectiveReturnPct <= EPSILON);
|
|
340
340
|
if (alt.card.id !== winner.card.id)
|
|
341
|
-
warnings.push(`${as}, ${displayName(alt)} wins (${pct(alt.
|
|
341
|
+
warnings.push(`${as}, ${displayName(alt)} wins (${pct(alt.rateReturnPct)}).`);
|
|
342
342
|
else if (twin)
|
|
343
|
-
warnings.push(`${as}, ${displayName(twin)} earns as much (${pct(twin.
|
|
343
|
+
warnings.push(`${as}, ${displayName(twin)} earns as much (${pct(twin.rateReturnPct)}).`);
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
if (!winner) {
|
|
@@ -359,7 +359,7 @@ export function recommend(catalog, wallet, purchase, merchant = resolveMerchant(
|
|
|
359
359
|
? rankEntries(catalog, wallet, { ...purchase, channel: 'online' }, online, online.category).entries[0]
|
|
360
360
|
: undefined;
|
|
361
361
|
if (alt)
|
|
362
|
-
warnings.push(`Ordering online instead? ${displayName(alt)} works there (${pct(alt.
|
|
362
|
+
warnings.push(`Ordering online instead? ${displayName(alt)} works there (${pct(alt.rateReturnPct)}).`);
|
|
363
363
|
}
|
|
364
364
|
else if (merchant.accepts === null &&
|
|
365
365
|
(winner.card.network === 'amex' || winner.card.network === 'discover')) {
|
|
@@ -384,7 +384,7 @@ export function recommend(catalog, wallet, purchase, merchant = resolveMerchant(
|
|
|
384
384
|
const alt = rankEntries(catalog, wallet, purchase, via, via.category)
|
|
385
385
|
.entries[0];
|
|
386
386
|
if (alt && alt.effectiveReturnPct > winner.effectiveReturnPct + EPSILON) {
|
|
387
|
-
notes.push(`If you book it as ${via.resolvedName}, ${displayName(alt)} earns ${pct(alt.
|
|
387
|
+
notes.push(`If you book it as ${via.resolvedName}, ${displayName(alt)} earns ${pct(alt.rateReturnPct)}.`);
|
|
388
388
|
}
|
|
389
389
|
}
|
|
390
390
|
const open = bonusPick(catalog, wallet, entries, purchase.date);
|
|
@@ -400,7 +400,7 @@ export function recommend(catalog, wallet, purchase, merchant = resolveMerchant(
|
|
|
400
400
|
const bonus = open && (open.behind || ties) ? open : null;
|
|
401
401
|
const best = winner?.effectiveReturnPct ?? 0;
|
|
402
402
|
if (open && !bonus && isClose(open.entry.effectiveReturnPct, best)) {
|
|
403
|
-
notes.push(`${displayName(open.entry)} earns ${pct(open.entry.
|
|
403
|
+
notes.push(`${displayName(open.entry)} earns ${pct(open.entry.rateReturnPct)} here, ${num(gap)} ${gap === 1 ? 'point' : 'points'} less than ${pct(best)}, and would help its welcome bonus (${money(open.status.remaining ?? 0)} more by ${open.status.deadline ?? ''}). You're on pace, so this uses the higher rate.`);
|
|
404
404
|
}
|
|
405
405
|
const chosen = bonus?.entry ?? winner;
|
|
406
406
|
// A tie among the top cards doesn't matter when a welcome bonus decides.
|
|
@@ -417,7 +417,9 @@ export function recommend(catalog, wallet, purchase, merchant = resolveMerchant(
|
|
|
417
417
|
const daysLeft = status.daysLeft ?? 0;
|
|
418
418
|
const costHere = ties || top.earned === null || first.earned === null
|
|
419
419
|
? null
|
|
420
|
-
: roundCents(first.earned
|
|
420
|
+
: roundCents(first.earned +
|
|
421
|
+
(first.milestoneBonus?.dollars ?? 0) -
|
|
422
|
+
(top.earned + (top.milestoneBonus?.dollars ?? 0)));
|
|
421
423
|
bonusResult = {
|
|
422
424
|
cardId: top.cardId,
|
|
423
425
|
checkoutLabel: top.checkoutLabel,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Ready-made questions a client can offer: the user's question, then what
|
|
3
|
+
// to call and how to answer. Rates and card facts come only from the tools.
|
|
4
|
+
export const PROMPTS = ['which-card', 'cheat-sheet', 'this-quarter', 'set-up'];
|
|
5
|
+
const FROM_SAVED_OR_ALL = "the user's saved cards if there are any, otherwise every card earnrate knows";
|
|
6
|
+
const ask = (question, guidance) => ({
|
|
7
|
+
messages: [
|
|
8
|
+
{
|
|
9
|
+
role: 'user',
|
|
10
|
+
content: {
|
|
11
|
+
type: 'text',
|
|
12
|
+
text: `${question}\n\n${guidance.join(' ')}`,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
const dollars = (amount) => (/^\d/.test(amount) ? `$${amount}` : amount);
|
|
18
|
+
export function registerPrompts(server, deps, ctx) {
|
|
19
|
+
server.registerPrompt('which-card', {
|
|
20
|
+
title: 'Which credit card should I use?',
|
|
21
|
+
description: 'The card that earns the most at a store or for a kind of purchase, with the reason. From your saved cards, or every card if you have none saved.',
|
|
22
|
+
argsSchema: z.object({
|
|
23
|
+
store: z
|
|
24
|
+
.string()
|
|
25
|
+
.trim()
|
|
26
|
+
.min(1)
|
|
27
|
+
.describe('The store, or the kind of purchase: Costco, groceries'),
|
|
28
|
+
amount: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('How much you are spending, in US dollars'),
|
|
32
|
+
country: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Where you are paying, if not the U.S.: FR, Japan'),
|
|
36
|
+
}),
|
|
37
|
+
}, (args) => {
|
|
38
|
+
const amount = args.amount?.trim() || null;
|
|
39
|
+
const country = args.country?.trim() || null;
|
|
40
|
+
const call = [
|
|
41
|
+
`merchant ${JSON.stringify(args.store)}`,
|
|
42
|
+
...(amount === null
|
|
43
|
+
? []
|
|
44
|
+
: [`amount ${JSON.stringify(amount)} as a number of US dollars`]),
|
|
45
|
+
...(country === null
|
|
46
|
+
? []
|
|
47
|
+
: [`country as the two-letter code for ${JSON.stringify(country)}`]),
|
|
48
|
+
];
|
|
49
|
+
return ask(`Which credit card should I use at ${args.store}${amount === null ? '' : ` for ${dollars(amount)}`}${country === null ? '' : ` in ${country}`}?`, [
|
|
50
|
+
`Call earnrate's best_card_for_purchase with ${call.join(', ')}.`,
|
|
51
|
+
`It answers from ${FROM_SAVED_OR_ALL}, so don't ask for their cards first.`,
|
|
52
|
+
'Say which card to use and why, with the assumptions and warnings it gives.',
|
|
53
|
+
...(country === null
|
|
54
|
+
? [
|
|
55
|
+
'If the user mentions paying abroad or in another currency, ask where, or pass `country` as its two-letter code.',
|
|
56
|
+
]
|
|
57
|
+
: []),
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
server.registerPrompt('cheat-sheet', {
|
|
61
|
+
title: 'Which card for everyday spending?',
|
|
62
|
+
description: 'The card to use for each everyday kind of purchase right now, from your saved cards or every card.',
|
|
63
|
+
}, () => ask('Which card should I use for everyday spending?', [
|
|
64
|
+
"Call earnrate's cheat_sheet and show the card for each kind of purchase, with its notes.",
|
|
65
|
+
`It uses ${FROM_SAVED_OR_ALL}.`,
|
|
66
|
+
]));
|
|
67
|
+
server.registerPrompt('this-quarter', {
|
|
68
|
+
title: 'What should I do with my cards this quarter?',
|
|
69
|
+
description: 'Bonuses to activate, categories to pick, caps running low and welcome bonuses to finish, from your saved cards.',
|
|
70
|
+
}, () => ask('What should I do with my cards this quarter?', [
|
|
71
|
+
"Call earnrate's list_wallet: it starts with a to-do list. Walk through it, soonest first.",
|
|
72
|
+
ctx.walletUse,
|
|
73
|
+
'If no cards are saved, say in a sentence or two what saving them adds (reminders to activate bonuses, pick categories, and watch caps and welcome bonuses), without pressing the user to do it, and offer the cheat sheet from every card instead.',
|
|
74
|
+
]));
|
|
75
|
+
server.registerPrompt('set-up', {
|
|
76
|
+
title: 'Set up my cards',
|
|
77
|
+
description: 'Save the cards you hold, by name, with their category picks, memberships and activations.',
|
|
78
|
+
}, () => ask('Help me set up my cards in earnrate.', [
|
|
79
|
+
'Ask which cards the user has; their names are enough. Never ask for card numbers, expiry dates or security codes.',
|
|
80
|
+
...(deps.savedWallets === 'account'
|
|
81
|
+
? [
|
|
82
|
+
"Never ask for the recovery code either; it's typed only on earnrate's page.",
|
|
83
|
+
]
|
|
84
|
+
: []),
|
|
85
|
+
'Find each card with search_cards and check the match with the user, then save it with add_card.',
|
|
86
|
+
ctx.walletUse,
|
|
87
|
+
'When a card needs category picks, a membership like Amazon Prime, activations or its anniversary, ask rather than guessing, and save the answers with update_card or set_memberships.',
|
|
88
|
+
'When done, call list_wallet and show its to-do list.',
|
|
89
|
+
]));
|
|
90
|
+
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/server';
|
|
|
2
2
|
import { MCP_SERVER_NAME } from "../brand.js";
|
|
3
3
|
import { makeContext } from "./context.js";
|
|
4
4
|
import { makeInputs } from "./inputs.js";
|
|
5
|
+
import { registerPrompts } from "./prompts.js";
|
|
5
6
|
import { registerAccountTools } from "./tools/account.js";
|
|
6
7
|
import { registerAnswerTools } from "./tools/answers.js";
|
|
7
8
|
import { registerWalletTools } from "./tools/wallet.js";
|
|
@@ -66,6 +67,7 @@ export function createEarnrateServer(deps) {
|
|
|
66
67
|
const inputs = makeInputs(deps.catalog);
|
|
67
68
|
registerAnswerTools(server, deps, ctx, inputs);
|
|
68
69
|
registerWalletTools(server, deps, ctx, inputs);
|
|
70
|
+
registerPrompts(server, deps, ctx);
|
|
69
71
|
if (deps.savedWallets === 'account')
|
|
70
72
|
registerAccountTools(server, deps.catalog, deps.store, ctx);
|
|
71
73
|
return server;
|
|
@@ -26,6 +26,7 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
26
26
|
annotations: {
|
|
27
27
|
title: 'Export the wallet',
|
|
28
28
|
readOnlyHint: true,
|
|
29
|
+
destructiveHint: false,
|
|
29
30
|
openWorldHint: false,
|
|
30
31
|
},
|
|
31
32
|
}, async () => {
|
|
@@ -82,7 +83,7 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
82
83
|
});
|
|
83
84
|
server.registerTool('delete_wallet', {
|
|
84
85
|
title: 'Delete the wallet',
|
|
85
|
-
description: "Deletes the saved wallet and its recovery code now and for good, and signs out every
|
|
86
|
+
description: "Deletes the saved wallet and its recovery code now and for good, and signs out every app connected to it, this one included. earnrate.dev and the earnrate command line keep their own wallets and aren't affected. Only when the user asks to delete it: show it with list_wallet, offer export_wallet, and pass confirm, cards and saved only after the user confirms. It refuses if the wallet no longer holds what list_wallet showed, so a retried call can't delete a different wallet.",
|
|
86
87
|
inputSchema: z.object({
|
|
87
88
|
confirm: z
|
|
88
89
|
.literal(true)
|
|
@@ -121,7 +122,7 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
121
122
|
content: [
|
|
122
123
|
{
|
|
123
124
|
type: 'text',
|
|
124
|
-
text: "Deleted the saved wallet and its recovery code, and signed out every
|
|
125
|
+
text: "Deleted the saved wallet and its recovery code, and signed out every app connected to it, this one included. To keep using earnrate here, the user connects again on earnrate's page and starts a new wallet.",
|
|
125
126
|
},
|
|
126
127
|
],
|
|
127
128
|
structuredContent: { deleted: true },
|
|
@@ -129,7 +130,7 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
129
130
|
});
|
|
130
131
|
server.registerTool('switch_wallet', {
|
|
131
132
|
title: 'Switch wallets',
|
|
132
|
-
description: "Disconnects this app from its saved wallet so the user can connect a different one with its recovery code, or start a new one, on earnrate's page. Nothing is deleted. It also signs out anyone else connected to this wallet through the same app (another
|
|
133
|
+
description: "Disconnects this app from its saved wallet so the user can connect a different one with its recovery code, or start a new one, on earnrate's page. Nothing is deleted. It also signs out anyone else connected to this wallet through the same app (another account the recovery code was shared with): say so before calling it. Use it when the user wants another wallet. Never ask for the recovery code in the conversation.",
|
|
133
134
|
inputSchema: z.object({}),
|
|
134
135
|
outputSchema: z.object({ disconnected: z.boolean() }),
|
|
135
136
|
annotations: {
|
|
@@ -145,7 +146,7 @@ export function registerAccountTools(server, catalog, store, ctx) {
|
|
|
145
146
|
content: [
|
|
146
147
|
{
|
|
147
148
|
type: 'text',
|
|
148
|
-
text: "Disconnected from this saved wallet; nothing was deleted. earnrate's sign-in page opens on the next earnrate request (or reconnect earnrate in the app's connector settings). There the user chooses \"Already have a wallet?\" and types the other wallet's recovery code, or starts a new wallet. Other
|
|
149
|
+
text: "Disconnected from this saved wallet; nothing was deleted. earnrate's sign-in page opens on the next earnrate request (or reconnect earnrate in the app's connector settings). There the user chooses \"Already have a wallet?\" and types the other wallet's recovery code, or starts a new wallet. Other accounts connected to this wallet through the same app are signed out too.",
|
|
149
150
|
},
|
|
150
151
|
],
|
|
151
152
|
structuredContent: { disconnected: true },
|
|
@@ -54,6 +54,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
54
54
|
annotations: {
|
|
55
55
|
title: 'Best card for a purchase',
|
|
56
56
|
readOnlyHint: true,
|
|
57
|
+
destructiveHint: false,
|
|
57
58
|
openWorldHint: deps.llm !== null,
|
|
58
59
|
},
|
|
59
60
|
}, async ({ merchant, amount, channel, date, country, category, cards, memberships, valuation, }) => {
|
|
@@ -127,6 +128,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
127
128
|
annotations: {
|
|
128
129
|
title: 'Resolve a merchant',
|
|
129
130
|
readOnlyHint: true,
|
|
131
|
+
destructiveHint: false,
|
|
130
132
|
openWorldHint: deps.llm !== null,
|
|
131
133
|
},
|
|
132
134
|
}, async ({ merchant, channel, category }) => {
|
|
@@ -166,6 +168,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
166
168
|
annotations: {
|
|
167
169
|
title: 'Compare welcome offers',
|
|
168
170
|
readOnlyHint: true,
|
|
171
|
+
destructiveHint: false,
|
|
169
172
|
openWorldHint: false,
|
|
170
173
|
},
|
|
171
174
|
}, async (args) => {
|
|
@@ -219,6 +222,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
219
222
|
annotations: {
|
|
220
223
|
title: 'Best cards for a purchase',
|
|
221
224
|
readOnlyHint: true,
|
|
225
|
+
destructiveHint: false,
|
|
222
226
|
openWorldHint: false,
|
|
223
227
|
},
|
|
224
228
|
}, async (args) => {
|
|
@@ -266,6 +270,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
266
270
|
annotations: {
|
|
267
271
|
title: 'Simulate a year of spending',
|
|
268
272
|
readOnlyHint: true,
|
|
273
|
+
destructiveHint: false,
|
|
269
274
|
openWorldHint: false,
|
|
270
275
|
},
|
|
271
276
|
}, async (args) => {
|
|
@@ -349,6 +354,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
349
354
|
annotations: {
|
|
350
355
|
title: 'Cheat sheet',
|
|
351
356
|
readOnlyHint: true,
|
|
357
|
+
destructiveHint: false,
|
|
352
358
|
openWorldHint: false,
|
|
353
359
|
},
|
|
354
360
|
}, async (args) => {
|
|
@@ -391,6 +397,7 @@ export function registerAnswerTools(server, deps, ctx, inputs) {
|
|
|
391
397
|
annotations: {
|
|
392
398
|
title: 'Search cards',
|
|
393
399
|
readOnlyHint: true,
|
|
400
|
+
destructiveHint: false,
|
|
394
401
|
openWorldHint: false,
|
|
395
402
|
},
|
|
396
403
|
}, async ({ query }) => {
|
package/dist/mcp/tools/wallet.js
CHANGED