earnrate 0.2.12 → 0.2.13
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 +20 -7
- package/data/CHANGELOG.md +181 -0
- package/data/VERSION +1 -1
- package/data/cards/amex-blue-cash-everyday.yaml +28 -2
- package/data/cards/amex-blue-cash-preferred.yaml +19 -0
- package/data/cards/amex-business-gold.yaml +38 -3
- package/data/cards/amex-business-platinum.yaml +72 -3
- package/data/cards/amex-delta-skymiles-gold.yaml +27 -1
- package/data/cards/amex-delta-skymiles-platinum.yaml +33 -1
- package/data/cards/amex-delta-skymiles-reserve.yaml +33 -1
- package/data/cards/amex-gold.yaml +52 -0
- package/data/cards/amex-hilton-honors-aspire.yaml +23 -1
- package/data/cards/amex-hilton-honors-business.yaml +14 -1
- package/data/cards/amex-hilton-honors-surpass.yaml +14 -1
- package/data/cards/amex-marriott-bonvoy-brilliant.yaml +15 -1
- package/data/cards/amex-platinum.yaml +110 -0
- package/data/cards/apple-card.yaml +76 -0
- package/data/cards/bilt-obsidian.yaml +12 -2
- package/data/cards/bilt-palladium.yaml +12 -2
- package/data/cards/caltech-efcu-platinum-mastercard.yaml +50 -0
- package/data/cards/capital-one-rei-co-op-mastercard.yaml +62 -0
- package/data/cards/capital-one-venture-x-business.yaml +28 -1
- package/data/cards/capital-one-venture-x.yaml +30 -2
- package/data/cards/chase-aeroplan.yaml +14 -0
- package/data/cards/chase-ihg-one-rewards-business.yaml +27 -1
- package/data/cards/chase-ihg-one-rewards-premier-select.yaml +16 -0
- package/data/cards/chase-ihg-one-rewards-premier.yaml +15 -0
- package/data/cards/chase-ink-business-cash.yaml +13 -0
- package/data/cards/chase-ink-business-unlimited.yaml +13 -2
- package/data/cards/chase-sapphire-preferred.yaml +11 -0
- package/data/cards/chase-sapphire-reserve.yaml +44 -4
- package/data/cards/chase-united-business.yaml +32 -0
- package/data/cards/chase-united-club-business.yaml +40 -0
- package/data/cards/chase-united-club.yaml +40 -0
- package/data/cards/chase-united-explorer.yaml +32 -0
- package/data/cards/chase-united-quest.yaml +40 -0
- package/data/cards/citi-aadvantage-executive.yaml +26 -1
- package/data/cards/citi-strata-elite.yaml +20 -1
- package/data/cards/fidelity-rewards-visa-signature.yaml +60 -0
- package/data/cards/navy-federal-cash-rewards-plus.yaml +61 -0
- package/data/cards/navy-federal-cash-rewards.yaml +60 -0
- package/data/cards/navy-federal-more-rewards-amex.yaml +92 -0
- package/data/cards/paypal-cashback-mastercard.yaml +13 -2
- package/data/cards/penfed-power-cash-rewards.yaml +47 -0
- package/data/cards/usbank-altitude-connect.yaml +8 -2
- package/data/cards/usbank-altitude-go.yaml +8 -2
- package/data/cards/usbank-smartly.yaml +107 -0
- package/data/cards/venmo-credit-card.yaml +151 -0
- package/data/cards/verizon-visa-card.yaml +122 -0
- package/data/currencies.yaml +14 -0
- package/data/issuers.yaml +22 -0
- package/data/memberships.yaml +35 -0
- package/data/merchants/overrides.yaml +448 -0
- package/data/merchants/seed.yaml +1082 -65
- package/dist/cli/args.js +18 -0
- package/dist/cli/explore.js +10 -1
- package/dist/cli/run.js +4 -1
- package/dist/cli/wallet.js +30 -3
- package/dist/data/file-store.js +5 -2
- package/dist/data/integrity.js +22 -0
- package/dist/data/store.js +4 -1
- package/dist/data/wallet-edit.js +25 -6
- package/dist/engine/cheat-sheet.js +17 -5
- package/dist/engine/checklist.js +25 -4
- package/dist/engine/credits.js +65 -0
- package/dist/engine/evaluate.js +38 -11
- package/dist/engine/explore.js +70 -7
- package/dist/engine/offers.js +2 -1
- package/dist/engine/periods.js +10 -3
- package/dist/engine/recommend.js +72 -20
- package/dist/engine/record.js +41 -1
- package/dist/engine/rotation.js +3 -0
- package/dist/engine/rule-text.js +14 -3
- package/dist/engine/setup.js +15 -4
- package/dist/engine/simulate.js +10 -1
- package/dist/engine/text.js +84 -0
- package/dist/engine/wallet-change.js +37 -1
- package/dist/engine/wallet-summary.js +32 -4
- package/dist/format/card-text.js +4 -3
- package/dist/format/cheat-sheet.js +2 -0
- package/dist/format/explore.js +20 -4
- package/dist/format/recommendation.js +6 -2
- package/dist/format/wallet.js +6 -2
- package/dist/import/analyze.js +7 -2
- package/dist/mcp/inputs.js +10 -0
- package/dist/mcp/prompts.js +9 -1
- package/dist/mcp/schemas.js +70 -1
- package/dist/mcp/tools/answers.js +9 -4
- package/dist/mcp/tools/wallet.js +15 -7
- package/dist/merchants/resolve.js +8 -0
- package/dist/schema/card.js +37 -3
- package/dist/schema/common.js +1 -1
- package/dist/schema/merchant.js +3 -0
- package/dist/schema/wallet.js +8 -0
- package/package.json +1 -1
package/dist/mcp/tools/wallet.js
CHANGED
|
@@ -51,6 +51,12 @@ function changes(catalog, args) {
|
|
|
51
51
|
out.push(`${money(spent)} already spent on ${key} this period`);
|
|
52
52
|
for (const [id, spent] of Object.entries(args.milestoneSpent ?? {}))
|
|
53
53
|
out.push(`${money(spent)} spent toward ${id} this ${card?.milestones.find((m) => m.id === id)?.period === 'cardmember_year' ? 'cardmember year' : 'year'}`);
|
|
54
|
+
if (args.enrolled !== undefined)
|
|
55
|
+
out.push(args.enrolled.length === 0
|
|
56
|
+
? 'no credits enrolled'
|
|
57
|
+
: `enrolled in: ${args.enrolled.join(', ')}`);
|
|
58
|
+
for (const [id, used] of Object.entries(args.creditUsed ?? {}))
|
|
59
|
+
out.push(`${money(used)} of the ${id} credit used this period`);
|
|
54
60
|
if (args.bonus === null)
|
|
55
61
|
out.push('welcome bonus no longer tracked');
|
|
56
62
|
else if (args.bonus !== undefined)
|
|
@@ -62,7 +68,7 @@ export function registerWalletTools(server, deps, ctx, inputs) {
|
|
|
62
68
|
const { catalog, store } = deps;
|
|
63
69
|
server.registerTool('list_wallet', {
|
|
64
70
|
title: 'List wallet',
|
|
65
|
-
description: `Starts with a to-do list for the user's cards right now (bonuses to activate and their deadlines, categories to pick or confirm, caps nearly used up, welcome bonuses to finish and the weekly pace, yearly spend bonuses at least 75% of the way); use it when the user asks what to do with their cards this quarter or what they're forgetting. Then lists the user's cards: checkout label, network, rewards currency and its value, spending caps with how much is left this period, chosen bonus categories (and whether they're current), welcome-bonus progress, progress toward yearly spend bonuses, which stores a store card works at, when each card's data was last verified, the point values the user saved, and (for the hosted wallet) when it was started. ${ctx.walletUse}`,
|
|
71
|
+
description: `Starts with a to-do list for the user's cards right now (bonuses to activate and their deadlines, categories to pick or confirm, caps nearly used up, welcome bonuses to finish and the weekly pace, yearly spend bonuses at least 75% of the way, merchant credits to enroll in or use before they reset); use it when the user asks what to do with their cards this quarter or what they're forgetting. Then lists the user's cards: checkout label, network, rewards currency and its value, spending caps with how much is left this period, chosen bonus categories (and whether they're current), welcome-bonus progress, progress toward yearly spend bonuses, merchant statement credits (enrolled or not, and what's left this period), which stores a store card works at, when each card's data was last verified, the point values the user saved, and (for the hosted wallet) when it was started. ${ctx.walletUse}`,
|
|
66
72
|
outputSchema: WalletSummarySchema,
|
|
67
73
|
annotations: {
|
|
68
74
|
title: 'List wallet',
|
|
@@ -96,7 +102,7 @@ export function registerWalletTools(server, deps, ctx, inputs) {
|
|
|
96
102
|
});
|
|
97
103
|
server.registerTool('record_purchase', {
|
|
98
104
|
title: 'Record a purchase',
|
|
99
|
-
description: `Records a purchase the user has already made, so spending caps, welcome-bonus progress
|
|
105
|
+
description: `Records a purchase the user has already made, so spending caps, welcome-bonus progress, yearly spend milestones and merchant credits used stay accurate for future recommendations. It updates the user's saved wallet. Only call it after the purchase has happened, and only once per purchase: calling it twice counts the spend twice. Purchases with no cap, no open welcome bonus, no spend milestone and no enrolled merchant credit are acknowledged and nothing is written. For a merchant earnrate doesn't recognize it asks for category first, so caps aren't missed. ${ctx.walletUse}`,
|
|
100
106
|
inputSchema: z.object({
|
|
101
107
|
cardId: z
|
|
102
108
|
.string()
|
|
@@ -135,21 +141,23 @@ export function registerWalletTools(server, deps, ctx, inputs) {
|
|
|
135
141
|
const result = await store.record(purchase, resolved.merchant);
|
|
136
142
|
if (!result.ok)
|
|
137
143
|
return fail(`Couldn't record it: ${result.error}`);
|
|
138
|
-
const { cap: booking, bonus, milestones } = result;
|
|
139
|
-
const output = booking || bonus || milestones.length > 0
|
|
144
|
+
const { cap: booking, bonus, milestones, credit } = result;
|
|
145
|
+
const output = booking || bonus || milestones.length > 0 || credit
|
|
140
146
|
? {
|
|
141
147
|
recorded: true,
|
|
142
148
|
booking,
|
|
143
149
|
bonus,
|
|
144
150
|
milestones,
|
|
145
|
-
|
|
151
|
+
credit,
|
|
152
|
+
message: `${formatBooking(amount, cardId, booking, bonus, milestones, credit)} Saved in ${ctx.walletName}.`,
|
|
146
153
|
}
|
|
147
154
|
: {
|
|
148
155
|
recorded: false,
|
|
149
156
|
booking: null,
|
|
150
157
|
bonus: null,
|
|
151
158
|
milestones: [],
|
|
152
|
-
|
|
159
|
+
credit: null,
|
|
160
|
+
message: `Nothing to track: ${cardId} has no spending cap, open welcome bonus, spend milestone or enrolled merchant credit for this purchase, so the wallet wasn't changed.`,
|
|
153
161
|
};
|
|
154
162
|
return {
|
|
155
163
|
content: [
|
|
@@ -183,7 +191,7 @@ export function registerWalletTools(server, deps, ctx, inputs) {
|
|
|
183
191
|
});
|
|
184
192
|
server.registerTool('update_card', {
|
|
185
193
|
title: 'Update a card',
|
|
186
|
-
description: `Changes settings on a card already in the wallet: its category picks for this period, checkout label, anniversary, opening date (optional; for first-year rates), activated categories, cap spending so far (capSpent), spending this year toward its spend milestones (milestoneSpent) or a welcome bonus to track. Picks replace the saved ones in each choice group you set (other groups are kept) for the current period. Use it when the user picks new categories, such as each quarter on U.S. Bank Cash+. ${ctx.walletUse}`,
|
|
194
|
+
description: `Changes settings on a card already in the wallet: its category picks for this period, checkout label, anniversary, opening date (optional; for first-year rates), activated categories, cap spending so far (capSpent), spending this year toward its spend milestones (milestoneSpent), merchant credits enrolled in (enrolled) and used this period (creditUsed), or a welcome bonus to track. Picks replace the saved ones in each choice group you set (other groups are kept) for the current period. Use it when the user picks new categories, such as each quarter on U.S. Bank Cash+. ${ctx.walletUse}`,
|
|
187
195
|
inputSchema: CardChange,
|
|
188
196
|
outputSchema: WalletChangeSchema,
|
|
189
197
|
annotations: {
|
|
@@ -121,6 +121,14 @@ export function findMerchant(catalog, id) {
|
|
|
121
121
|
return (catalog.merchants.overrides.find((m) => m.id === id) ??
|
|
122
122
|
catalog.merchants.seed.find((m) => m.id === id));
|
|
123
123
|
}
|
|
124
|
+
// The ids a card rule can name to match this merchant: its own and, for a
|
|
125
|
+
// brand, its parent's.
|
|
126
|
+
export function merchantFamily(catalog, id) {
|
|
127
|
+
if (id === null)
|
|
128
|
+
return [];
|
|
129
|
+
const parent = findMerchant(catalog, id)?.brand_of;
|
|
130
|
+
return parent === undefined ? [id] : [id, parent];
|
|
131
|
+
}
|
|
124
132
|
export function resolveMerchantById(catalog, id, channel) {
|
|
125
133
|
const override = catalog.merchants.overrides.find((m) => m.id === id);
|
|
126
134
|
const found = findMerchant(catalog, id);
|
package/dist/schema/card.js
CHANGED
|
@@ -33,6 +33,16 @@ export const SharedCapSchema = CapSchema.extend({
|
|
|
33
33
|
// aren't in the data yet, not that there's no bonus.
|
|
34
34
|
rotating: z.literal(true).optional(),
|
|
35
35
|
});
|
|
36
|
+
// How a purchase is paid, for rates that depend on it: Apple Card's 2% with
|
|
37
|
+
// Apple Pay, PayPal Cashback's 3% through PayPal checkout. `card` is the card
|
|
38
|
+
// itself or its number, not a phone wallet or PayPal. Unknown unless the
|
|
39
|
+
// person says, so a rule that requires one never applies by assumption.
|
|
40
|
+
export const PaymentMethod = z.enum([
|
|
41
|
+
'card',
|
|
42
|
+
'apple-pay',
|
|
43
|
+
'google-pay',
|
|
44
|
+
'paypal',
|
|
45
|
+
]);
|
|
36
46
|
const hasAny = (m) => Object.values(m).some((v) => v === true || (v !== undefined && v.length > 0));
|
|
37
47
|
// Kinds combine with AND, values within a kind with OR: `{ merchants:
|
|
38
48
|
// [lyft], categories: [transit] }` is a Lyft purchase that codes as transit.
|
|
@@ -52,8 +62,9 @@ export const ExcludeSchema = z
|
|
|
52
62
|
categories: z.array(CategoryId).optional(),
|
|
53
63
|
merchants: z.array(KebabId).optional(),
|
|
54
64
|
mccs: z.array(Mcc).optional(),
|
|
65
|
+
payment: z.array(PaymentMethod).optional(),
|
|
55
66
|
})
|
|
56
|
-
.refine(hasAny, 'exclude needs categories, merchants or
|
|
67
|
+
.refine(hasAny, 'exclude needs categories, merchants, mccs or payment');
|
|
57
68
|
export const RuleSchema = z
|
|
58
69
|
.strictObject({
|
|
59
70
|
id: KebabId,
|
|
@@ -71,6 +82,7 @@ export const RuleSchema = z
|
|
|
71
82
|
.strictObject({
|
|
72
83
|
membership: KebabId.optional(),
|
|
73
84
|
activation: z.boolean().optional(),
|
|
85
|
+
payment: PaymentMethod.optional(),
|
|
74
86
|
})
|
|
75
87
|
.optional(),
|
|
76
88
|
valid_from: IsoDate.optional(),
|
|
@@ -160,6 +172,26 @@ export const IntroRateSchema = z
|
|
|
160
172
|
note: z.string().optional(),
|
|
161
173
|
})
|
|
162
174
|
.refine((i) => (i.multiply === undefined) !== (i.add === undefined), 'an intro rate needs multiply or add, not both');
|
|
175
|
+
// Merchant statement credits: dollars back for spending at named merchants
|
|
176
|
+
// (or a category) up to `amount` each period, like Amex Platinum's $75 a
|
|
177
|
+
// quarter at lululemon. They're separate from earn rates: the purchase still
|
|
178
|
+
// earns the card's rate on its whole amount, and the credit comes on top.
|
|
179
|
+
// `half_year` runs January to June and July to December.
|
|
180
|
+
export const CreditPeriod = z.enum([...CapPeriod.options, 'half_year']);
|
|
181
|
+
export const CreditSchema = z.strictObject({
|
|
182
|
+
id: KebabId,
|
|
183
|
+
match: z
|
|
184
|
+
.strictObject({
|
|
185
|
+
merchants: z.array(KebabId).min(1).optional(),
|
|
186
|
+
categories: z.array(CategoryId).min(1).optional(),
|
|
187
|
+
})
|
|
188
|
+
.refine((m) => (m.merchants === undefined) !== (m.categories === undefined), 'a credit matches merchants or categories, not both'),
|
|
189
|
+
amount: z.number().positive(), // dollars of credit per period
|
|
190
|
+
period: CreditPeriod,
|
|
191
|
+
// The cardholder must enroll with the issuer before it pays.
|
|
192
|
+
enroll: z.literal(true).optional(),
|
|
193
|
+
note: z.string().optional(),
|
|
194
|
+
});
|
|
163
195
|
const HexColor = z.string().regex(/^#[0-9a-f]{6}$/);
|
|
164
196
|
export const CardSchema = z
|
|
165
197
|
.strictObject({
|
|
@@ -206,8 +238,10 @@ export const CardSchema = z
|
|
|
206
238
|
rules: z.array(RuleSchema).default([]),
|
|
207
239
|
milestones: z.array(MilestoneSchema).default([]),
|
|
208
240
|
intro_rates: z.array(IntroRateSchema).default([]),
|
|
209
|
-
|
|
210
|
-
//
|
|
241
|
+
credits: z.array(CreditSchema).default([]),
|
|
242
|
+
// A short quote from an issuer page for each rule (or `base_rate`, or a
|
|
243
|
+
// credit's id). The weekly source check fails a rule whose quote
|
|
244
|
+
// disappears from its page.
|
|
211
245
|
evidence: z
|
|
212
246
|
.array(z.strictObject({
|
|
213
247
|
rule: z.string().min(1),
|
package/dist/schema/common.js
CHANGED
|
@@ -19,7 +19,7 @@ export const IsoDate = z.iso.date();
|
|
|
19
19
|
// YAML reads `period: 2026` as a number, so accept both and normalize.
|
|
20
20
|
export const PeriodKey = z.preprocess((v) => (typeof v === 'number' ? String(v) : v), z
|
|
21
21
|
.string()
|
|
22
|
-
.regex(/^\d{4}(-Q[1-4]|-(0[1-9]|1[0-2]))?$/, 'expected a period like 2026, 2026-Q4 or 2026-10'));
|
|
22
|
+
.regex(/^\d{4}(-Q[1-4]|-H[12]|-(0[1-9]|1[0-2]))?$/, 'expected a period like 2026, 2026-Q4, 2026-H2 or 2026-10'));
|
|
23
23
|
export const ValueRange = z
|
|
24
24
|
.tuple([z.number().positive(), z.number().positive()])
|
|
25
25
|
.refine(([low, high]) => low <= high, 'range must be [low, high]');
|
package/dist/schema/merchant.js
CHANGED
|
@@ -13,6 +13,9 @@ export const MerchantSchema = z.strictObject({
|
|
|
13
13
|
aliases: z.array(z.string().min(1)).default([]),
|
|
14
14
|
domains: z.array(z.string().min(1)).default([]),
|
|
15
15
|
category: CategoryId,
|
|
16
|
+
// A brand of another merchant (Hampton Inn of Hilton): rules that name the
|
|
17
|
+
// parent's id also match the brand. One level only.
|
|
18
|
+
brand_of: KebabId.optional(),
|
|
16
19
|
likely_mcc: Mcc.optional(),
|
|
17
20
|
accepts: Accepts.optional(),
|
|
18
21
|
in_store: ChannelOverride.optional(),
|
package/dist/schema/wallet.js
CHANGED
|
@@ -16,6 +16,10 @@ const CapUsage = z.strictObject({
|
|
|
16
16
|
period: PeriodKey,
|
|
17
17
|
spent: z.number().nonnegative(), // dollars
|
|
18
18
|
});
|
|
19
|
+
const CreditUsage = z.strictObject({
|
|
20
|
+
period: PeriodKey,
|
|
21
|
+
used: z.number().nonnegative(), // dollars of credit
|
|
22
|
+
});
|
|
19
23
|
export const WalletCardSchema = z.strictObject({
|
|
20
24
|
id: KebabId,
|
|
21
25
|
checkout_label: z
|
|
@@ -42,6 +46,10 @@ export const WalletCardSchema = z.strictObject({
|
|
|
42
46
|
activated: z.array(KebabId).default([]),
|
|
43
47
|
// Keyed by milestone id: every purchase on the card this period.
|
|
44
48
|
milestone_usage: z.record(KebabId, CapUsage).optional(),
|
|
49
|
+
// Merchant credits (the card's `credits`) the cardholder has enrolled in,
|
|
50
|
+
// by credit id, and the dollars of each credit already received this period.
|
|
51
|
+
enrolled: z.array(KebabId).optional(),
|
|
52
|
+
credit_usage: z.record(KebabId, CreditUsage).optional(),
|
|
45
53
|
// A welcome bonus in progress. Terms default to the card's published
|
|
46
54
|
// offer; set spend, by, points or dollars when yours was different.
|
|
47
55
|
bonus: z
|