earnrate 0.2.2 → 0.2.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 +13 -12
- package/data/CHANGELOG.md +35 -0
- package/data/VERSION +1 -1
- package/data/cards/amex-blue-cash-everyday.yaml +5 -4
- package/data/cards/chase-freedom-flex.yaml +1 -1
- package/data/cards/chase-southwest-plus.yaml +4 -0
- package/data/cards/chase-southwest-premier.yaml +4 -0
- package/data/cards/chase-southwest-priority.yaml +4 -0
- package/data/cards/citi-aadvantage-globe.yaml +7 -7
- package/data/cards/citi-costco-anywhere-visa-business.yaml +15 -1
- package/data/cards/citi-costco-anywhere-visa.yaml +15 -1
- package/data/merchants/overrides.yaml +12 -1
- package/data/merchants/seed.yaml +0 -6
- package/dist/cli/cheatsheet.js +1 -1
- package/dist/cli/explore.js +1 -1
- package/dist/cli/import.js +4 -3
- package/dist/cli/run.js +1 -1
- package/dist/data/file-store.js +3 -1
- package/dist/data/integrity.js +10 -0
- package/dist/data/store.js +9 -1
- package/dist/data/wallet-edit.js +8 -2
- package/dist/engine/cheat-sheet.js +44 -2
- package/dist/engine/explore.js +14 -5
- package/dist/engine/memberships.js +45 -0
- package/dist/engine/offers.js +22 -2
- package/dist/engine/recommend.js +91 -31
- package/dist/engine/setup.js +23 -4
- package/dist/engine/simulate.js +32 -10
- package/dist/engine/text.js +51 -0
- package/dist/engine/wallet-change.js +27 -7
- package/dist/engine/wallet-summary.js +133 -3
- package/dist/format/card-text.js +7 -13
- package/dist/format/cards.js +7 -8
- package/dist/format/explore.js +17 -8
- package/dist/format/offers.js +5 -9
- package/dist/format/recommendation.js +11 -5
- package/dist/format/wallet-yaml.js +18 -3
- package/dist/format/wallet.js +21 -18
- package/dist/mcp/context.js +7 -3
- package/dist/mcp/inputs.js +2 -2
- package/dist/mcp/schemas.js +18 -0
- package/dist/mcp/server.js +3 -3
- package/dist/mcp/tools/account.js +55 -7
- package/dist/mcp/tools/answers.js +24 -7
- package/dist/mcp/tools/wallet.js +49 -7
- package/dist/schema/card.js +14 -0
- package/dist/schema/wallet.js +4 -0
- package/package.json +1 -1
- package/wallet.example.yaml +1 -0
package/README.md
CHANGED
|
@@ -65,6 +65,7 @@ Options: `--online` or `--in-store`, `--valuation flat|points-heavy`, `--all`, `
|
|
|
65
65
|
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:
|
|
66
66
|
|
|
67
67
|
```yaml
|
|
68
|
+
version: 1 # the file's format; earnrate writes it
|
|
68
69
|
valuations:
|
|
69
70
|
UR: 1.5 # cents per point; leave out to use earnrate's defaults
|
|
70
71
|
memberships: [amazon-prime] # unlocks Prime Visa's 5%
|
|
@@ -81,7 +82,7 @@ cards:
|
|
|
81
82
|
two-percent: [gas-ev-charging]
|
|
82
83
|
```
|
|
83
84
|
|
|
84
|
-
Rule IDs for `activated`, the choice slots for `chosen` and the membership IDs that unlock bonuses (such as `bofa-rewards-premier` for a BofA Rewards tier) are in each card's file. Currencies and their default values are in [`data/currencies.yaml`](data/currencies.yaml). Spending caps fill up as you record purchases from an AI assistant or import a statement with `--apply`, and reset with each period.
|
|
85
|
+
Rule IDs for `activated`, the choice slots for `chosen` and the membership IDs that unlock bonuses (such as `bofa-rewards-premier` for a BofA Rewards tier) are in each card's file. Add `costco` if you have a Costco membership: Costco Anywhere needs one, and spending answers count its $65 a year unless you have it, hold the card or spend at Costco. Currencies and their default values are in [`data/currencies.yaml`](data/currencies.yaml). Spending caps fill up as you record purchases from an AI assistant or import a statement with `--apply`, and reset with each period.
|
|
85
86
|
|
|
86
87
|
### Merchants earnrate doesn't know
|
|
87
88
|
|
|
@@ -107,7 +108,7 @@ Or tell your AI assistant ("I opened my Sapphire Preferred on October 1"), or ch
|
|
|
107
108
|
bonus: { opened: 2026-10-01, spend: 5000, by: 2026-12-31, points: 75000 }
|
|
108
109
|
```
|
|
109
110
|
|
|
110
|
-
While the bonus is open, that card wins wherever it's accepted, and the answer says how much is left, the weekly pace to finish on time, what the bonus is worth at your point values, and what using it costs compared with your best earner. Spend counts up as you record purchases
|
|
111
|
+
While the bonus is open and you're behind an even pace to the deadline, that card wins wherever it's accepted, and the answer says how much is left, the weekly pace to finish on time, what the bonus is worth at your point values, and what using it costs compared with your best earner. On pace, it's the pick only where it earns as much as your best card, and it's mentioned where it comes within a point; otherwise your best card wins. Spend counts up as you record purchases from an AI assistant or import a statement with `--apply`; refunds count down. Once it's met or the deadline passes, the card goes back to the normal ranking. With two open bonuses, one behind pace comes first, then the one needing more spend per day.
|
|
111
112
|
|
|
112
113
|
`spend`, `by` and the reward (`points`, or `dollars` for cash back) default to the card's published offer once earnrate has one; set them when your offer was different.
|
|
113
114
|
|
|
@@ -121,13 +122,13 @@ earnrate simulate groceries=600 dining=300 other=1000 # best cards and pairs for
|
|
|
121
122
|
earnrate simulate groceries=600 other=1000 --wallet # what a new card would add to yours
|
|
122
123
|
```
|
|
123
124
|
|
|
124
|
-
`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
|
|
125
|
+
`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`, and a card that needs a membership to get (Costco Anywhere) says so. `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, counting the cost of a membership a card needs (Costco's, for Costco Anywhere) unless you pass `--member costco` or spend at Costco. Both take `--no-fee`, `--cash` or `--points`, `--issuer`, `--business` and `--valuation`. Welcome bonuses are left out of both; for those:
|
|
125
126
|
|
|
126
127
|
```sh
|
|
127
128
|
earnrate offers --cash
|
|
128
129
|
```
|
|
129
130
|
|
|
130
|
-
Ranks the welcome offers on cards you don't have by first-year value: the bonus at your point values minus the first year's annual fee. `--points`, `--no-fee` and `--business` narrow it down. Each offer is the public one on the issuer's page, checked weekly like the earning rules, with the issuer's eligibility rules in its note. Offers can differ by person and by how you apply, and it covers only the cards earnrate knows. There are no affiliate or apply links.
|
|
131
|
+
Ranks the welcome offers on cards you don't have by first-year value: the bonus at your point values minus the first year's annual fee (and a membership the card needs, if you don't have it). An offer the issuer rules out because of a card in your wallet is marked "likely not eligible" and listed last. `--points`, `--no-fee` and `--business` narrow it down. Each offer is the public one on the issuer's page, checked weekly like the earning rules, with the issuer's eligibility rules in its note. Offers can differ by person and by how you apply, and it covers only the cards earnrate knows. There are no affiliate or apply links.
|
|
131
132
|
|
|
132
133
|
### Import a statement
|
|
133
134
|
|
|
@@ -140,8 +141,8 @@ earnrate import gold.csv:amex-gold sapphire.csv:chase-sapphire-preferred
|
|
|
140
141
|
|
|
141
142
|
It totals what each card earned against the best card in your wallet for each purchase, lists the merchants where another card would have earned the most, and flags merchants your issuer codes differently than earnrate assumes. Name several statements, each with its card, to get one report across them. It reads Amex and Chase activity CSVs. Download the CSV rather than a PDF statement: Chase's PDFs print the full card number in the payment coupon, and earnrate never handles card numbers.
|
|
142
143
|
|
|
143
|
-
- It's a dry run unless you pass `--apply`, which records cap usage for the current period in your wallet. Refunds lower
|
|
144
|
-
- Only the date, description, amount, category and (for Chase) transaction type columns are read. The statements themselves are never stored: `--apply` writes cap totals plus a hash of each recorded purchase to `~/.earnrate/imported.json`, so importing the same file twice never counts twice.
|
|
144
|
+
- It's a dry run unless you pass `--apply`, which records cap usage for the current period and spending toward an open welcome bonus in your wallet. Refunds lower both.
|
|
145
|
+
- Only the date, description, amount, category and (for Chase) transaction type columns are read. The statements themselves are never stored: `--apply` writes cap and bonus totals plus a hash of each recorded purchase to `~/.earnrate/imported.json`, so importing the same file twice never counts twice.
|
|
145
146
|
- For a merchant earnrate doesn't know, it uses the statement's category and marks the row, since another issuer may code it differently.
|
|
146
147
|
- `--valuation points-heavy` values points at transfer rates instead of your wallet's values, which can change which card wins.
|
|
147
148
|
|
|
@@ -167,7 +168,7 @@ Spotted a wrong rate, or want a card added? [Open an issue](https://github.com/c
|
|
|
167
168
|
|
|
168
169
|
## Use it in an AI assistant (MCP)
|
|
169
170
|
|
|
170
|
-
**In Claude, nothing to install:** add a custom connector (Settings → Connectors) with the address `https://mcp.earnrate.dev`. It works in Claude on the web, desktop and phone.
|
|
171
|
+
**In Claude, nothing to install:** add a custom connector (Settings → Connectors) with the address `https://mcp.earnrate.dev`. It works in Claude on the web, desktop and phone. When you add it, an earnrate page opens once, where you start an anonymous wallet: no email, just a recovery code you keep. Then ask anything: with no cards saved it answers from every card. Name your cards for answers about yours, and save them to track caps and welcome bonuses across your Claude apps. From the chat you can export the wallet for the command line, switch to another one, or delete it. In Claude Code:
|
|
171
172
|
|
|
172
173
|
```sh
|
|
173
174
|
claude mcp add --transport http earnrate https://mcp.earnrate.dev
|
|
@@ -175,7 +176,7 @@ claude mcp add --transport http earnrate https://mcp.earnrate.dev
|
|
|
175
176
|
|
|
176
177
|
Or install it as a Claude Code plugin, which connects Claude Code and Cowork to the same server: `/plugin marketplace add cwasley/earnrate`, then `/plugin install earnrate@earnrate`.
|
|
177
178
|
|
|
178
|
-
**Or run it on your computer:** `earnrate mcp` runs earnrate as a local MCP server over stdio, with 13 tools (the connector has the same ones, plus `export_wallet`, `import_wallet` and `
|
|
179
|
+
**Or run it on your computer:** `earnrate mcp` runs earnrate as a local MCP server over stdio, with 13 tools (the connector has the same ones, plus `export_wallet`, `import_wallet`, `delete_wallet` and `switch_wallet` for its saved wallets):
|
|
179
180
|
|
|
180
181
|
- `best_card_for_purchase`: which card to use, with reasons
|
|
181
182
|
- `best_cards_for` and `simulate_spending`: compare every card earnrate knows, for one purchase or a year of spending, with no wallet needed
|
|
@@ -224,7 +225,7 @@ merchant text
|
|
|
224
225
|
- **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.
|
|
225
226
|
- **The engine is pure and deterministic.** Given a purchase, a date and a set of cards (yours, or every card), 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.
|
|
226
227
|
- **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.
|
|
227
|
-
- **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.
|
|
228
|
+
- **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. A daily one checks earnrate.dev and the hosted server end to end (sign in, ask, save a card, delete the wallet) and opens an issue if anything fails.
|
|
228
229
|
- **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.
|
|
229
230
|
|
|
230
231
|
The code is TypeScript run directly by Node 24 (`src/`), with the engine in `src/engine/`, merchant resolution in `src/merchants/`, and the CLI and MCP server as thin layers over them. `site/` is earnrate.dev, which runs the same engine in the browser, and `remote/` is the hosted server at mcp.earnrate.dev.
|
|
@@ -242,14 +243,14 @@ Rotating categories (Freedom Flex) are added each quarter once the issuer posts
|
|
|
242
243
|
|
|
243
244
|
## Privacy Policy
|
|
244
245
|
|
|
245
|
-
earnrate needs no account. The local version has no telemetry; the hosted server counts requests by tool name only. The full policy is at [earnrate.dev/privacy](https://earnrate.dev/privacy); in short:
|
|
246
|
+
earnrate needs no account. The local version has no telemetry; the hosted server counts requests by method and tool name only. The full policy is at [earnrate.dev/privacy](https://earnrate.dev/privacy); in short:
|
|
246
247
|
|
|
247
|
-
- **The Claude connector:** earnrate's server at mcp.earnrate.dev receives each question Claude asks it (a store or kind of purchase, and any amount, spending or cards you gave) and answers without keeping it; request logging is off. It counts requests by tool name only.
|
|
248
|
+
- **The Claude connector:** earnrate's server at mcp.earnrate.dev receives each question Claude asks it (a store or kind of purchase, and any amount, spending or cards you gave) and answers without keeping it; request logging is off. It counts requests by method and tool name only. Connecting it starts an anonymous wallet (a recovery code, no email). It holds cards only if you save some: card IDs and the details you gave, never card numbers, and is encrypted with its own key that only your recovery code or your connected apps can unlock. Delete it any time; it's deleted automatically after 13 months unused, or a week if it's never used.
|
|
248
249
|
- **What it stores locally:** your wallet, a YAML file on your computer (`~/.earnrate/wallet.yaml` unless you set `EARNRATE_WALLET`). It lists cards by product ID, like `amex-gold`, plus your category picks, activations and how much you've spent toward caps. earnrate never asks for or stores card numbers, expiry dates, security codes or bank logins.
|
|
249
250
|
- **What the local version sends:** nothing, by default. The optional merchant lookup (off unless you turn it on with your own Anthropic API key) sends only the name of a store earnrate doesn't recognize to Anthropic's API, and saves the answer in `~/.earnrate/merchant-cache.json`.
|
|
250
251
|
- **AI assistants:** when you use earnrate through Claude or another assistant, that assistant's provider sees the conversation, including earnrate's answers, under its own privacy policy.
|
|
251
252
|
- **earnrate.dev:** everything you enter on the site stays in your browser. The site counts visits with Cloudflare Web Analytics, without cookies.
|
|
252
|
-
- **Retention:** locally, your data lives only in files you control, for as long as you keep them. Delete the wallet file, or clear the site's data in your browser, to remove it. A saved wallet on mcp.earnrate.dev lasts until you delete it or leave it unused for 13 months.
|
|
253
|
+
- **Retention:** locally, your data lives only in files you control, for as long as you keep them. Delete the wallet file, or clear the site's data in your browser, to remove it. A saved wallet on mcp.earnrate.dev lasts until you delete it or leave it unused for 13 months (a week, if it's never used); the record that an app is connected goes after 90 days unused.
|
|
253
254
|
- **Terms:** [earnrate.dev/terms](https://earnrate.dev/terms).
|
|
254
255
|
- **Contact:** [hello@earnrate.dev](mailto:hello@earnrate.dev).
|
|
255
256
|
|
package/data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
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.01.3
|
|
6
|
+
|
|
7
|
+
Welcome offers can name other cards whose holders the issuer rules out (`welcome_offer.not_with`), so offer comparisons mark them and list them last for a wallet holding one. No rates changed.
|
|
8
|
+
|
|
9
|
+
| Card | Field | Old → new | Source |
|
|
10
|
+
| ----------------------------------------------------------------------- | ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
11
|
+
| amex-blue-cash-everyday | welcome_offer.not_with | amex-blue-cash-preferred | [offer terms](https://www.americanexpress.com/en-us/credit-cards/apply/terms/personal/blue-cash-everyday-credit-card/25330-10-0) |
|
|
12
|
+
| chase-southwest-plus, chase-southwest-priority, chase-southwest-premier | welcome_offer.not_with | the other two personal Southwest cards | [Plus](https://creditcards.chase.com/travel-credit-cards/southwest/plus), [Priority](https://creditcards.chase.com/travel-credit-cards/southwest/priority), [Premier](https://creditcards.chase.com/travel-credit-cards/southwest/premier) |
|
|
13
|
+
| amex-blue-cash-everyday | welcome_offer evidence | card page headline now "As High As $200"; same offer | [card page](https://www.americanexpress.com/us/credit-cards/card/blue-cash-everyday/) |
|
|
14
|
+
|
|
15
|
+
## 2026.11.01.2
|
|
16
|
+
|
|
17
|
+
Chipotle may post as fast food (MCC 5814) instead of a restaurant, so answers there say which card wins either way. No rates changed.
|
|
18
|
+
|
|
19
|
+
| Merchant | Field | Old → new | Source |
|
|
20
|
+
| -------- | --------- | --------------------- | ------ |
|
|
21
|
+
| chipotle | ambiguity | may code as fast food | n/a |
|
|
22
|
+
|
|
23
|
+
## 2026.11.01.1
|
|
24
|
+
|
|
25
|
+
Freedom Flex's Q4 dining note quotes Chase's "5% total" and says where the 7% figure comes from. No rates changed.
|
|
26
|
+
|
|
27
|
+
| Card | Field | Old → new | Source |
|
|
28
|
+
| ------------------ | ------------------- | -------------------- | ------------------------------------------------------------------------------ |
|
|
29
|
+
| chase-freedom-flex | q4-2026-dining.note | quotes Chase's terms | [card page](https://creditcards.chase.com/cash-back-credit-cards/freedom/flex) |
|
|
30
|
+
|
|
31
|
+
## 2026.11.01
|
|
32
|
+
|
|
33
|
+
Cards can name a paid membership they need (`requires_membership`). Costco Anywhere needs a Costco membership, $65 a year for Gold Star; spending answers count it unless the person has one or shops at Costco. The AAdvantage Globe card page reworded its earning lines; no rates changed.
|
|
34
|
+
|
|
35
|
+
| Card | Field | Old → new | Source |
|
|
36
|
+
| ------------------------------------------------------------- | ------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
37
|
+
| citi-costco-anywhere-visa, citi-costco-anywhere-visa-business | requires_membership | Costco membership, $65 a year | [Citi card page](https://www.citi.com/credit-cards/citi-costco-anywhere-visa-credit-card), [Costco membership page](https://www.costco.com/join-costco.html) |
|
|
38
|
+
| citi-aadvantage-globe | evidence | quotes follow Citi's new wording (same rates) | [card page](https://www.citi.com/credit-cards/citi-aadvantage-globe-mastercard) |
|
|
39
|
+
|
|
5
40
|
## 2026.10.30.3
|
|
6
41
|
|
|
7
42
|
More everyday words find categories, for questions asked without a store name ("coffee shop", "dinner", "train"). Each maps to the category those purchases already fall under. No rates changed.
|
package/data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026.
|
|
1
|
+
2026.11.01.3
|
|
@@ -15,7 +15,8 @@ welcome_offer:
|
|
|
15
15
|
spend: 2000
|
|
16
16
|
months: 6
|
|
17
17
|
varies: true
|
|
18
|
-
|
|
18
|
+
not_with: [amex-blue-cash-preferred]
|
|
19
|
+
note: 'Paid as Reward Dollars, redeemable for a statement credit or at Amazon.com checkout. The card page and offer terms say "as high as $200", with the exact amount shown before you accept; the card page has also shown "Earn $200". Not available if you have or have had this Card, the Cash Magnet Card, the Blue Cash Preferred Card (including the Morgan Stanley version) or previous versions of these Cards.'
|
|
19
20
|
rules:
|
|
20
21
|
- id: us-supermarkets
|
|
21
22
|
match: { categories: [grocery.supermarket] }
|
|
@@ -76,14 +77,14 @@ evidence:
|
|
|
76
77
|
- rule: annual_fee
|
|
77
78
|
url: https://www.americanexpress.com/us/credit-cards/card/blue-cash-everyday/
|
|
78
79
|
quote: 'No Annual Fee'
|
|
79
|
-
- rule: welcome_offer
|
|
80
|
-
url: https://www.americanexpress.com/us/credit-cards/card/blue-cash-everyday/
|
|
81
|
-
quote: 'Earn $200 Cash Back'
|
|
82
80
|
- rule: welcome_offer
|
|
83
81
|
url: https://www.americanexpress.com/us/credit-cards/card/blue-cash-everyday/
|
|
84
82
|
quote: 'after you spend $2,000 in purchases on your new Card in the first 6 months of Card Membership'
|
|
85
83
|
- rule: welcome_offer
|
|
86
84
|
url: https://www.americanexpress.com/en-us/credit-cards/apply/terms/personal/blue-cash-everyday-credit-card/25330-10-0
|
|
87
85
|
quote: 'As High as $200 Cash Back Welcome Offer'
|
|
86
|
+
- rule: not_with
|
|
87
|
+
url: https://www.americanexpress.com/en-us/credit-cards/apply/terms/personal/blue-cash-everyday-credit-card/25330-10-0
|
|
88
|
+
quote: 'if you have or have had this Card, the Cash Magnet® Card, the Blue Cash Preferred® Card'
|
|
88
89
|
todo:
|
|
89
90
|
- 'Amazon is filed as online retail, so it earns 3% here; superstore sites (walmart.com, target.com) and online drugstores may too, but the terms define the category by merchant code and type of goods, so they are not modeled.'
|
|
@@ -66,7 +66,7 @@ rules:
|
|
|
66
66
|
requires: { activation: true }
|
|
67
67
|
valid_from: 2026-10-01
|
|
68
68
|
valid_to: 2026-12-31
|
|
69
|
-
note: Chase
|
|
69
|
+
note: Chase's terms say activated categories earn "5% Cash Back rewards total" (4% on top of the 1% base), so earnrate shows 5% until the $1,500 quarterly cap. Some blogs report 7% for dining (the 3% dining rate plus the 4% bonus); Chase's own terms don't say that.
|
|
70
70
|
- id: q4-2026-american-red-cross
|
|
71
71
|
match: { merchants: [american-red-cross] }
|
|
72
72
|
rate: 5
|
|
@@ -14,6 +14,7 @@ welcome_offer:
|
|
|
14
14
|
points: 80000
|
|
15
15
|
spend: 1000
|
|
16
16
|
months: 3
|
|
17
|
+
not_with: [chase-southwest-priority, chase-southwest-premier]
|
|
17
18
|
note: 'Chase labels it a limited-time offer and states no end date. Not available to current cardmembers of any Southwest Rapid Rewards Credit Card, or previous cardmembers of any Southwest Rapid Rewards Credit Card who received a new Cardmember bonus within the last 24 months (this does not apply to business cards).'
|
|
18
19
|
caps:
|
|
19
20
|
- id: gas-and-grocery
|
|
@@ -70,3 +71,6 @@ evidence:
|
|
|
70
71
|
- rule: lyft
|
|
71
72
|
url: https://creditcards.chase.com/travel-credit-cards/southwest/plus
|
|
72
73
|
quote: 'earn 2X Rapid Rewards points on qualifying Lyft products and services purchased through the Lyft mobile application through 12/31/2027'
|
|
74
|
+
- rule: not_with
|
|
75
|
+
url: https://creditcards.chase.com/travel-credit-cards/southwest/plus
|
|
76
|
+
quote: 'not available to either (i) current Cardmembers of any Southwest Rapid Rewards® Credit Card'
|
|
@@ -14,6 +14,7 @@ welcome_offer:
|
|
|
14
14
|
points: 85000
|
|
15
15
|
spend: 2000
|
|
16
16
|
months: 3
|
|
17
|
+
not_with: [chase-southwest-plus, chase-southwest-priority]
|
|
17
18
|
note: 'Chase labels it a limited-time offer and states no end date. Not available to current cardmembers of any Southwest Rapid Rewards Credit Card, or previous cardmembers of any Southwest Rapid Rewards Credit Card who received a new Cardmember bonus within the last 24 months (this does not apply to business cards).'
|
|
18
19
|
caps:
|
|
19
20
|
- id: grocery-and-dining
|
|
@@ -70,3 +71,6 @@ evidence:
|
|
|
70
71
|
- rule: lyft
|
|
71
72
|
url: https://creditcards.chase.com/travel-credit-cards/southwest/premier
|
|
72
73
|
quote: 'earn 3X Rapid Rewards points on qualifying Lyft products and services purchased through the Lyft mobile application through 12/31/2027'
|
|
74
|
+
- rule: not_with
|
|
75
|
+
url: https://creditcards.chase.com/travel-credit-cards/southwest/premier
|
|
76
|
+
quote: 'not available to either (i) current Cardmembers of any Southwest Rapid Rewards® Credit Card'
|
|
@@ -13,6 +13,7 @@ welcome_offer:
|
|
|
13
13
|
points: 90000
|
|
14
14
|
spend: 3000
|
|
15
15
|
months: 3
|
|
16
|
+
not_with: [chase-southwest-plus, chase-southwest-premier]
|
|
16
17
|
note: 'Chase labels it a limited-time offer and states no end date. Not available to current cardmembers of any Southwest Rapid Rewards Credit Card, or previous cardmembers of any Southwest Rapid Rewards Credit Card who received a new Cardmember bonus within the last 24 months (this does not apply to business cards).'
|
|
17
18
|
rules:
|
|
18
19
|
- id: southwest
|
|
@@ -56,3 +57,6 @@ evidence:
|
|
|
56
57
|
- rule: lyft
|
|
57
58
|
url: https://creditcards.chase.com/travel-credit-cards/southwest/priority
|
|
58
59
|
quote: 'earn 5X Rapid Rewards points on qualifying Lyft products and services purchased through the Lyft mobile application through 12/31/2027'
|
|
60
|
+
- rule: not_with
|
|
61
|
+
url: https://creditcards.chase.com/travel-credit-cards/southwest/priority
|
|
62
|
+
quote: 'not available to either (i) current Cardmembers of any Southwest Rapid Rewards® Credit Card'
|
|
@@ -3,8 +3,8 @@ name: Citi / AAdvantage Globe Mastercard
|
|
|
3
3
|
issuer: citi
|
|
4
4
|
network: mastercard
|
|
5
5
|
currency: aadvantage-miles
|
|
6
|
-
last_verified: 2026-
|
|
7
|
-
verified_by:
|
|
6
|
+
last_verified: 2026-11-01
|
|
7
|
+
verified_by: claude
|
|
8
8
|
source_urls:
|
|
9
9
|
- https://www.citi.com/credit-cards/citi-aadvantage-globe-mastercard
|
|
10
10
|
base_rate: 1
|
|
@@ -35,22 +35,22 @@ rules:
|
|
|
35
35
|
evidence:
|
|
36
36
|
- rule: base_rate
|
|
37
37
|
url: https://www.citi.com/credit-cards/citi-aadvantage-globe-mastercard
|
|
38
|
-
quote: 'Earn 1 AAdvantage® mile for every $1 spent on all other purchases
|
|
38
|
+
quote: 'Earn 1 AAdvantage® mile for every $1 spent on all other purchases'
|
|
39
39
|
- rule: aadvantage-hotels
|
|
40
40
|
url: https://www.citi.com/credit-cards/citi-aadvantage-globe-mastercard
|
|
41
|
-
quote: 'Earn 6 AAdvantage® miles
|
|
41
|
+
quote: 'Earn 6 AAdvantage® miles for every $1 spent on eligible AAdvantage Hotels™ bookings'
|
|
42
42
|
- rule: american-airlines
|
|
43
43
|
url: https://www.citi.com/credit-cards/citi-aadvantage-globe-mastercard
|
|
44
|
-
quote: '3 AAdvantage® miles
|
|
44
|
+
quote: '3 AAdvantage® miles for every $1 spent on eligible American Airlines purchases'
|
|
45
45
|
- rule: american-airlines
|
|
46
46
|
url: https://www.citi.com/credit-cards/citi-aadvantage-globe-mastercard
|
|
47
47
|
quote: 'Eligible American Airlines purchases are items billed by American Airlines as the merchant of record.'
|
|
48
48
|
- rule: restaurants
|
|
49
49
|
url: https://www.citi.com/credit-cards/citi-aadvantage-globe-mastercard
|
|
50
|
-
quote: 'Earn 2 AAdvantage® miles for every $1 spent at restaurants, including takeout and delivery
|
|
50
|
+
quote: 'Earn 2 AAdvantage® miles for every $1 spent at restaurants, including takeout and delivery'
|
|
51
51
|
- rule: rides-and-rails
|
|
52
52
|
url: https://www.citi.com/credit-cards/citi-aadvantage-globe-mastercard
|
|
53
|
-
quote: 'Earn 2 AAdvantage® miles for every $1 spent on eligible Rides and Rails™ purchases, including taxis, rideshares and public transit
|
|
53
|
+
quote: 'Earn 2 AAdvantage® miles for every $1 spent on eligible Rides and Rails™ purchases, including taxis, rideshares and public transit'
|
|
54
54
|
- rule: rides-and-rails
|
|
55
55
|
url: https://www.citi.com/credit-cards/citi-aadvantage-globe-mastercard
|
|
56
56
|
quote: 'passenger railways, bridge and road tolls, parking lots'
|
|
@@ -7,11 +7,19 @@ last_verified: 2026-10-31
|
|
|
7
7
|
verified_by: claude
|
|
8
8
|
source_urls:
|
|
9
9
|
- https://www.citi.com/credit-cards/costco-anywhere-visa-business-card
|
|
10
|
+
- https://www.costco.com/join-costco.html
|
|
10
11
|
# Paid once a year as a Costco reward certificate after the February statement,
|
|
11
12
|
# redeemable for cash or merchandise at a U.S. Costco warehouse.
|
|
12
13
|
base_rate: 1
|
|
13
14
|
business: true
|
|
14
15
|
annual_fee: 0
|
|
16
|
+
# Gold Star, the basic tier. Executive ($130) pays some of its cost back as
|
|
17
|
+
# Costco's own 2% reward, which earnrate doesn't model.
|
|
18
|
+
requires_membership:
|
|
19
|
+
id: costco
|
|
20
|
+
name: a Costco membership
|
|
21
|
+
annual_fee: 65
|
|
22
|
+
members_shop_at: [costco, costco-gas, costco-travel]
|
|
15
23
|
caps:
|
|
16
24
|
# Costco gas, other gas and EV charging share one $7,000 cap. Rewards run on
|
|
17
25
|
# January–December billing statements.
|
|
@@ -47,7 +55,7 @@ rules:
|
|
|
47
55
|
rate: 2
|
|
48
56
|
note: All other purchases at Costco warehouses and Costco.com.
|
|
49
57
|
todo:
|
|
50
|
-
- 'The
|
|
58
|
+
- 'The card page shows no welcome offer.'
|
|
51
59
|
evidence:
|
|
52
60
|
- rule: base_rate
|
|
53
61
|
url: https://www.citi.com/credit-cards/costco-anywhere-visa-business-card
|
|
@@ -85,3 +93,9 @@ evidence:
|
|
|
85
93
|
- rule: annual_fee
|
|
86
94
|
url: https://www.citi.com/credit-cards/costco-anywhere-visa-business-card
|
|
87
95
|
quote: '$0 annual fee with active Costco membership.'
|
|
96
|
+
- rule: requires_membership
|
|
97
|
+
url: https://www.citi.com/credit-cards/costco-anywhere-visa-business-card
|
|
98
|
+
quote: 'No annual fee with paid Costco membership.'
|
|
99
|
+
- rule: requires_membership
|
|
100
|
+
url: https://www.costco.com/join-costco.html
|
|
101
|
+
quote: 'Gold Star $65/yr'
|
|
@@ -7,10 +7,18 @@ last_verified: 2026-10-26
|
|
|
7
7
|
verified_by: collin
|
|
8
8
|
source_urls:
|
|
9
9
|
- https://www.citi.com/credit-cards/citi-costco-anywhere-visa-credit-card
|
|
10
|
+
- https://www.costco.com/join-costco.html
|
|
10
11
|
# Paid once a year as a Costco reward certificate after the February statement,
|
|
11
12
|
# redeemable for cash or merchandise at a U.S. Costco warehouse.
|
|
12
13
|
base_rate: 1
|
|
13
14
|
annual_fee: 0
|
|
15
|
+
# Gold Star, the basic tier. Executive ($130) pays some of its cost back as
|
|
16
|
+
# Costco's own 2% reward, which earnrate doesn't model.
|
|
17
|
+
requires_membership:
|
|
18
|
+
id: costco
|
|
19
|
+
name: a Costco membership
|
|
20
|
+
annual_fee: 65
|
|
21
|
+
members_shop_at: [costco, costco-gas, costco-travel]
|
|
14
22
|
caps:
|
|
15
23
|
# Costco gas, other gas and EV charging share one $7,000 cap. Rewards run on
|
|
16
24
|
# January–December billing statements.
|
|
@@ -46,7 +54,7 @@ rules:
|
|
|
46
54
|
rate: 2
|
|
47
55
|
note: All other purchases at Costco warehouses and Costco.com.
|
|
48
56
|
todo:
|
|
49
|
-
- 'The
|
|
57
|
+
- 'The card page shows no welcome offer.'
|
|
50
58
|
evidence:
|
|
51
59
|
- rule: base_rate
|
|
52
60
|
url: https://www.citi.com/credit-cards/citi-costco-anywhere-visa-credit-card
|
|
@@ -84,3 +92,9 @@ evidence:
|
|
|
84
92
|
- rule: annual_fee
|
|
85
93
|
url: https://www.citi.com/credit-cards/citi-costco-anywhere-visa-credit-card
|
|
86
94
|
quote: '$0 annual fee with active Costco membership.'
|
|
95
|
+
- rule: requires_membership
|
|
96
|
+
url: https://www.citi.com/credit-cards/citi-costco-anywhere-visa-credit-card
|
|
97
|
+
quote: 'No annual fee with paid Costco membership.'
|
|
98
|
+
- rule: requires_membership
|
|
99
|
+
url: https://www.costco.com/join-costco.html
|
|
100
|
+
quote: 'Gold Star $65/yr'
|
|
@@ -113,7 +113,10 @@
|
|
|
113
113
|
category: warehouse_club
|
|
114
114
|
in_store:
|
|
115
115
|
accepts: [visa]
|
|
116
|
-
|
|
116
|
+
online:
|
|
117
|
+
accepts: [visa, mastercard]
|
|
118
|
+
# customerservice.costco.com/app/answers/answer_view/a_id/719 (2026-10-31)
|
|
119
|
+
note: Warehouse registers take Visa only for credit; Costco.com takes Visa and Mastercard.
|
|
117
120
|
- id: costco-gas
|
|
118
121
|
name: Costco Gas
|
|
119
122
|
aliases: [costco gas station]
|
|
@@ -157,6 +160,14 @@
|
|
|
157
160
|
may_code_as: [dining, entertainment]
|
|
158
161
|
note: Restaurants inside a hotel may code as dining, or as lodging if charged to the room. Shows and casino charges code separately.
|
|
159
162
|
|
|
163
|
+
- id: chipotle
|
|
164
|
+
name: Chipotle
|
|
165
|
+
aliases: [chipotle mexican grill]
|
|
166
|
+
category: dining
|
|
167
|
+
ambiguity:
|
|
168
|
+
may_code_as: [dining.fast_food]
|
|
169
|
+
note: Fast-casual chains can post as fast food (MCC 5814) instead of a restaurant (5812); a statement shows which.
|
|
170
|
+
|
|
160
171
|
- id: comcast
|
|
161
172
|
name: Comcast Xfinity
|
|
162
173
|
aliases: [comcast, xfinity]
|
package/data/merchants/seed.yaml
CHANGED
package/dist/cli/cheatsheet.js
CHANGED
|
@@ -15,7 +15,7 @@ name with --cards, or else from every card earnrate knows.
|
|
|
15
15
|
|
|
16
16
|
Options:
|
|
17
17
|
--cards <ids> Use these cards instead of your wallet, comma separated.
|
|
18
|
-
--member <name> A membership you have, like amazon-prime. Repeatable.
|
|
18
|
+
--member <name> A membership you have, like amazon-prime or costco. Repeatable.
|
|
19
19
|
--valuation <profile> flat or points-heavy, instead of your wallet's values.
|
|
20
20
|
--date <YYYY-MM-DD> The day to check. Default: today.
|
|
21
21
|
--json Print the cheat sheet as JSON.
|
package/dist/cli/explore.js
CHANGED
|
@@ -12,7 +12,7 @@ const FILTER_OPTIONS = ` --cash, --points Cash back cards only, or point
|
|
|
12
12
|
--issuer <id> One issuer's cards, e.g. chase or amex.
|
|
13
13
|
--business Business cards instead of personal ones.
|
|
14
14
|
--closed Include cards the issuer no longer takes applications for.
|
|
15
|
-
--member <name> A membership you have, e.g. amazon-prime. Repeatable.
|
|
15
|
+
--member <name> A membership you have, e.g. amazon-prime or costco. Repeatable.
|
|
16
16
|
--valuation <profile> flat (points at 1¢, the default) or points-heavy
|
|
17
17
|
(MR and UR at 1.7¢).`;
|
|
18
18
|
const BEST_HELP = `Usage: ${CLI_NAME} best <merchant or category> [amount] [options]
|
package/dist/cli/import.js
CHANGED
|
@@ -22,13 +22,14 @@ Reads card statements (Amex and Chase activity CSVs) and reports:
|
|
|
22
22
|
|
|
23
23
|
Name several statements, each with its card, to see one report across them.
|
|
24
24
|
|
|
25
|
-
Nothing is written unless you pass --apply, and then only cap totals
|
|
26
|
-
fingerprint of each recorded purchase, so
|
|
25
|
+
Nothing is written unless you pass --apply, and then only cap totals,
|
|
26
|
+
welcome-bonus spending and a fingerprint of each recorded purchase, so
|
|
27
|
+
importing twice never counts twice.
|
|
27
28
|
The statements themselves are never stored.
|
|
28
29
|
|
|
29
30
|
Options:
|
|
30
31
|
--card <id> The card a statement is for, as in your wallet.
|
|
31
|
-
--apply Record cap usage
|
|
32
|
+
--apply Record cap usage and welcome-bonus spending.
|
|
32
33
|
--valuation <profile> flat or points-heavy, instead of your wallet's values.
|
|
33
34
|
-h, --help Show this help.
|
|
34
35
|
`;
|
package/dist/cli/run.js
CHANGED
|
@@ -42,7 +42,7 @@ Save your cards (optional):
|
|
|
42
42
|
${CLI_NAME} add <card-id> [--pick ...] Add a card you hold.
|
|
43
43
|
${CLI_NAME} update <card-id> [--pick ...] Change picks, label or activations.
|
|
44
44
|
${CLI_NAME} remove <card-id> Remove a card.
|
|
45
|
-
${CLI_NAME} memberships [names...] Show or set memberships (amazon-prime).
|
|
45
|
+
${CLI_NAME} memberships [names...] Show or set memberships (amazon-prime, costco).
|
|
46
46
|
|
|
47
47
|
Other commands:
|
|
48
48
|
${CLI_NAME} cheatsheet The best card for each category this quarter.
|
package/dist/data/file-store.js
CHANGED
|
@@ -69,7 +69,9 @@ export class FileWalletStore {
|
|
|
69
69
|
return this.change(this.existing((w) => updateInWallet(this.catalog, w, input, today)), (text, change) => updateCardText(text ?? '', input.id, change.fields));
|
|
70
70
|
}
|
|
71
71
|
removeCard(cardId) {
|
|
72
|
-
return this.change(this.existing((w) =>
|
|
72
|
+
return this.change(this.existing((w) => w.cards.length === 1 && w.cards[0]?.id === cardId
|
|
73
|
+
? failed(`${cardId} is the last card in ${this.where}. To start over, delete that file; to swap cards, add the new one first.`)
|
|
74
|
+
: removeFromWallet(w, cardId)), (text) => removeCardText(text ?? '', cardId));
|
|
73
75
|
}
|
|
74
76
|
setMemberships(memberships) {
|
|
75
77
|
return this.change(this.existing((w) => withMemberships(this.catalog, w, memberships)), (text, change) => setMembershipsText(text ?? '', change.wallet.memberships));
|
package/dist/data/integrity.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WALLET_VERSION } from "../schema/wallet.js";
|
|
1
2
|
function duplicates(ids) {
|
|
2
3
|
const seen = new Set();
|
|
3
4
|
const dupes = new Set();
|
|
@@ -56,6 +57,11 @@ function cardProblems(card, catalog, merchantIds) {
|
|
|
56
57
|
if (!currencies.has(card.currency)) {
|
|
57
58
|
problems.push(`${where}: unknown currency '${card.currency}'`);
|
|
58
59
|
}
|
|
60
|
+
for (const id of card.welcome_offer?.not_with ?? [])
|
|
61
|
+
if (!cardIds.has(id))
|
|
62
|
+
problems.push(`${where} welcome_offer.not_with: unknown card '${id}'`);
|
|
63
|
+
for (const m of card.requires_membership?.members_shop_at ?? [])
|
|
64
|
+
checkMerchant(m, 'requires_membership');
|
|
59
65
|
if (card.pools_into) {
|
|
60
66
|
if (!currencies.has(card.pools_into.currency)) {
|
|
61
67
|
problems.push(`${where} pools_into: unknown currency '${card.pools_into.currency}'`);
|
|
@@ -79,6 +85,8 @@ function cardProblems(card, catalog, merchantIds) {
|
|
|
79
85
|
...(card.annual_fee !== undefined ? ['annual_fee'] : []),
|
|
80
86
|
...(card.first_year_fee !== undefined ? ['first_year_fee'] : []),
|
|
81
87
|
...(card.welcome_offer ? ['welcome_offer'] : []),
|
|
88
|
+
...(card.welcome_offer?.not_with ? ['not_with'] : []),
|
|
89
|
+
...(card.requires_membership ? ['requires_membership'] : []),
|
|
82
90
|
...card.rules.map((r) => r.id),
|
|
83
91
|
]);
|
|
84
92
|
const sourcePages = new Set(card.source_urls.map((u) => u.split('#')[0]));
|
|
@@ -188,6 +196,8 @@ export function walletProblems(wallet, catalog) {
|
|
|
188
196
|
const problems = [];
|
|
189
197
|
const cards = new Map(catalog.cards.map((c) => [c.id, c]));
|
|
190
198
|
const currencies = new Map(catalog.currencies.map((c) => [c.id, c]));
|
|
199
|
+
if ((wallet.version ?? 1) > WALLET_VERSION)
|
|
200
|
+
problems.push(`wallet: saved by a newer earnrate (format ${String(wallet.version)}); update earnrate to read it`);
|
|
191
201
|
for (const id of duplicates(wallet.cards.map((c) => c.id))) {
|
|
192
202
|
problems.push(`wallet: card '${id}' is listed twice`);
|
|
193
203
|
}
|
package/dist/data/store.js
CHANGED
|
@@ -30,7 +30,11 @@ export class JsonWalletStore {
|
|
|
30
30
|
const change = apply(before);
|
|
31
31
|
if (!change.ok)
|
|
32
32
|
return failed(change.error);
|
|
33
|
-
|
|
33
|
+
// An empty wallet is stored as none: the schema needs a card.
|
|
34
|
+
if (change.wallet.cards.length === 0)
|
|
35
|
+
await this.backend.remove();
|
|
36
|
+
else
|
|
37
|
+
await this.backend.write(JSON.stringify(change.wallet));
|
|
34
38
|
return {
|
|
35
39
|
ok: true,
|
|
36
40
|
wallet: change.wallet,
|
|
@@ -62,6 +66,10 @@ export class JsonWalletStore {
|
|
|
62
66
|
erase() {
|
|
63
67
|
return this.serialized(() => this.backend.remove());
|
|
64
68
|
}
|
|
69
|
+
// Nothing to sign out of: the backend is the caller's own storage.
|
|
70
|
+
disconnect() {
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
}
|
|
65
73
|
record(purchase, merchant) {
|
|
66
74
|
return this.serialized(async () => {
|
|
67
75
|
const wallet = await this.load();
|
package/dist/data/wallet-edit.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
4
|
import { isMap, isSeq, parseDocument, stringify } from 'yaml';
|
|
5
5
|
import { applyBonusBooking, applyBooking, } from "../engine/record.js";
|
|
6
|
-
import { WalletSchema } from "../schema/wallet.js";
|
|
6
|
+
import { WALLET_VERSION, WalletSchema } from "../schema/wallet.js";
|
|
7
7
|
const NEW_WALLET = `# Your earnrate wallet: the cards you hold, by id. Never card numbers.
|
|
8
8
|
# Format: https://github.com/cwasley/earnrate#your-wallet
|
|
9
9
|
|
|
@@ -57,6 +57,12 @@ function fieldLines(field, f, indent) {
|
|
|
57
57
|
`${indent} spent: ${String(b.spent)}\n`,
|
|
58
58
|
b.spend === undefined ? '' : `${indent} spend: ${String(b.spend)}\n`,
|
|
59
59
|
b.by === undefined ? '' : `${indent} by: ${scalar(b.by)}\n`,
|
|
60
|
+
b.points === undefined
|
|
61
|
+
? ''
|
|
62
|
+
: `${indent} points: ${String(b.points)}\n`,
|
|
63
|
+
b.dollars === undefined
|
|
64
|
+
? ''
|
|
65
|
+
: `${indent} dollars: ${String(b.dollars)}\n`,
|
|
60
66
|
].join('');
|
|
61
67
|
}
|
|
62
68
|
case 'chosen': {
|
|
@@ -100,7 +106,7 @@ const column = (text, at) => at - lineStart(text, at);
|
|
|
100
106
|
// Appends a card after the last one, matching the list's indentation.
|
|
101
107
|
export function addCardText(text, entry) {
|
|
102
108
|
if (text === null || text.trim() === '') {
|
|
103
|
-
return `${NEW_WALLET}
|
|
109
|
+
return `${NEW_WALLET}version: ${String(WALLET_VERSION)}\ncards:\n${cardLines(entry, ' ')}`;
|
|
104
110
|
}
|
|
105
111
|
const { cards } = cardsSeq(text);
|
|
106
112
|
const first = cards.items[0];
|