earnrate 0.2.9 → 0.2.10

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/data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  Every change to card rules, merchants, categories or currencies under `data/`. Bump `data/VERSION` (`YYYY.MM.DD`, plus `.N` for a second release on the same day) and add a section here in the same commit. Each row cites the issuer page the new value came from.
4
4
 
5
+ ## 2026.11.03.4
6
+
7
+ Fixes from testing 0.2.9 in Claude. "BJ's" alone now means BJ's Wholesale Club (BJ's Restaurant & Brewhouse keeps its own names), and the streaming rules on Blue Cash Preferred, United Quest and IHG One Rewards read "select streaming services" with a few examples instead of every service. No rates changed.
8
+
9
+ | Card or merchant | Field | Old → new | Source |
10
+ | ------------------------ | ----------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
11
+ | bjs-wholesale-club | aliases | adds "BJ's" | n/a |
12
+ | bjs-restaurant | file | moved to overrides.yaml, so its own names still win over "BJ's" | n/a |
13
+ | amex-blue-cash-preferred | us-streaming note | shortened; the services earnrate doesn't know yet move to a todo | [Amex Select U.S. Streaming list](https://www.americanexpress.com/us/content/rewards-info/retail.html) |
14
+ | chase-united-quest | streaming note | shortened | [Chase rewards category FAQ](https://www.chase.com/personal/credit-cards/rewards-category-faq) |
15
+ | chase-ihg-one-rewards | streaming note | shortened | [Chase rewards category FAQ](https://www.chase.com/personal/credit-cards/rewards-category-faq) |
16
+
5
17
  ## 2026.11.03.3
6
18
 
7
19
  Every card has a foreign transaction fee, from its issuer's rates-and-fees table where one is readable. Abroad, answers rank each card after its fee. Atmos Rewards Summit's 3x on foreign purchases is modeled with a new `foreign_only` geography. Six cards have no readable page stating the fee and are left as todos.
package/data/VERSION CHANGED
@@ -1 +1 @@
1
- 2026.11.03.3
1
+ 2026.11.03.4
@@ -61,7 +61,7 @@ rules:
61
61
  }
62
62
  rate: 6
63
63
  geography: us_only
64
- note: Only providers on Amex's Select U.S. Streaming Subscriptions list (americanexpress.com/rewards-info, read 2026-11-03). The list also names AMC+, Britbox, iHeartRadio, Kindle Unlimited, Luminary and MGM+, which earnrate doesn't know as stores yet. Subscriptions bundled with or billed by another company (cable, phone, a car maker) may not count.
64
+ note: Only the services on Amex's Select U.S. Streaming list, like Netflix, Hulu, Disney+, Spotify and Audible. Subscriptions bundled with or billed by another company (cable, phone, a car maker) may not count.
65
65
  - id: transit
66
66
  match: { categories: [transit] }
67
67
  rate: 3
@@ -72,6 +72,8 @@ rules:
72
72
  rate: 3
73
73
  geography: us_only
74
74
  note: Superstores, supermarkets and warehouse clubs that sell gas are not gas stations.
75
+ todo:
76
+ - Amex's Select U.S. Streaming list also names AMC+, Britbox, iHeartRadio, Kindle Unlimited, Luminary and MGM+, which earnrate doesn't know as stores yet.
75
77
  # Issuer text backing each rule; `yarn check-sources` checks it weekly.
76
78
  evidence:
77
79
  - rule: base_rate
@@ -60,7 +60,7 @@ rules:
60
60
  }
61
61
  rate: 3
62
62
  valid_to: 2026-12-31
63
- note: Only Chase's select streaming list (Apple Music, Apple TV, Disney+, ESPN+, Fubo TV, Max, Hulu, Netflix, Pandora, Paramount+, Peacock, Showtime, SiriusXM, Sling, Spotify, YouTube Premium, YouTube TV and Vudu). Purchases posting after Dec 31, 2026 earn the 2x base rate.
63
+ note: Only the services on Chase's select streaming list, like Netflix, Hulu, Disney+ and Spotify. Purchases posting after Dec 31, 2026 earn the 2x base rate.
64
64
  milestones:
65
65
  - id: spend-15k
66
66
  spend: 15000
@@ -64,7 +64,7 @@ rules:
64
64
  ],
65
65
  }
66
66
  rate: 2
67
- note: Only Chase's select streaming list (Apple Music, Apple TV, Disney+, ESPN+, Fubo TV, Max, Hulu, Netflix, Pandora, Paramount+, Peacock, Showtime, SiriusXM, Sling, Spotify, YouTube Premium, YouTube TV, Vudu).
67
+ note: Only the services on Chase's select streaming list, like Netflix, Hulu, Disney+ and Spotify.
68
68
  todo:
69
69
  - United Card Events from Chase also earn 4x; no merchant exists for them, so they aren't modeled.
70
70
  # Issuer text backing each rule; `yarn check-sources` checks it weekly.
@@ -287,10 +287,16 @@
287
287
  # drugstores" (chase.com/personal/credit-cards/rewards-category-faq).
288
288
  - id: bjs-wholesale-club
289
289
  name: "BJ's Wholesale Club"
290
- aliases: [bjs wholesale, bjs wholesale club, bjs club]
290
+ aliases: [bjs, bjs wholesale, bjs wholesale club, bjs club]
291
291
  domains: [bjs.com]
292
292
  category: warehouse_club
293
- note: Codes as a warehouse club, which Amex, Chase and Capital One leave out of their grocery bonuses.
293
+ note: Codes as a warehouse club, which Amex, Chase and Capital One leave out of their grocery bonuses. For the restaurant chain, ask about BJ's Restaurant & Brewhouse.
294
+ # Listed here, beside BJ's Wholesale Club, so "BJ's" alone means the club
295
+ # and the restaurant still answers to its own names.
296
+ - id: bjs-restaurant
297
+ name: "BJ's Restaurant & Brewhouse"
298
+ aliases: [bjs restaurant, bjs restaurant and brewhouse, bjs brewhouse]
299
+ category: dining
294
300
  - id: bjs-gas
295
301
  name: "BJ's Gas"
296
302
  aliases: [bjs gas, bjs gas station]
@@ -266,12 +266,6 @@
266
266
  aliases: [pf changs, p f changs, p.f. changs],
267
267
  category: dining,
268
268
  }
269
- - {
270
- id: bjs-restaurant,
271
- name: "BJ's Restaurant & Brewhouse",
272
- aliases: [bjs restaurant, bjs restaurant and brewhouse, bjs brewhouse],
273
- category: dining,
274
- }
275
269
  - { id: waffle-house, name: Waffle House, category: dining }
276
270
  - {
277
271
  id: tgi-fridays,
@@ -2,7 +2,7 @@ import { categoryResolution, resolvePlace } from "../merchants/resolve.js";
2
2
  import { evaluateCard, purchaseContext } from "./evaluate.js";
3
3
  import { membershipName, membershipNeeded, paidMemberships, } from "./memberships.js";
4
4
  import { choicePeriod, periodKey } from "./periods.js";
5
- import { recommend } from "./recommend.js";
5
+ import { FOREIGN_ASSUMPTION, isForeign, recommend } from "./recommend.js";
6
6
  import { rotationGap } from "./rotation.js";
7
7
  import { membershipsUsed } from "./setup.js";
8
8
  import { optionName } from "./rule-text.js";
@@ -129,8 +129,10 @@ function membershipUpgrades(catalog, card, held, merchant, purchase, options, cu
129
129
  memberships: [...options.memberships, membership],
130
130
  });
131
131
  const ctx = purchaseContext(catalog, wallet, merchant, merchant.category, purchase);
132
+ const fee = isForeign(purchase) ? (card.foreign_transaction_fee ?? 0) : 0;
132
133
  const pct = evaluateCard(card, held, ctx).best.rate *
133
- cardValuation(catalog, wallet, card).centsPerUnit;
134
+ cardValuation(catalog, wallet, card).centsPerUnit -
135
+ fee;
134
136
  if (pct > current + EPSILON)
135
137
  upgrades.push({ membership, effectiveReturnPct: pct });
136
138
  }
@@ -212,11 +214,11 @@ export function exploreCards(catalog, query, options, resolved) {
212
214
  merchant,
213
215
  ranked,
214
216
  excluded,
215
- assumptions: exploreAssumptions(catalog, merchant, ranked, options),
217
+ assumptions: exploreAssumptions(catalog, merchant, ranked, options, purchase),
216
218
  warnings: exploreWarnings(catalog, merchant),
217
219
  };
218
220
  }
219
- function exploreAssumptions(catalog, merchant, ranked, options) {
221
+ function exploreAssumptions(catalog, merchant, ranked, options, purchase) {
220
222
  const assumptions = [];
221
223
  if (merchant.source === 'category') {
222
224
  assumptions.push(`Ranks purchases that code as ${categoryPhrase(catalog, merchant.category)}.`);
@@ -231,6 +233,8 @@ function exploreAssumptions(catalog, merchant, ranked, options) {
231
233
  const values = pointValuesText(catalog, ranked.slice(0, 10));
232
234
  if (values !== null)
233
235
  assumptions.push(values);
236
+ if (isForeign(purchase))
237
+ assumptions.push(FOREIGN_ASSUMPTION);
234
238
  return assumptions;
235
239
  }
236
240
  function exploreWarnings(catalog, merchant) {
@@ -45,11 +45,19 @@ export function milestonePays(m) {
45
45
  }
46
46
  // "the $15,000 calendar-year spend for 10,000 bonus points"
47
47
  export const milestoneWhat = (m) => `the ${wholeDollars(m.spend)} ${MILESTONE_PERIODS[m.period]} spend for ${milestonePays(m)}`;
48
- export function reachedText(card, hit) {
48
+ export function reachedText(card, hit, centsPerPoint) {
49
49
  const m = card.milestones.find((x) => x.id === hit.id);
50
50
  if (!m)
51
51
  return '';
52
- return `This purchase reaches ${milestoneWhat(m)} (${money(hit.before)} tracked on this card in ${hit.period} before it).`;
52
+ const points = hit.points === null
53
+ ? null
54
+ : `${hit.points.toLocaleString('en-US')} bonus points, worth about ${money(roundCents((hit.points * centsPerPoint) / 100))}`;
55
+ const pays = points === null
56
+ ? listed(hit.rewards)
57
+ : hit.rewards.length === 0
58
+ ? points
59
+ : `${points}, plus ${listed(hit.rewards)}`;
60
+ return `This purchase reaches the ${wholeDollars(m.spend)} ${MILESTONE_PERIODS[m.period]} spend: ${pays} (${money(hit.before)} tracked on this card in ${hit.period} before it).`;
53
61
  }
54
62
  const THIS_PERIOD = {
55
63
  calendar_year: 'this year',
@@ -11,6 +11,7 @@ import { CAP_ADJECTIVES, categoryPhrase, codesAs, currencyLabel, currencyName, e
11
11
  import { cardValuation } from "./valuation.js";
12
12
  export const STALE_AFTER_DAYS = 180;
13
13
  // The engine only tells the U.S. from everywhere else.
14
+ export const FOREIGN_ASSUMPTION = 'Treats this as a purchase outside the U.S.: each card is ranked after its foreign transaction fee.';
14
15
  export const isForeign = (purchase) => purchase.country !== 'US';
15
16
  // Any purchase outside the U.S., when no country is named: ZZ is a code ISO
16
17
  // leaves unassigned, and nothing prints it.
@@ -87,6 +88,7 @@ function rankEntries(catalog, wallet, purchase, merchant, category) {
87
88
  perDollar,
88
89
  fee,
89
90
  effectiveReturnPct: perDollar * valuation.centsPerUnit - fee,
91
+ rateReturnPct: evaluation.best.rate * valuation.centsPerUnit - fee,
90
92
  upside: perDollar * (valuation.range?.[1] ?? valuation.centsPerUnit) - fee,
91
93
  });
92
94
  }
@@ -111,7 +113,7 @@ function reasoningFor(catalog, wallet, entry, amount) {
111
113
  lines.push(`${money(cap.remaining)} earns ${earnText(catalog, best.nominalRate, currency)} before the cap, then ${money(over)} earns ${earnText(catalog, best.afterCap ?? cap.cap.then_rate, currency)}: ${num(best.rate)} per dollar overall.`);
112
114
  }
113
115
  for (const hit of entry.milestones)
114
- lines.push(reachedText(card, hit));
116
+ lines.push(reachedText(card, hit, valuation.centsPerUnit));
115
117
  if (valuation.pooledFrom !== null) {
116
118
  const partners = card.pools_into?.when_wallet_has ?? [];
117
119
  const heldPartners = wallet.cards
@@ -122,7 +124,7 @@ function reasoningFor(catalog, wallet, entry, amount) {
122
124
  lines.push(`Pools into ${currencyName(catalog, currency)} because you hold ${heldPartners.join(' and ')}.`);
123
125
  }
124
126
  if (currency !== 'cash') {
125
- lines.push(`Valued at ${num(valuation.centsPerUnit)}¢ per point: ${pct(entry.perDollar * valuation.centsPerUnit)} back.`);
127
+ lines.push(`Valued at ${num(valuation.centsPerUnit)}¢ per point: ${pct(entry.evaluation.best.rate * valuation.centsPerUnit)} back.`);
126
128
  }
127
129
  return lines;
128
130
  }
@@ -184,11 +186,19 @@ function toRanked(catalog, wallet, entry, purchase) {
184
186
  rate: evaluation.best.rate,
185
187
  currency: valuation.currency,
186
188
  centsPerUnit: valuation.centsPerUnit,
187
- effectiveReturnPct: entry.effectiveReturnPct,
189
+ effectiveReturnPct: entry.rateReturnPct,
188
190
  foreignFee: entry.fee > 0 ? entry.fee : null,
189
191
  earned: purchase.amount === null
190
192
  ? null
191
- : Math.round(purchase.amount * entry.effectiveReturnPct) / 100,
193
+ : Math.round(purchase.amount * entry.rateReturnPct) / 100,
194
+ milestoneBonus: entry.milestones.length === 0
195
+ ? null
196
+ : {
197
+ points: milestonePoints(entry.milestones),
198
+ dollars: roundCents((milestonePoints(entry.milestones) *
199
+ entry.valuation.centsPerUnit) /
200
+ 100),
201
+ },
192
202
  capped: evaluation.best.cap !== null,
193
203
  reasoning: reasoningFor(catalog, wallet, entry, purchase.amount),
194
204
  warnings: warningsFor(catalog, entry, purchase),
@@ -215,6 +225,8 @@ function tieNote(entries) {
215
225
  // Finds the closest point inside a currency's value range where the winner
216
226
  // stops winning, varying one currency at a time. Returns are linear in the
217
227
  // cents-per-point value, so each pair of cards flips at most once.
228
+ // "Chase Ultimate Rewards points", but not "IHG One Rewards points points".
229
+ const pointsName = (label) => /\b(points|miles)$/i.test(label) ? label : `${label} points`;
218
230
  function sensitivity(catalog, entries) {
219
231
  const [winner, ...rest] = entries;
220
232
  if (!winner)
@@ -235,13 +247,13 @@ function sensitivity(catalog, entries) {
235
247
  if (w.range && wRate > 0) {
236
248
  const threshold = (other.effectiveReturnPct + winner.fee) / wRate;
237
249
  if (threshold < w.centsPerUnit && threshold >= w.range[0]) {
238
- consider((w.centsPerUnit - threshold) / w.centsPerUnit, `${displayName(other)} beats ${displayName(winner)} here if ${currencyLabel(catalog, w.currency)} points are worth less than ${roundDown(threshold)}¢ (you value them at ${num(w.centsPerUnit)}¢).`);
250
+ consider((w.centsPerUnit - threshold) / w.centsPerUnit, `${displayName(other)} beats ${displayName(winner)} here if ${pointsName(currencyLabel(catalog, w.currency))} are worth less than ${roundDown(threshold)}¢ (you value them at ${num(w.centsPerUnit)}¢).`);
239
251
  }
240
252
  }
241
253
  if (o.range && oRate > 0) {
242
254
  const threshold = (winner.effectiveReturnPct + other.fee) / oRate;
243
255
  if (threshold > o.centsPerUnit && threshold <= o.range[1]) {
244
- consider((threshold - o.centsPerUnit) / o.centsPerUnit, `${displayName(other)} beats ${displayName(winner)} here if ${currencyLabel(catalog, o.currency)} points are worth ${worthAbove(threshold)} (you value them at ${num(o.centsPerUnit)}¢).`);
256
+ consider((threshold - o.centsPerUnit) / o.centsPerUnit, `${displayName(other)} beats ${displayName(winner)} here if ${pointsName(currencyLabel(catalog, o.currency))} are worth ${worthAbove(threshold)} (you value them at ${num(o.centsPerUnit)}¢).`);
245
257
  }
246
258
  }
247
259
  }
@@ -256,7 +268,19 @@ function assumptionsFor(catalog, merchant, purchase, entries) {
256
268
  assumptions.push(`Ranks purchases that code as ${categoryPhrase(catalog, merchant.category)}.`);
257
269
  }
258
270
  else if (merchant.category === 'other') {
259
- assumptions.push(`Assumes ${merchant.resolvedName} falls outside the usual bonus categories.`);
271
+ // A card whose rule names this store by name (Audible on Amex's
272
+ // streaming list) earns its bonus there anyway.
273
+ const naming = entries.flatMap((e) => {
274
+ const rule = e.evaluation.best.rule;
275
+ return rule &&
276
+ merchant.merchantId !== null &&
277
+ (rule.match.merchants ?? []).includes(merchant.merchantId)
278
+ ? [`${displayName(e)} (${ruleWhat(catalog, rule)})`]
279
+ : [];
280
+ });
281
+ assumptions.push(naming.length === 0
282
+ ? `Assumes ${merchant.resolvedName} falls outside the usual bonus categories.`
283
+ : `Assumes ${merchant.resolvedName} falls outside the usual bonus categories, except on cards that name it: ${listed(naming)}.`);
260
284
  }
261
285
  else {
262
286
  assumptions.push(codesAs(catalog, merchant));
@@ -274,7 +298,7 @@ function assumptionsFor(catalog, merchant, purchase, entries) {
274
298
  if (values !== null)
275
299
  assumptions.push(values);
276
300
  if (isForeign(purchase))
277
- assumptions.push('Treats this as a purchase outside the U.S.: each card is ranked after its foreign transaction fee.');
301
+ assumptions.push(FOREIGN_ASSUMPTION);
278
302
  for (const e of entries)
279
303
  for (const hit of e.milestones)
280
304
  assumptions.push(`Counts the ${money(hit.before)} tracked on ${displayName(e)} in ${hit.period} toward its ${wholeDollars(hit.spend)} spend bonus; spending earnrate wasn't told about isn't counted.`);
@@ -34,10 +34,18 @@ export function ruleParts(catalog, rule) {
34
34
  if (rule.match.choice !== undefined)
35
35
  return [`your ${choiceName(rule.match.choice)} picks`];
36
36
  const merchants = rule.match.merchants ?? [];
37
- // A long list of one kind of store reads as "select streaming services".
38
- const kinds = new Set(merchants.map((m) => findMerchant(catalog, m)?.category));
39
- const [kind] = kinds;
40
- const merchantParts = merchants.length > 4 && kinds.size === 1 && kind !== undefined
37
+ // A long list of mostly one kind of store reads as "select streaming
38
+ // services": Amex's streaming list also names Audible, which isn't one.
39
+ const counts = new Map();
40
+ for (const m of merchants) {
41
+ const category = findMerchant(catalog, m)?.category;
42
+ if (category)
43
+ counts.set(category, (counts.get(category) ?? 0) + 1);
44
+ }
45
+ const [kind, count = 0] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [];
46
+ const merchantParts = merchants.length > 4 &&
47
+ kind !== undefined &&
48
+ count >= merchants.length * 0.75
41
49
  ? [`select ${categoryPhrase(catalog, kind)}`]
42
50
  : merchants.map((m) => merchantName(catalog, m));
43
51
  const parts = [
@@ -30,7 +30,10 @@ function cardLines(cards, style, bonusPick) {
30
30
  const pctWidth = Math.max(...cards.map((c) => pct(c.effectiveReturnPct).length));
31
31
  const lines = [];
32
32
  cards.forEach((c) => {
33
- const earned = c.earned === null ? '' : ` ${dollarsAndCents(c.earned)}`;
33
+ const earned = (c.earned === null ? '' : ` ${dollarsAndCents(c.earned)}`) +
34
+ (c.milestoneBonus === null
35
+ ? ''
36
+ : ` + ${money(c.milestoneBonus.dollars)} spend bonus`);
34
37
  const pick = c.cardId === bonusPick ? ' ← the pick, for its welcome bonus' : '';
35
38
  lines.push(` ${String(c.rank).padStart(String(rankWidth).length)}. ${label(c).padEnd(nameWidth)} ${pct(c.effectiveReturnPct).padStart(pctWidth)}${earned}${pick}`);
36
39
  for (const r of c.reasoning)
@@ -53,9 +56,12 @@ export function formatRecommendation(rec, purchase, { all = false, style = plain
53
56
  const noCard = noCardText(rec.merchant.resolvedName);
54
57
  if (rec.decision && top) {
55
58
  const earned = top.earned === null ? '' : `, about ${dollarsAndCents(top.earned)}`;
59
+ const milestone = top.milestoneBonus === null
60
+ ? ''
61
+ : `, plus a spend bonus worth about ${money(top.milestoneBonus.dollars)}`;
56
62
  const confidence = rec.decision.confidence;
57
63
  const why = rec.bonus ? ' for its welcome bonus' : '';
58
- lines.push(`${style('bold', `Use ${label(top)}${why}: ${pct(top.effectiveReturnPct)} back${earned}.`)} ${capitalize(confidence)} confidence.`);
64
+ lines.push(`${style('bold', `Use ${label(top)}${why}: ${pct(top.effectiveReturnPct)} back${earned}${milestone}.`)} ${capitalize(confidence)} confidence.`);
59
65
  if (rec.bonus && rec.notes[0])
60
66
  lines.push(rec.notes[0]);
61
67
  }
@@ -36,7 +36,7 @@ export function formatWalletSummary(summary) {
36
36
  for (const cap of c.caps)
37
37
  lines.push(cap.remaining === null
38
38
  ? ` Cap on ${cap.label}: ${money(cap.amount)} ${CAP_PERIODS[cap.period]}; give the card's anniversary to track it (cap key ${cap.key}).`
39
- : ` Cap on ${cap.label}: ${money(cap.spent)} of ${money(cap.amount)} used for ${cap.currentPeriod ?? ''}${cap.until === null ? '' : `, through ${shortDate(cap.until)}`} (cap key ${cap.key}).`);
39
+ : ` Cap on ${cap.label}: ${money(cap.spent)} of ${money(cap.amount)} used ${cap.period === 'cardmember_year' ? 'this cardmember year' : `for ${cap.currentPeriod ?? ''}`}${cap.until === null ? '' : `, through ${shortDate(cap.until)}`} (cap key ${cap.key}).`);
40
40
  for (const m of c.milestones)
41
41
  lines.push(` Spend bonus (${m.id}): ${m.text}`);
42
42
  if (c.anniversary !== null)
@@ -76,7 +76,11 @@ export function formatBooking(amount, cardId, cap, bonus, milestones = []) {
76
76
  messages.push([
77
77
  `Spend on ${cardId} this year: ${money(first.after)}.`,
78
78
  ...reached.map((m) => `That reaches its ${money(m.spend)} spend bonus.`),
79
- ...(next ? [`${money(next.spend - next.after)} to its next one.`] : []),
79
+ ...(next
80
+ ? [
81
+ `${money(next.spend - next.after)} to go for the ${money(next.spend)} spend bonus.`,
82
+ ]
83
+ : []),
80
84
  ].join(' '));
81
85
  }
82
86
  return messages.join(' ');
@@ -40,6 +40,9 @@ const RankedCardSchema = z.object({
40
40
  effectiveReturnPct: z.number(),
41
41
  earned: z.number().nullable(),
42
42
  foreignFee: z.number().nullable(),
43
+ milestoneBonus: z
44
+ .object({ points: z.number(), dollars: z.number() })
45
+ .nullable(),
43
46
  capped: z.boolean(),
44
47
  reasoning: z.array(z.string()),
45
48
  warnings: z.array(z.string()),
@@ -45,7 +45,23 @@ The wallet lists cards by product id, never card numbers; never ask for card num
45
45
 
46
46
  ${WHICH_WALLET[savedWallets]} The earnrate extension on the user's computer and the hosted earnrate connector keep separate wallets with the same tools. If both are connected, say which wallet an answer comes from, and ask the user which one to use before saving or recording anything.`;
47
47
  export function createEarnrateServer(deps) {
48
- const server = new McpServer({ name: MCP_SERVER_NAME, version: deps.version }, { instructions: instructions(deps.savedWallets) });
48
+ const server = new McpServer({
49
+ name: MCP_SERVER_NAME,
50
+ version: deps.version,
51
+ websiteUrl: 'https://earnrate.dev',
52
+ icons: [
53
+ {
54
+ src: 'https://earnrate.dev/icon-192.png',
55
+ mimeType: 'image/png',
56
+ sizes: ['192x192'],
57
+ },
58
+ {
59
+ src: 'https://earnrate.dev/favicon.svg',
60
+ mimeType: 'image/svg+xml',
61
+ sizes: ['any'],
62
+ },
63
+ ],
64
+ }, { instructions: instructions(deps.savedWallets) });
49
65
  const ctx = makeContext(deps);
50
66
  const inputs = makeInputs(deps.catalog);
51
67
  registerAnswerTools(server, deps, ctx, inputs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "earnrate",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
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",