earnrate 0.1.1 → 0.1.3

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,23 +9,26 @@ 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.
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.
16
16
 
17
17
  ## Try it
18
18
 
19
- Needs Node 24 or later.
19
+ Needs Node 22 or later.
20
20
 
21
21
  ```sh
22
22
  npm install -g earnrate
23
- mkdir -p ~/.earnrate
24
- curl -o ~/.earnrate/wallet.yaml https://raw.githubusercontent.com/cwasley/earnrate/main/wallet.example.yaml
23
+ earnrate cards sapphire # find your cards' ids
24
+ earnrate add chase-sapphire-preferred
25
+ earnrate add amex-gold
25
26
  earnrate whole foods 80
26
27
  ```
27
28
 
28
- Then edit `~/.earnrate/wallet.yaml` to list your own cards (see [Your wallet](#your-wallet)).
29
+ Adding a card tells you anything else it needs to earn correctly, such as the categories you picked on Cash+, Amazon Prime for Prime Visa's 5%, or which rotating categories you've activated. `earnrate update` sets those. Using an AI assistant instead? [Add the MCP server](#use-it-from-an-ai-agent-mcp) and tell it which cards you have.
30
+
31
+ With the [example wallet](wallet.example.yaml), it prints:
29
32
 
30
33
  ```
31
34
  Whole Foods Market · $80
@@ -50,7 +53,7 @@ Options: `--online` or `--in-store`, `--valuation flat|points-heavy`, `--all`, `
50
53
 
51
54
  ### Your wallet
52
55
 
53
- The wallet is a YAML file listing your cards by ID, which is each card's file name in [`data/cards/`](data/cards). Only the cards are required:
56
+ 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:
54
57
 
55
58
  ```yaml
56
59
  valuations:
@@ -95,6 +98,16 @@ While the bonus is open, that card wins wherever it's accepted, and the answer s
95
98
 
96
99
  ### Which new card?
97
100
 
101
+ No wallet needed for these:
102
+
103
+ ```sh
104
+ earnrate best groceries # every card, ranked for one purchase
105
+ earnrate simulate groceries=600 dining=300 other=1000 # best cards and pairs for a year
106
+ earnrate simulate groceries=600 other=1000 --wallet # what a new card would add to yours
107
+ ```
108
+
109
+ `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:
110
+
98
111
  ```sh
99
112
  earnrate offers --cash
100
113
  ```
@@ -139,13 +152,17 @@ Spotted a wrong rate, or want a card added? [Open an issue](https://github.com/c
139
152
 
140
153
  ## Use it from an AI agent (MCP)
141
154
 
142
- `earnrate mcp` runs earnrate as a local MCP server over stdio, with five tools:
155
+ `earnrate mcp` runs earnrate as a local MCP server over stdio, with twelve tools:
143
156
 
144
157
  - `best_card_for_purchase`: which card to use, with reasons
158
+ - `best_cards_for` and `simulate_spending`: compare every card earnrate knows, for one purchase or a year of spending, with no wallet needed
145
159
  - `resolve_merchant`: how a merchant was identified
146
160
  - `list_wallet`: your cards, caps left this period, chosen categories and welcome-bonus progress
147
161
  - `compare_welcome_offers`: the best public sign-up offers on cards you don't have
148
162
  - `record_purchase`: books a purchase against its cap and any open welcome bonus in your wallet file, keeping your comments
163
+ - `search_cards`, `add_card`, `update_card`, `remove_card` and `set_memberships`: set up the wallet in conversation
164
+
165
+ There's no wallet to write first: tell the assistant which cards you have, and it finds them, adds them, and asks about anything a card needs, like this quarter's Cash+ picks.
149
166
 
150
167
  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`.
151
168
 
@@ -179,8 +196,8 @@ merchant text
179
196
 
180
197
  - **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.
181
198
  - **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.
182
- - **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.
183
- - **Wallet edits keep your file intact.** Recording a purchase rewrites only the numbers that changed, so comments and formatting in your YAML survive.
199
+ - **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.
200
+ - **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.
184
201
  - **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.
185
202
  - **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.
186
203
 
@@ -195,6 +212,8 @@ Every rule in `data/cards/` carries a short quote from the issuer's own page. In
195
212
 
196
213
  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`.
197
214
 
215
+ 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.
216
+
198
217
  ## Working on earnrate
199
218
 
200
219
  ```sh
package/data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
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
+
13
+ ## 2026.10.30.1
14
+
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.
16
+
17
+ | Card | Field | Old → new | Source |
18
+ | ------------------------------------------------------------- | --------------------------- | ------------------------ | ------ |
19
+ | chase-freedom-flex | caps.five-percent-quarterly | adds `rotating` | n/a |
20
+ | capital-one-venture-x, capital-one-venture, capital-one-savor | issuer | capitalone → capital-one | n/a |
21
+
5
22
  ## 2026.10.28.2
6
23
 
7
24
  Bank of America Customized Cash Rewards, reviewed by Collin (`verified_by: collin`). Choice options can now be "any online purchase except these categories", and a choice period can be `until_changed` for picks that carry over.
package/data/VERSION CHANGED
@@ -1 +1 @@
1
- 2026.10.28.2
1
+ 2026.10.30.2
@@ -1,6 +1,6 @@
1
1
  id: capital-one-savor
2
2
  name: Capital One Savor Rewards Credit Card
3
- issuer: capitalone
3
+ issuer: capital-one
4
4
  # The card page never names the network in visible text; its embedded
5
5
  # product data says MasterCard.
6
6
  network: mastercard
@@ -1,6 +1,6 @@
1
1
  id: capital-one-venture-x
2
2
  name: Capital One Venture X Rewards Credit Card
3
- issuer: capitalone
3
+ issuer: capital-one
4
4
  network: visa # Visa Infinite, per the card page's footnotes
5
5
  currency: C1
6
6
  last_verified: 2026-10-26
@@ -1,6 +1,6 @@
1
1
  id: capital-one-venture
2
2
  name: Capital One Venture Rewards Credit Card
3
- issuer: capitalone
3
+ issuer: capital-one
4
4
  # The card page never names the network in visible text. Its embedded product
5
5
  # data says MasterCard; older Venture accounts may be on Visa.
6
6
  network: mastercard
@@ -31,6 +31,7 @@ caps:
31
31
  amount: 1500
32
32
  period: quarter
33
33
  then_rate: 1
34
+ rotating: true
34
35
  rules:
35
36
  - id: chase-travel
36
37
  match: { merchants: [chase-travel] }
@@ -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,34 @@
1
+ const NETWORKS = {
2
+ visa: 'Visa',
3
+ mastercard: 'Mastercard',
4
+ amex: 'Amex',
5
+ discover: 'Discover',
6
+ store: 'store card',
7
+ };
8
+ export function formatCardInfo(card) {
9
+ const fee = card.annualFee === null
10
+ ? ''
11
+ : card.annualFee === 0
12
+ ? ', no annual fee'
13
+ : `, $${String(card.annualFee)} a year`;
14
+ const needs = [
15
+ ...card.choices.map((c) => `${c.id}: pick ${String(c.pick)} of ${c.options.join(', ')}${c.default ? ` (default ${c.default.join(', ')})` : ''}`),
16
+ ...(card.memberships.length > 0
17
+ ? [`rates that need a membership: ${card.memberships.join(', ')}`]
18
+ : []),
19
+ ...(card.activation.length > 0
20
+ ? [`activate with the issuer: ${card.activation.join(', ')}`]
21
+ : []),
22
+ ...(card.needsAnniversary ? ['cap resets on the card anniversary'] : []),
23
+ ...(card.onlyAt ? [`works only at ${card.onlyAt.join(', ')}`] : []),
24
+ ];
25
+ return [
26
+ `${card.id}: ${card.name} (${NETWORKS[card.network]}${fee})${card.held ? ' [in wallet]' : ''}`,
27
+ ...needs.map((n) => ` ${n}`),
28
+ ].join('\n');
29
+ }
30
+ export function formatNotes(notes) {
31
+ return notes.length === 0
32
+ ? 'Nothing else to set up for it.'
33
+ : `Still to set:\n${notes.map((n) => `- ${n.text}`).join('\n')}`;
34
+ }