earnrate 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/README.md CHANGED
@@ -9,7 +9,7 @@ I wanted a ghost card: one card that always charges the best of my cards. The ca
9
9
 
10
10
  **earnrate is the best-card decision layer for people and AI agents.** Give it a merchant, and it tells you which card in your wallet earns the most there, and why. It's stateful (it knows your caps and chosen categories), valuation-aware, and transparent about its assumptions. It never sees a card number.
11
11
 
12
- - Runs on your machine. No account, no telemetry, no affiliate links. See the [privacy page](https://earnrate.dev/privacy).
12
+ - Runs on your machine, or in your browser at [earnrate.dev](https://earnrate.dev) with nothing to install. No account, no telemetry, no affiliate links. See the [privacy page](https://earnrate.dev/privacy).
13
13
  - US cards and US merchants, for now.
14
14
 
15
15
  > **Status:** early, and it works: the engine, CLI, optional Claude merchant lookup and MCP server. See [the benchmark](docs/benchmark.md) for how it compares to an existing card-picking MCP server.
@@ -51,6 +51,8 @@ Assumes Whole Foods Market codes as supermarkets (MCC 5411).
51
51
 
52
52
  Options: `--online` or `--in-store`, `--valuation flat|points-heavy`, `--all`, `--json`, `--date`, `--country`. Run `earnrate --help` for details. To keep the wallet somewhere else, set `EARNRATE_WALLET` to its path.
53
53
 
54
+ `earnrate cheatsheet` prints the card to use for each everyday kind of purchase this quarter (groceries, dining, gas, Amazon, Costco, travel, streaming, drugstores, everything else), with your caps, picks and activations as they are now. It's also in the My wallet tab on [earnrate.dev](https://earnrate.dev).
55
+
54
56
  ### Your wallet
55
57
 
56
58
  The wallet is a YAML file (`~/.earnrate/wallet.yaml`) listing your cards by ID, which is each card's file name in [`data/cards/`](data/cards). `earnrate add`, `update`, `remove` and `memberships` write it for you and keep your comments, or you can edit it by hand. Only the cards are required:
@@ -98,6 +100,16 @@ While the bonus is open, that card wins wherever it's accepted, and the answer s
98
100
 
99
101
  ### Which new card?
100
102
 
103
+ No wallet needed for these:
104
+
105
+ ```sh
106
+ earnrate best groceries # every card, ranked for one purchase
107
+ earnrate simulate groceries=600 dining=300 other=1000 # best cards and pairs for a year
108
+ earnrate simulate groceries=600 other=1000 --wallet # what a new card would add to yours
109
+ ```
110
+
111
+ `best` ranks every card earnrate knows for a merchant or a category, each set up at its best (the right category pick, activated, cap unused), with its annual fee and what earning the rate takes. Rates that need a membership like Amazon Prime are listed separately unless you pass `--member amazon-prime`. `simulate` spends your monthly amounts for a year, applying caps as they fill and reset, and ranks single cards and two-card pairs by rewards minus annual fees. Both take `--no-fee`, `--cash` or `--points`, `--issuer`, `--business` and `--valuation`. Welcome bonuses are left out of both; for those:
112
+
101
113
  ```sh
102
114
  earnrate offers --cash
103
115
  ```
@@ -142,11 +154,13 @@ Spotted a wrong rate, or want a card added? [Open an issue](https://github.com/c
142
154
 
143
155
  ## Use it from an AI agent (MCP)
144
156
 
145
- `earnrate mcp` runs earnrate as a local MCP server over stdio, with ten tools:
157
+ `earnrate mcp` runs earnrate as a local MCP server over stdio, with thirteen tools:
146
158
 
147
159
  - `best_card_for_purchase`: which card to use, with reasons
160
+ - `best_cards_for` and `simulate_spending`: compare every card earnrate knows, for one purchase or a year of spending, with no wallet needed
148
161
  - `resolve_merchant`: how a merchant was identified
149
162
  - `list_wallet`: your cards, caps left this period, chosen categories and welcome-bonus progress
163
+ - `cheat_sheet`: the card to use for each everyday kind of purchase this quarter
150
164
  - `compare_welcome_offers`: the best public sign-up offers on cards you don't have
151
165
  - `record_purchase`: books a purchase against its cap and any open welcome bonus in your wallet file, keeping your comments
152
166
  - `search_cards`, `add_card`, `update_card`, `remove_card` and `set_memberships`: set up the wallet in conversation
@@ -155,6 +169,8 @@ There's no wallet to write first: tell the assistant which cards you have, and i
155
169
 
156
170
  For a merchant earnrate doesn't know, the calling model can pass a `category` (e.g. `grocery.supermarket`). It's used only for unknown merchants and is labeled in the answer. The command line takes the same thing as `--category`.
157
171
 
172
+ **Claude Desktop, one click:** download [earnrate.mcpb](https://github.com/cwasley/earnrate/releases/latest/download/earnrate.mcpb) and open it; Claude Desktop installs it (Settings → Extensions). No terminal or Node install needed. Its one setting is where your wallet file lives, by default `~/.earnrate/wallet.yaml`, the same file the command line uses.
173
+
158
174
  To add it to Claude Code:
159
175
 
160
176
  ```sh
@@ -185,8 +201,8 @@ merchant text
185
201
 
186
202
  - **Card terms are data, not code.** Each card is a YAML file of rules (category, rate, cap, channel, dates, activation, required membership), validated with Zod. Every rule carries a short quote from the issuer's own page, and a test fails if one is missing. The engine has no card-specific branches.
187
203
  - **Merchants resolve to a category plus the assumption behind it.** "Whole Foods codes as a supermarket (MCC 5411)" is stated in the answer, and venues that can code more than one way (a restaurant inside a stadium or hotel) come back at medium confidence with the alternative spelled out. Unknown merchants fall back to base rates, or to an opt-in, cached and labeled Claude guess.
188
- - **The engine is pure and deterministic.** Given a purchase, a date and a wallet, it checks every rule against network acceptance, the date window, caps already used (split when a purchase straddles one), quarterly picks and activations, then values points at your cents-per-point, including pooling (Freedom Unlimited points are worth Sapphire points when you hold both). Ties, the valuation at which the runner-up would win, and stale data all show up in the answer.
189
- - **Wallet edits keep your file intact.** Recording a purchase rewrites only the numbers that changed, so comments and formatting in your YAML survive.
204
+ - **The engine is pure and deterministic.** Given a purchase, a date and a wallet, it checks every rule against network acceptance, the date window, caps already used (split when a purchase straddles one), quarterly picks and activations, then values points at your cents-per-point, including pooling (Freedom Unlimited points are worth Sapphire points when you hold both). Ties, the valuation at which the runner-up would win, stale data, and a rotating card whose new quarter isn't in the data yet all show up in the answer.
205
+ - **Wallet edits keep your file intact.** Adding or changing a card, or recording a purchase, splices in only the lines that changed and checks the result before saving, so comments and formatting in your YAML survive.
190
206
  - **Drift detection.** A weekly GitHub Action opens each issuer page in a headless browser, checks every quote is still there, and opens an issue when terms move. It stores only hashes of issuer text.
191
207
  - **Tested end to end.** Golden cases written from issuer terms, CLI snapshot tests, an MCP round trip over real stdio, an install test that packs and installs the npm package, and a check that no committed file contains a card number. CI runs all of it on every push.
192
208
 
@@ -201,6 +217,8 @@ Every rule in `data/cards/` carries a short quote from the issuer's own page. In
201
217
 
202
218
  A GitHub Action runs it every Monday and opens a "Card source changes" issue when something moved. After updating a card file, run `yarn check-sources --update` to record the new baseline. Some issuers block headless browsers on shared servers; those pages are listed as "couldn't check", and running the command locally covers them. The first run needs `yarn playwright install chromium`.
203
219
 
220
+ Rotating categories (Freedom Flex) are added each quarter once the issuer posts them. Until then, answers say that quarter's categories are missing rather than guessing.
221
+
204
222
  ## Working on earnrate
205
223
 
206
224
  ```sh
package/data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
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.10.30.2
6
+
7
+ Cards record when the issuer stopped taking applications, so "best card for" lists can leave out cards nobody can get. Categories gain `aliases`, the everyday words people search by ("groceries" finds supermarkets). No rates changed.
8
+
9
+ | Card | Field | Old → new | Source |
10
+ | ---------------- | ------------------- | ---------- | --------------------------------------------------------------------------- |
11
+ | citi-custom-cash | applications_closed | 2026-05-28 | [card page](https://www.citi.com/credit-cards/citi-custom-cash-credit-card) |
12
+
5
13
  ## 2026.10.30.1
6
14
 
7
15
  Freedom Flex's quarterly cap is marked `rotating`, so a quarter whose categories aren't in the data yet gets a warning instead of quietly ranking Flex at its everyday rates. Capital One's issuer id is now spelled the same on every card. No rates changed.
package/data/VERSION CHANGED
@@ -1 +1 @@
1
- 2026.10.30.1
1
+ 2026.10.30.2
@@ -5,9 +5,10 @@ network: mastercard
5
5
  currency: cash
6
6
  last_verified: 2026-10-26
7
7
  verified_by: collin
8
- # Citi stopped taking applications on May 28, 2026; existing cardmembers keep
9
- # the card and its rewards. The card page no longer lists the categories, so
10
- # the definitions come from the card's current ThankYou terms (Sept 2026).
8
+ applications_closed: 2026-05-28
9
+ # Existing cardmembers keep the card and its rewards. The card page no longer
10
+ # lists the categories, so the definitions come from the card's current
11
+ # ThankYou terms (Sept 2026).
11
12
  source_urls:
12
13
  - https://www.citi.com/credit-cards/citi-custom-cash-credit-card
13
14
  - https://www.thankyou.com/ccterms
@@ -5,24 +5,30 @@
5
5
 
6
6
  - id: dining
7
7
  name: Restaurants
8
+ aliases: [dining, restaurant, food, eating out, takeout]
8
9
  mccs: [5812, 5813, 5814]
9
10
  - id: dining.fast_food
10
11
  name: Fast food
12
+ aliases: [fast food restaurants]
11
13
  mccs: [5814]
12
14
 
13
15
  - id: grocery
14
16
  name: Grocery
15
17
  - id: grocery.supermarket
16
18
  name: Supermarkets
19
+ aliases: [groceries, grocery, grocery stores, supermarket]
17
20
  mccs: [5411]
18
21
  - id: grocery.online
19
22
  name: Online grocery and delivery
23
+ aliases: [grocery delivery]
20
24
 
21
25
  - id: superstore
22
26
  name: Superstores
27
+ aliases: [superstore, big box stores]
23
28
  mccs: [5310]
24
29
  - id: warehouse_club
25
30
  name: Warehouse clubs
31
+ aliases: [warehouse club, wholesale clubs]
26
32
  mccs: [5300]
27
33
 
28
34
  - id: convenience_store
@@ -31,6 +37,7 @@
31
37
 
32
38
  - id: gas
33
39
  name: Gas stations
40
+ aliases: [gas, fuel, gas station]
34
41
  mccs: [5541, 5542]
35
42
  - id: gas.grocery_store
36
43
  name: Gas at grocery stores
@@ -38,6 +45,7 @@
38
45
  name: Gas at superstores and warehouse clubs
39
46
  - id: ev_charging
40
47
  name: EV charging
48
+ aliases: [ev, charging]
41
49
  mccs: [5552]
42
50
 
43
51
  - id: transit
@@ -45,6 +53,7 @@
45
53
  mccs: [4111, 4112, 4131, 4784, 7523]
46
54
  - id: transit.rideshare
47
55
  name: Rideshare and taxis
56
+ aliases: [rideshare, taxi, taxis]
48
57
  mccs: [4121]
49
58
 
50
59
  - id: travel
@@ -52,41 +61,53 @@
52
61
  mccs: [4722]
53
62
  - id: travel.flights
54
63
  name: Flights
64
+ aliases: [flights, airfare, airlines]
55
65
  mccs: [4511]
56
66
  - id: travel.hotels
57
67
  name: Hotels
68
+ aliases: [hotel]
58
69
  mccs: [7011]
59
70
  - id: travel.car_rental
60
71
  name: Car rental
72
+ aliases: [car rentals, rental cars]
61
73
  mccs: [7512]
62
74
  - id: travel.cruise
63
75
  name: Cruises
76
+ aliases: [cruise]
64
77
  mccs: [4411]
65
78
 
66
79
  - id: streaming
67
80
  name: Streaming services
81
+ aliases: [streaming, subscriptions]
68
82
  mccs: [4899, 5815]
69
83
  - id: tv_internet
70
84
  name: TV and internet service
85
+ aliases: [internet, cable]
71
86
  mccs: [4899]
72
87
  - id: phone
73
88
  name: Cell phone service
89
+ aliases: [phone, cell phone, wireless]
74
90
  mccs: [4814]
75
91
  - id: utilities
76
92
  name: Home utilities
93
+ aliases: [utilities, electric, electricity]
77
94
  mccs: [4900]
78
95
 
79
96
  - id: drugstore
80
97
  name: Drugstores and pharmacies
98
+ aliases: [drugstores, pharmacy, pharmacies]
81
99
  mccs: [5912]
82
100
  - id: movie_theaters
83
101
  name: Movie theaters
102
+ aliases: [movies, movie theater]
84
103
  mccs: [7832]
85
104
  - id: entertainment
86
105
  name: Live entertainment and events
106
+ aliases: [concerts, events, tickets]
87
107
  mccs: [7922, 7941]
88
108
  - id: fitness
89
109
  name: Gyms and fitness
110
+ aliases: [gym, gyms, fitness]
90
111
  mccs: [7997]
91
112
 
92
113
  - id: department_store
@@ -103,6 +124,7 @@
103
124
  mccs: [5941]
104
125
  - id: home_improvement
105
126
  name: Home improvement
127
+ aliases: [hardware stores]
106
128
  mccs: [5200, 5211]
107
129
  - id: furniture
108
130
  name: Furniture stores
@@ -110,6 +132,7 @@
110
132
 
111
133
  - id: online_retail
112
134
  name: Online retail
135
+ aliases: [online shopping, shopping online]
113
136
  - id: office_supply
114
137
  name: Office supply stores
115
138
  - id: shipping
@@ -119,3 +142,4 @@
119
142
 
120
143
  - id: other
121
144
  name: Everything else
145
+ aliases: [everything, everything else, other]
@@ -0,0 +1,81 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { CLI_NAME } from "../brand.js";
3
+ import { walletProblems } from "../data/integrity.js";
4
+ import { loadCatalog, walletPath } from "../data/load.js";
5
+ import { cheatSheet } from "../engine/cheat-sheet.js";
6
+ import { isValuationProfile, withValuationProfile } from "../engine/profiles.js";
7
+ import { displayPath, readWallet, RUNTIME_ERROR, USAGE_ERROR, } from "./run.js";
8
+ export const CHEATSHEET_HELP = `Usage: ${CLI_NAME} cheatsheet [options]
9
+
10
+ The card in your wallet to use for each everyday kind of purchase this
11
+ quarter, with your caps, picks and activations as they are now.
12
+
13
+ Options:
14
+ --valuation <profile> flat or points-heavy, instead of your wallet's values.
15
+ --date <YYYY-MM-DD> The day to check. Default: today.
16
+ --json Print the cheat sheet as JSON.
17
+ -h, --help Show this help.
18
+ `;
19
+ const pct = (n) => `${String(Number(n.toFixed(2)))}%`;
20
+ export function formatCheatSheet(sheet) {
21
+ const labelWidth = Math.max(...sheet.rows.map((r) => r.label.length));
22
+ const cardWidth = Math.max(...sheet.rows.map((r) => (r.cardName ?? 'none of your cards').length));
23
+ const lines = [`Your cheat sheet for ${sheet.quarter}`, ''];
24
+ for (const row of sheet.rows) {
25
+ lines.push(` ${row.label.padEnd(labelWidth)} ${(row.cardName ?? 'none of your cards').padEnd(cardWidth)} ${row.effectiveReturnPct === null ? '' : pct(row.effectiveReturnPct).padStart(5)}`.trimEnd());
26
+ if (row.note)
27
+ lines.push(` ${' '.repeat(labelWidth)} ! ${row.note}`);
28
+ }
29
+ if (sheet.notes.length > 0)
30
+ lines.push('', ...sheet.notes);
31
+ return `${lines.join('\n')}\n`;
32
+ }
33
+ export async function runCheatSheet(argv, io) {
34
+ let parsed;
35
+ try {
36
+ parsed = parseArgs({
37
+ args: argv,
38
+ options: {
39
+ valuation: { type: 'string' },
40
+ date: { type: 'string' },
41
+ json: { type: 'boolean' },
42
+ help: { type: 'boolean', short: 'h' },
43
+ },
44
+ });
45
+ }
46
+ catch (e) {
47
+ io.stderr(`${e instanceof Error ? e.message : String(e)}\n${CHEATSHEET_HELP}`);
48
+ return USAGE_ERROR;
49
+ }
50
+ const { values } = parsed;
51
+ if (values.help) {
52
+ io.stdout(CHEATSHEET_HELP);
53
+ return 0;
54
+ }
55
+ if (values.valuation !== undefined && !isValuationProfile(values.valuation)) {
56
+ io.stderr(`Unknown valuation "${values.valuation}". Use flat or points-heavy.\n`);
57
+ return USAGE_ERROR;
58
+ }
59
+ if (values.date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(values.date)) {
60
+ io.stderr(`"${values.date}" isn't a date. Use YYYY-MM-DD.\n`);
61
+ return USAGE_ERROR;
62
+ }
63
+ const file = walletPath(io.env);
64
+ const loaded = await readWallet(file, io);
65
+ if (!loaded)
66
+ return RUNTIME_ERROR;
67
+ const catalog = await loadCatalog();
68
+ const problems = walletProblems(loaded, catalog);
69
+ if (problems.length > 0) {
70
+ io.stderr(`Your wallet at ${displayPath(file)} has problems:\n${problems.map((p) => ` - ${p}\n`).join('')}`);
71
+ return RUNTIME_ERROR;
72
+ }
73
+ const wallet = values.valuation !== undefined && isValuationProfile(values.valuation)
74
+ ? withValuationProfile(loaded, values.valuation)
75
+ : loaded;
76
+ const sheet = cheatSheet(catalog, wallet, values.date ?? io.today);
77
+ io.stdout(values.json
78
+ ? `${JSON.stringify(sheet, null, 2)}\n`
79
+ : formatCheatSheet(sheet));
80
+ return 0;
81
+ }