rightcard-mcp 0.1.2 → 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/LICENSE +21 -0
- package/dist/data.js +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +29 -7
- package/dist/model.d.ts +15 -1
- package/dist/model.js +40 -3
- package/dist/search.js +5 -1
- package/dist/tools.js +7 -2
- package/dist/valuation.d.ts +9 -2
- package/dist/valuation.js +32 -6
- package/package.json +3 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RightCard
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/data.js
CHANGED
|
@@ -25,7 +25,7 @@ async function getAll(path, fetchImpl) {
|
|
|
25
25
|
}
|
|
26
26
|
export async function loadCatalog(fetchImpl = fetch) {
|
|
27
27
|
const [cardRows, overrideRows, merchantRows] = await Promise.all([
|
|
28
|
-
getAll("cards?select=id,name,issuer,reward_currency,base_rate,standard_multipliers,annual_fee,verify_status&country=eq.US&order=id", fetchImpl),
|
|
28
|
+
getAll("cards?select=id,name,issuer,reward_currency,base_rate,standard_multipliers,annual_fee,verify_status,network&country=eq.US&order=id", fetchImpl),
|
|
29
29
|
getAll("active_overrides?select=*&order=id", fetchImpl),
|
|
30
30
|
getAll("merchants?select=id,display_name,category,sub_key,merchant_type,is_online,aliases&country=eq.US&order=id", fetchImpl),
|
|
31
31
|
]);
|
package/dist/engine.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface RecommendInput {
|
|
|
33
33
|
cardConfigs?: Record<string, Set<SpendCategory>>;
|
|
34
34
|
now: Date;
|
|
35
35
|
strictMerchantMatch?: boolean;
|
|
36
|
+
/** Register network lock (Costco = visa-only). null/undefined = no restriction. */
|
|
37
|
+
acceptedNetworks?: Set<string> | null;
|
|
36
38
|
}
|
|
37
39
|
export declare function formatPercent(v: number): string;
|
|
38
40
|
export declare function formatMultiplier(v: number): string;
|
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 `${
|
|
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 `${
|
|
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 `${
|
|
58
|
+
return `${other} also earns ${rate} here. We picked ${win} for ${why}.`;
|
|
59
59
|
}
|
|
60
60
|
if (premiumRank(winner) > premiumRank(runnerUp)) {
|
|
61
|
-
return `${
|
|
61
|
+
return `${other} also earns ${rate} here. We picked ${win} for its purchase protection.`;
|
|
62
62
|
}
|
|
63
|
-
return `${
|
|
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
|
-
|
|
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;
|
|
@@ -241,9 +248,24 @@ function subcategoryBreakdown(cards, category, overrides, cardConfigs, v, now) {
|
|
|
241
248
|
}
|
|
242
249
|
/** MerchantYieldEngine.recommend — base-rate mode (offers: []) */
|
|
243
250
|
export function recommend(input) {
|
|
244
|
-
const {
|
|
251
|
+
const { category, overrides, now } = input;
|
|
252
|
+
let cards = input.cards;
|
|
245
253
|
if (cards.length === 0)
|
|
246
254
|
return null;
|
|
255
|
+
// Network-acceptance gate (Costco's Visa-only registers). Mirrors the Swift
|
|
256
|
+
// engine: a KNOWN-incompatible card can't win when a compatible (or
|
|
257
|
+
// unknown-network) card exists; if every card is known-incompatible, the
|
|
258
|
+
// full wallet answers (works on the merchant's site) and the warehouse
|
|
259
|
+
// caveat carries the register story. Unknown (null) network never gates.
|
|
260
|
+
const accepted = input.acceptedNetworks;
|
|
261
|
+
if (accepted && accepted.size > 0) {
|
|
262
|
+
const eligible = cards.filter((c) => {
|
|
263
|
+
const net = c.network?.toLowerCase();
|
|
264
|
+
return net == null || accepted.has(net);
|
|
265
|
+
});
|
|
266
|
+
if (eligible.length > 0)
|
|
267
|
+
cards = eligible;
|
|
268
|
+
}
|
|
247
269
|
const v = withPooling(input.valuation, cards);
|
|
248
270
|
const cardConfigs = input.cardConfigs ?? {};
|
|
249
271
|
const strict = input.strictMerchantMatch ?? false;
|
package/dist/model.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface CreditCard {
|
|
|
15
15
|
multipliers: Record<string, MultiplierField>;
|
|
16
16
|
annualFee?: number | null;
|
|
17
17
|
verifyStatus?: string | null;
|
|
18
|
+
/** Payment network (migration 0017): visa|mastercard|amex|discover; null = unknown, never gates. */
|
|
19
|
+
network?: string | null;
|
|
18
20
|
}
|
|
19
21
|
/** Supabase `cards` row → CreditCard (SupabaseCard.toCreditCard). */
|
|
20
22
|
export declare function cardFromRow(row: any): CreditCard;
|
|
@@ -34,7 +36,13 @@ export interface Override {
|
|
|
34
36
|
merchant: string | null;
|
|
35
37
|
}
|
|
36
38
|
export declare function overrideFromRow(r: any): Override;
|
|
37
|
-
/**
|
|
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. */
|
|
38
46
|
export declare function utcDay(now: Date): string;
|
|
39
47
|
export declare function overrideIsActive(o: Override, now: Date): boolean;
|
|
40
48
|
/** SupabaseOverride.isPermanentMerchantBenefit: merchant-scoped, no activation, ends > 2 years out. */
|
|
@@ -51,5 +59,11 @@ export interface Merchant {
|
|
|
51
59
|
export declare function merchantFromRow(r: any): Merchant;
|
|
52
60
|
/** Merchant.exclusionCaveat */
|
|
53
61
|
export declare function exclusionCaveat(m: Merchant): string | null;
|
|
62
|
+
/** Merchant.acceptedNetworksAtRegister — Costco's Visa-only register lock.
|
|
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. */
|
|
67
|
+
export declare function acceptedNetworksAtRegister(m: Merchant): Set<string> | null;
|
|
54
68
|
/** Merchant.isCuratedBrand */
|
|
55
69
|
export declare function isCuratedBrand(m: Merchant): boolean;
|
package/dist/model.js
CHANGED
|
@@ -27,6 +27,7 @@ export function cardFromRow(row) {
|
|
|
27
27
|
multipliers,
|
|
28
28
|
annualFee: row.annual_fee ?? null,
|
|
29
29
|
verifyStatus: row.verify_status ?? null,
|
|
30
|
+
network: row.network ?? null,
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
function fieldFromJSON(v) {
|
|
@@ -85,7 +86,13 @@ export function overrideFromRow(r) {
|
|
|
85
86
|
label: r.label ?? null, merchant: r.merchant ?? null,
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
|
-
/**
|
|
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. */
|
|
89
96
|
export function utcDay(now) {
|
|
90
97
|
return now.toISOString().slice(0, 10);
|
|
91
98
|
}
|
|
@@ -117,12 +124,42 @@ export function exclusionCaveat(m) {
|
|
|
117
124
|
switch (m.merchantType) {
|
|
118
125
|
case "superstore":
|
|
119
126
|
return `${m.displayName} usually codes as a superstore (not a supermarket), so grocery bonuses often don't post — this is your best guaranteed card.`;
|
|
120
|
-
case "warehouse_club":
|
|
121
|
-
|
|
127
|
+
case "warehouse_club": {
|
|
128
|
+
const base = `${m.displayName} codes as a warehouse club, so category bonuses usually don't post — this is your best guaranteed card.`;
|
|
129
|
+
// Costco alone is also NETWORK-locked: warehouse registers take Visa
|
|
130
|
+
// only (Costco.com takes other networks). Mirrors the Swift engine —
|
|
131
|
+
// keep both sides identical or the parity gate fails.
|
|
132
|
+
if (m.displayName.toLowerCase().includes("costco")) {
|
|
133
|
+
return base + " Note: Costco warehouses take Visa only at the register; other networks work on Costco.com.";
|
|
134
|
+
}
|
|
135
|
+
return base;
|
|
136
|
+
}
|
|
122
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
|
+
}
|
|
123
144
|
return null;
|
|
124
145
|
}
|
|
125
146
|
}
|
|
147
|
+
/** Merchant.acceptedNetworksAtRegister — Costco's Visa-only register lock.
|
|
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. */
|
|
152
|
+
export function acceptedNetworksAtRegister(m) {
|
|
153
|
+
const name = m.displayName.toLowerCase();
|
|
154
|
+
if (!name.includes("costco"))
|
|
155
|
+
return null;
|
|
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"))
|
|
160
|
+
return null;
|
|
161
|
+
return new Set(["visa"]);
|
|
162
|
+
}
|
|
126
163
|
/** Merchant.isCuratedBrand */
|
|
127
164
|
export function isCuratedBrand(m) {
|
|
128
165
|
if (m.aliases.length > 0)
|
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
|
-
|
|
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);
|
package/dist/tools.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// and `caveats`; verified rows only in rankings; quarantined cards are NAMED, never ranked.
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { ALL_CATEGORIES, exclusionCaveat, hasAnyReward, multiplier, overrideIsActive, isPermanentMerchantBenefit } from "./model.js";
|
|
5
|
+
import { ALL_CATEGORIES, acceptedNetworksAtRegister, exclusionCaveat, hasAnyReward, multiplier, overrideIsActive, isPermanentMerchantBenefit } from "./model.js";
|
|
6
6
|
import { recommend, formatPercent, formatMultiplier } from "./engine.js";
|
|
7
7
|
import { rankedBrandMatches, searchNormalized } from "./search.js";
|
|
8
8
|
import { makeValuation, centsPerPoint, withPooling, effectiveProgram, PROGRAM_DISPLAY, TRAVEL_CPP } from "./valuation.js";
|
|
@@ -13,6 +13,10 @@ function cardSummary(c) {
|
|
|
13
13
|
return {
|
|
14
14
|
id: c.id, name: c.displayName, issuer: c.issuer, reward_currency: c.rewardCurrency,
|
|
15
15
|
verify_status: c.verifyStatus ?? "unknown", annual_fee: c.annualFee ?? null,
|
|
16
|
+
// Payment network (visa|mastercard|amex|discover) or null = unknown.
|
|
17
|
+
// Stated so the model never guesses it (Costco's registers are Visa-only;
|
|
18
|
+
// a guessed network in an answer is exactly the hallucination this bans).
|
|
19
|
+
network: c.network ?? null,
|
|
16
20
|
};
|
|
17
21
|
}
|
|
18
22
|
function resolveWallet(catalog, ids) {
|
|
@@ -92,7 +96,8 @@ export function buildServer(getCatalog) {
|
|
|
92
96
|
configs[k] = new Set(v);
|
|
93
97
|
const now = on ? new Date(on + "T00:00:00Z") : new Date();
|
|
94
98
|
const r = recommend({ cards, category: cat, subKey, merchantName: merchant ?? null, overrides: catalog.overrides,
|
|
95
|
-
valuation: makeValuation(mode), cardConfigs: configs, now
|
|
99
|
+
valuation: makeValuation(mode), cardConfigs: configs, now,
|
|
100
|
+
acceptedNetworks: dir ? acceptedNetworksAtRegister(dir) : null });
|
|
96
101
|
if (!r)
|
|
97
102
|
return err("No recommendation could be computed.");
|
|
98
103
|
const answer = {
|
package/dist/valuation.d.ts
CHANGED
|
@@ -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,5 +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")
|
|
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. */
|
|
24
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
|
-
|
|
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.
|
|
15
|
-
delta: 1.1, united: 1.
|
|
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,7 +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")
|
|
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. */
|
|
158
175
|
export function fmt1(x) {
|
|
159
|
-
|
|
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);
|
|
160
186
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rightcard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"mcpName": "ai.rightcard/mcp",
|
|
5
|
-
"description": "RightCard MCP
|
|
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",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"bin": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "https://github.com/
|
|
28
|
+
"url": "git+https://github.com/rightcard/rightcard-mcp.git"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|