rightcard-mcp 0.1.3 → 0.1.4

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/dist/engine.js CHANGED
@@ -48,19 +48,19 @@ function tieNote(winner, runnerUp, value, v) {
48
48
  if (wRank > rRank) {
49
49
  const winProgram = effectiveProgram(v, winner);
50
50
  if (v.mode === "cash" && winProgram !== "cash" && cashOutCPP(winProgram) >= 1.0) {
51
- return `${win} ties ${other} at ${rate} — picked ${win}: its points cash out the same, and can be worth more if you ever redeem for travel.`;
51
+ return `${other} also earns ${rate} here. We picked ${win} because its points cash out the same and can be worth more for travel.`;
52
52
  }
53
53
  const smart = { mode: "smart", pooledPrograms: v.pooledPrograms };
54
54
  if (v.mode === "cash" && centsPerPoint(smart, runnerUp) > 1.0) {
55
- return `${win} and ${other} tie at ${rate} for cash. ${other}'s points can be worth more if you redeem for travel — switch to Points & miles to compare.`;
55
+ return `${other} also earns ${rate} here. Its points can be worth more for travel, so try Points & miles.`;
56
56
  }
57
57
  const why = v.mode === "smart" ? "transfer flexibility" : "guaranteed cash back";
58
- return `${win} ties ${other} at ${rate} — picked ${win} for ${why}.`;
58
+ return `${other} also earns ${rate} here. We picked ${win} for ${why}.`;
59
59
  }
60
60
  if (premiumRank(winner) > premiumRank(runnerUp)) {
61
- return `${win} ties ${other} at ${rate} — picked ${win}: the premium card's purchase protections come free at the same rate.`;
61
+ return `${other} also earns ${rate} here. We picked ${win} for its purchase protection.`;
62
62
  }
63
- return `${win} ties ${other} at ${rate} — either works.`;
63
+ return `${other} also earns ${rate} here. Either one works.`;
64
64
  }
65
65
  const PORTAL_BONUSES = [
66
66
  { needles: ["sapphire reserve"], rate: 10, portal: "Chase Travel", premium: true },
@@ -202,7 +202,14 @@ function rateContext(card, earn, valueCents, override, configured, v, category,
202
202
  }
203
203
  else {
204
204
  reason = `${card.displayName} earns the most for ${cat} — ${valueStr} per dollar.`;
205
- lines.push(`Base ${cat} earn: ${valueStr}`);
205
+ // Mirrors the Swift engine (Sep 2026): a points/miles card in cash mode
206
+ // still EARNS points — never blur the earn (4x points) with its value (4%).
207
+ if (card.rewardCurrency !== null && card.rewardCurrency !== "cashback") {
208
+ lines.push(`Base ${cat} earn: ${formatMultiplier(earn)} ${earnUnitLabel(v, card)} → ${valueStr}`);
209
+ }
210
+ else {
211
+ lines.push(`Base ${cat} earn: ${valueStr}`);
212
+ }
206
213
  }
207
214
  let caption = configured ? "your bonus category · value per $1" : override ? "rotating bonus · value per $1" : "value per $1";
208
215
  let valueMath = null;
package/dist/model.d.ts CHANGED
@@ -36,7 +36,13 @@ export interface Override {
36
36
  merchant: string | null;
37
37
  }
38
38
  export declare function overrideFromRow(r: any): Override;
39
- /** UTC-day canon (SupabaseDateFormat.utcCalendar): compare yyyy-MM-dd strings as UTC days. */
39
+ /** Compare yyyy-MM-dd strings as UTC days.
40
+ *
41
+ * The iOS app judges windows in the USER'S calendar date (Sep 13 2026: an offer
42
+ * or quarter marked 9/30 is live through the end of 9/30 where the user is).
43
+ * This server has no user timezone, so it keeps UTC; the two can differ only
44
+ * inside the few hours around midnight on a window's first or last day. The
45
+ * golden fixture is exported at noon UTC so parity is unaffected. */
40
46
  export declare function utcDay(now: Date): string;
41
47
  export declare function overrideIsActive(o: Override, now: Date): boolean;
42
48
  /** SupabaseOverride.isPermanentMerchantBenefit: merchant-scoped, no activation, ends > 2 years out. */
@@ -54,8 +60,10 @@ export declare function merchantFromRow(r: any): Merchant;
54
60
  /** Merchant.exclusionCaveat */
55
61
  export declare function exclusionCaveat(m: Merchant): string | null;
56
62
  /** Merchant.acceptedNetworksAtRegister — Costco's Visa-only register lock.
57
- * null for everything else (no restriction). Costco.com / online rows are
58
- * never gated (they take other networks). Mirrors the Swift twin exactly. */
63
+ * EVERY physical Costco row (checkout, pharmacy, gas, tire) is Visa-only
64
+ * for credit; null for everything else (no restriction). Costco.com /
65
+ * online rows are never gated (they take other networks). Mirrors the
66
+ * Swift twin exactly. */
59
67
  export declare function acceptedNetworksAtRegister(m: Merchant): Set<string> | null;
60
68
  /** Merchant.isCuratedBrand */
61
69
  export declare function isCuratedBrand(m: Merchant): boolean;
package/dist/model.js CHANGED
@@ -86,7 +86,13 @@ export function overrideFromRow(r) {
86
86
  label: r.label ?? null, merchant: r.merchant ?? null,
87
87
  };
88
88
  }
89
- /** UTC-day canon (SupabaseDateFormat.utcCalendar): compare yyyy-MM-dd strings as UTC days. */
89
+ /** Compare yyyy-MM-dd strings as UTC days.
90
+ *
91
+ * The iOS app judges windows in the USER'S calendar date (Sep 13 2026: an offer
92
+ * or quarter marked 9/30 is live through the end of 9/30 where the user is).
93
+ * This server has no user timezone, so it keeps UTC; the two can differ only
94
+ * inside the few hours around midnight on a window's first or last day. The
95
+ * golden fixture is exported at noon UTC so parity is unaffected. */
90
96
  export function utcDay(now) {
91
97
  return now.toISOString().slice(0, 10);
92
98
  }
@@ -129,17 +135,28 @@ export function exclusionCaveat(m) {
129
135
  return base;
130
136
  }
131
137
  default:
138
+ // Physical Costco DEPARTMENTS (pharmacy / gas / tire / …) share the
139
+ // warehouse's Visa-only registers; their own MCC stays honest (no
140
+ // "bonuses don't post" clause). Mirrors the Swift engine (Sep 2026).
141
+ if (acceptedNetworksAtRegister(m) !== null) {
142
+ return "Costco takes Visa only at the register — this is your best card that works in-store. Other networks work on Costco.com.";
143
+ }
132
144
  return null;
133
145
  }
134
146
  }
135
147
  /** Merchant.acceptedNetworksAtRegister — Costco's Visa-only register lock.
136
- * null for everything else (no restriction). Costco.com / online rows are
137
- * never gated (they take other networks). Mirrors the Swift twin exactly. */
148
+ * EVERY physical Costco row (checkout, pharmacy, gas, tire) is Visa-only
149
+ * for credit; null for everything else (no restriction). Costco.com /
150
+ * online rows are never gated (they take other networks). Mirrors the
151
+ * Swift twin exactly. */
138
152
  export function acceptedNetworksAtRegister(m) {
139
153
  const name = m.displayName.toLowerCase();
140
- if (m.merchantType !== "warehouse_club" || !name.includes("costco"))
154
+ if (!name.includes("costco"))
141
155
  return null;
142
- if (m.isOnline || name.includes(".com"))
156
+ // NAME is the online signal, deliberately not isOnline — the LLM-minted
157
+ // flag is noise on this family (costco_pharmacy sat is_online=true).
158
+ // Mirrors the Swift twin exactly.
159
+ if (name.includes(".com") || name.includes("online"))
143
160
  return null;
144
161
  return new Set(["visa"]);
145
162
  }
package/dist/search.js CHANGED
@@ -96,7 +96,11 @@ export function matchScore(q, qc, rawQuery, candidates) {
96
96
  const words = n.split(" ");
97
97
  if (words.slice(1).some((w) => w.startsWith(q)))
98
98
  best = Math.max(best, 70);
99
- if (n.includes(q))
99
+ // INTERIOR substring, only for a query long enough to mean something. A 2–3
100
+ // character query matching mid-word is noise: "LG" returned Algenist,
101
+ // Bulgari, Trafalgar and the glued alias "naturalgrocers.com" and no LG
102
+ // (user report, Sep 17 2026). Mirrors Merchant.matchScore in the app.
103
+ if ([...q].length >= 4 && n.includes(q))
100
104
  best = Math.max(best, 55);
101
105
  if (best < 45 && merchantMatches(c.raw, rawQuery))
102
106
  best = Math.max(best, 45);
@@ -1,6 +1,6 @@
1
1
  import type { CreditCard } from "./model.js";
2
2
  export type ValuationMode = "cash" | "smart";
3
- export type RewardProgram = "cash" | "amexMR" | "chaseUR" | "capOneMiles" | "citiTYP" | "wellsFargo" | "bilt" | "tdRewards" | "scenePlus" | "rbcAvion" | "bmoRewards" | "hyatt" | "marriott" | "ihg" | "hilton" | "delta" | "united" | "american" | "southwest" | "jetBlue" | "alaska" | "avios" | "aeroplan" | "fixedValue" | "genericPoints" | "genericMiles";
3
+ export type RewardProgram = "cash" | "amexMR" | "chaseUR" | "capOneMiles" | "citiTYP" | "wellsFargo" | "bilt" | "tdRewards" | "scenePlus" | "rbcAvion" | "bmoRewards" | "hyatt" | "marriott" | "ihg" | "choice" | "hilton" | "delta" | "united" | "american" | "southwest" | "jetBlue" | "alaska" | "avios" | "aeroplan" | "fixedValue" | "genericPoints" | "genericMiles";
4
4
  export declare const PROGRAM_DISPLAY: Record<RewardProgram, string>;
5
5
  export declare const TRAVEL_CPP: Record<RewardProgram, number>;
6
6
  /** RewardProgram.cashOutCPP */
@@ -20,7 +20,12 @@ export declare function centsPerPoint(v: Valuation, card: CreditCard): number;
20
20
  export declare function valuesPointsAboveCash(v: Valuation, card: CreditCard): boolean;
21
21
  export declare function earnUnitLabel(v: Valuation, card: CreditCard): string;
22
22
  export declare function basisLabel(v: Valuation, card: CreditCard): string;
23
- /** Swift String(format: "%.1f") — printf rounds EXACT binary ties to even
24
- * (1.25 → "1.2"), where Math.round rounds them up ("1.3"). Surfaced Aug 29
25
- * 2026 when a live card hit a 1.25x earn and 27 golden cases split. */
23
+ /** Swift String(format: "%.1f") — printf rounds the EXACT binary value, and
24
+ * only an exact tie (1.25 is representable) goes to even ("1.2"). Surfaced
25
+ * Aug 29 2026 when a live card hit a 1.25x earn and 27 golden cases split.
26
+ * Sep 15 2026: the first mirror scaled by 10 first, and 1.5 × 1.1 =
27
+ * 1.6500000000000001 × 10 landed on exactly 16.5 — a FALSE tie, rounded to
28
+ * "1.6" where Swift prints "1.7" (Delta Biz Platinum transit 1.5x @ 1.1¢).
29
+ * toFixed(20) is the exact decimal expansion of the double, so a tie is a
30
+ * tie only when the digits past the tenths are literally 5000…0. */
26
31
  export declare function fmt1(x: number): string;
package/dist/valuation.js CHANGED
@@ -2,17 +2,19 @@ export const PROGRAM_DISPLAY = {
2
2
  cash: "Cash back", amexMR: "Amex Membership Rewards", chaseUR: "Chase Ultimate Rewards",
3
3
  capOneMiles: "Capital One miles", citiTYP: "Citi ThankYou Points", wellsFargo: "Wells Fargo Rewards",
4
4
  bilt: "Bilt Rewards", tdRewards: "TD Rewards", scenePlus: "Scene+", rbcAvion: "RBC Avion points",
5
- bmoRewards: "BMO Rewards", hyatt: "World of Hyatt", marriott: "Marriott Bonvoy", ihg: "IHG One Rewards",
5
+ bmoRewards: "BMO Rewards", hyatt: "World of Hyatt", marriott: "Marriott Bonvoy", ihg: "IHG One Rewards", choice: "Choice Privileges",
6
6
  hilton: "Hilton Honors", delta: "Delta SkyMiles", united: "United MileagePlus",
7
7
  american: "American AAdvantage", southwest: "Southwest Rapid Rewards", jetBlue: "JetBlue TrueBlue",
8
8
  alaska: "Alaska Mileage Plan", avios: "British Airways Avios", aeroplan: "Air Canada Aeroplan",
9
9
  fixedValue: "Fixed-value miles", genericPoints: "Points", genericMiles: "Miles",
10
10
  };
11
11
  export const TRAVEL_CPP = {
12
- cash: 1.0, amexMR: 2.0, chaseUR: 2.0, capOneMiles: 1.7, citiTYP: 1.7, wellsFargo: 1.5, bilt: 2.0,
12
+ // Sep 2026: median of TPG / Frequent Miler / Upgraded Points / OMAAT, rounded
13
+ // down, never above (mirrors Swift RewardProgram.travelCPP).
14
+ cash: 1.0, amexMR: 1.8, chaseUR: 1.8, capOneMiles: 1.7, citiTYP: 1.6, wellsFargo: 1.5, bilt: 1.8,
13
15
  tdRewards: 0.5, scenePlus: 1.0, rbcAvion: 1.5, bmoRewards: 0.7,
14
- hyatt: 1.7, marriott: 0.7, ihg: 0.6, hilton: 0.5,
15
- delta: 1.1, united: 1.3, american: 1.4, southwest: 1.3, jetBlue: 1.3, alaska: 1.4, avios: 1.3, aeroplan: 1.3,
16
+ hyatt: 1.4, marriott: 0.7, ihg: 0.5, choice: 0.6, hilton: 0.4,
17
+ delta: 1.1, united: 1.2, american: 1.4, southwest: 1.2, jetBlue: 1.3, alaska: 1.4, avios: 1.2, aeroplan: 1.3,
16
18
  fixedValue: 1.0, genericPoints: 2.0, genericMiles: 1.5,
17
19
  };
18
20
  /** RewardProgram.cashOutCPP */
@@ -40,6 +42,10 @@ export function inferProgram(issuer, id, currency) {
40
42
  return "hyatt";
41
43
  if (has("ihg"))
42
44
  return "ihg";
45
+ // Choice Privileges (Wells Fargo-issued) must precede the "wells" needle,
46
+ // which valued these hotel points at 1.5¢ (mirrors Swift, Sep 24 2026).
47
+ if (has("choice"))
48
+ return "choice";
43
49
  if (has("delta", "skymiles"))
44
50
  return "delta";
45
51
  if (has("united", "mileageplus"))
@@ -84,6 +90,10 @@ export function inferProgram(issuer, id, currency) {
84
90
  return "fixedValue";
85
91
  if (has("altitude"))
86
92
  return "fixedValue";
93
+ // GM Rewards (Barclays, Sep 2026): points pegged at 1¢, redeemable only toward
94
+ // GM — the generic 2¢ fallback read its flat 3x as "6%" (mirrors Swift).
95
+ if (has("gm_rewards", "gm rewards"))
96
+ return "fixedValue";
87
97
  return currency === "miles" ? "genericMiles" : "genericPoints";
88
98
  }
89
99
  export function makeValuation(mode = "cash") {
@@ -154,13 +164,23 @@ export function basisLabel(v, card) {
154
164
  const pooled = v.pooledPrograms[card.id] !== undefined ? " (pooled with your wallet)" : "";
155
165
  return `${PROGRAM_DISPLAY[effectiveProgram(v, card)]} · ${fmt1(cpp)}¢ travel value${pooled}`;
156
166
  }
157
- /** Swift String(format: "%.1f") — printf rounds EXACT binary ties to even
158
- * (1.25 → "1.2"), where Math.round rounds them up ("1.3"). Surfaced Aug 29
159
- * 2026 when a live card hit a 1.25x earn and 27 golden cases split. */
167
+ /** Swift String(format: "%.1f") — printf rounds the EXACT binary value, and
168
+ * only an exact tie (1.25 is representable) goes to even ("1.2"). Surfaced
169
+ * Aug 29 2026 when a live card hit a 1.25x earn and 27 golden cases split.
170
+ * Sep 15 2026: the first mirror scaled by 10 first, and 1.5 × 1.1 =
171
+ * 1.6500000000000001 × 10 landed on exactly 16.5 — a FALSE tie, rounded to
172
+ * "1.6" where Swift prints "1.7" (Delta Biz Platinum transit 1.5x @ 1.1¢).
173
+ * toFixed(20) is the exact decimal expansion of the double, so a tie is a
174
+ * tie only when the digits past the tenths are literally 5000…0. */
160
175
  export function fmt1(x) {
161
- const scaled = x * 10;
162
- let r = Math.round(scaled);
163
- if (scaled - Math.floor(scaled) === 0.5 && r % 2 !== 0)
164
- r -= 1;
165
- return (r / 10).toFixed(1);
176
+ const exact = Math.abs(x).toFixed(20);
177
+ const [ip, fp] = exact.split(".");
178
+ if (/^[0-9]50{18}$/.test(fp)) {
179
+ let n = parseInt(ip, 10) * 10 + parseInt(fp[0], 10);
180
+ if (n % 2 !== 0)
181
+ n += 1;
182
+ const s = (n / 10).toFixed(1);
183
+ return x < 0 ? "-" + s : s;
184
+ }
185
+ return x.toFixed(1);
166
186
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rightcard-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "mcpName": "ai.rightcard/mcp",
5
5
  "description": "RightCard MCP — the card-selection oracle for agents. Which of your credit cards to use at any store, from verified data. No bank login.",
6
6
  "type": "module",