earnrate 0.1.0 → 0.1.1

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
@@ -69,7 +69,7 @@ cards:
69
69
  two-percent: [gas-ev-charging]
70
70
  ```
71
71
 
72
- Rule IDs for `activated` and the choice slots for `chosen` 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 agent or import a statement with `--apply`, and reset with each period.
72
+ 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 agent or import a statement with `--apply`, and reset with each period.
73
73
 
74
74
  ### Merchants earnrate doesn't know
75
75
 
@@ -119,7 +119,7 @@ It totals what each card earned against the best card in your wallet for each pu
119
119
 
120
120
  ## Supported cards
121
121
 
122
- 56 cards, each modeled from the issuer's own terms and checked weekly against them:
122
+ 57 cards, each modeled from the issuer's own terms and checked weekly against them:
123
123
 
124
124
  - **American Express:** Gold, Platinum, Blue Cash Preferred, Blue Cash Everyday, Blue Business Plus
125
125
  - **Chase:** Sapphire Preferred, Sapphire Reserve, Freedom Unlimited, Freedom Flex, Ink Business Cash, Ink Business Preferred, Prime Visa
@@ -127,7 +127,7 @@ It totals what each card earned against the best card in your wallet for each pu
127
127
  - **Citi:** Strata Elite, Strata Premier, Double Cash, Custom Cash, My Best Buy Visa, Costco Anywhere Visa (personal and business); AAdvantage MileUp, Platinum Select, Globe, Executive, Business
128
128
  - **Capital One:** Venture X, Venture, Savor, Kohl's Card, Kohl's Rewards Visa
129
129
  - **Wells Fargo:** Active Cash, Autograph
130
- - **Bank of America:** Atmos Rewards Ascent, Summit, Business
130
+ - **Bank of America:** Atmos Rewards Ascent, Summit, Business; Customized Cash Rewards
131
131
  - **TD Bank:** Target Circle Card, Nordstrom card, Nordstrom Visa
132
132
  - **Others:** U.S. Bank Cash+, Bilt Obsidian, SoFi Unlimited 2%
133
133
 
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.28.2
6
+
7
+ 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.
8
+
9
+ | Card | Field | Old → new | Source |
10
+ | --------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | bank-of-america-customized-cash-rewards | new | 3% in a choice category, 2% at grocery stores and wholesale clubs ($2,500 combined a quarter), 1% otherwise; BofA Rewards tiers add 10%–75% | [card page](https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/), [categories](https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/cash-back-category-choices/), terms, [BofA Rewards](https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/) |
12
+
5
13
  ## 2026.10.28.1
6
14
 
7
15
  Collin reviewed the family-cards and offers packets and kept every value as recorded, including the open calls: no Peloton rule on Sapphire Preferred yet, My Best Buy Visa's fee at the top of Citi's $0–59 range, and Amex "as high as" offers at the public figure. All 56 cards are now `verified_by: collin`.
package/data/VERSION CHANGED
@@ -1 +1 @@
1
- 2026.10.28.1
1
+ 2026.10.28.2
@@ -0,0 +1,210 @@
1
+ id: bank-of-america-customized-cash-rewards
2
+ name: Bank of America Customized Cash Rewards Credit Card
3
+ issuer: bank-of-america
4
+ # Bank of America's terms carry only the Visa and Visa Signature trademark
5
+ # notice, so the network is Visa.
6
+ network: visa
7
+ currency: cash
8
+ last_verified: 2026-10-28
9
+ verified_by: collin
10
+ source_urls:
11
+ - https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/
12
+ - https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/cash-back-category-choices/
13
+ - https://www.bankofamerica.com/credit-cards/terms-and-conditions/?campaignid=4079537&productoffercode=8H&locale=en_US
14
+ - https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
15
+ base_rate: 1
16
+ annual_fee: 0
17
+ welcome_offer:
18
+ dollars: 200
19
+ spend: 1000
20
+ months: 3
21
+ note: Online offer; the window is 90 days from account opening, modeled as 3 months. The offer may not be available elsewhere after leaving the page.
22
+ caps:
23
+ - id: three-and-two-percent-combined
24
+ amount: 2500
25
+ period: quarter
26
+ then_rate: 1
27
+ choices:
28
+ # Gas is the default until changed. It can be changed once per calendar
29
+ # month and otherwise carries over.
30
+ - id: three-percent
31
+ pick: 1
32
+ period: until_changed
33
+ default: [gas-ev-charging]
34
+ options:
35
+ gas-ev-charging: [gas, ev_charging]
36
+ # Bank of America also excludes doctors and hospitals, government
37
+ # services and taxes, insurance, schools and other non-retail services,
38
+ # which have no earnrate category.
39
+ online-shopping:
40
+ { channel: online, exclude: [utilities, shipping, fitness] }
41
+ dining: [dining]
42
+ travel: [travel, transit]
43
+ drug-stores: [drugstore]
44
+ home-improvement-furnishings: [home_improvement, furniture]
45
+ rules:
46
+ - id: three-percent
47
+ match: { choice: three-percent }
48
+ rate: 3
49
+ cap: three-and-two-percent-combined
50
+ note: Gas and EV charging, online shopping, dining, travel, drug stores, or home improvement and furnishings, whichever you picked. Travel includes parking, transit, rideshare and tolls. Online shopping is any purchase paid on a website or app, as reported by the merchant; ordered online but paid in store, in-store mobile wallet and phone or mail orders don't count, and doctors, government, insurance, membership organizations, schools, some utilities and other non-retail services are excluded. The first $2,500 a calendar quarter across the 3% and 2% categories, then 1%.
51
+ - id: two-percent
52
+ match: { categories: [grocery.supermarket, warehouse_club] }
53
+ rate: 2
54
+ cap: three-and-two-percent-combined
55
+ note: Grocery stores, supermarkets, wholesale clubs, and meat and seafood, candy, dairy and bakery stores. Superstores and smaller stores that also sell groceries, like drug stores, are excluded. Shares the $2,500 quarterly cap with the 3% category.
56
+ # BofA Rewards members earn a tier bonus on all cash back: 10% for Member,
57
+ # 25% Preferred Plus, 50% Preferred Honors, 75% Premier.
58
+ - id: three-percent-bofa-rewards-member
59
+ match: { choice: three-percent }
60
+ rate: 3.3
61
+ cap: three-and-two-percent-combined
62
+ requires: { membership: bofa-rewards-member }
63
+ note: 3% plus the 10% BofA Rewards Member bonus.
64
+ - id: two-percent-bofa-rewards-member
65
+ match: { categories: [grocery.supermarket, warehouse_club] }
66
+ rate: 2.2
67
+ cap: three-and-two-percent-combined
68
+ requires: { membership: bofa-rewards-member }
69
+ note: 2% plus the 10% BofA Rewards Member bonus.
70
+ - id: everything-else-bofa-rewards-member
71
+ match: { any: true }
72
+ rate: 1.1
73
+ requires: { membership: bofa-rewards-member }
74
+ note: 1% plus the 10% BofA Rewards Member bonus.
75
+ - id: three-percent-bofa-rewards-preferred-plus
76
+ match: { choice: three-percent }
77
+ rate: 3.75
78
+ cap: three-and-two-percent-combined
79
+ requires: { membership: bofa-rewards-preferred-plus }
80
+ note: 3% plus the 25% BofA Rewards Preferred Plus bonus.
81
+ - id: two-percent-bofa-rewards-preferred-plus
82
+ match: { categories: [grocery.supermarket, warehouse_club] }
83
+ rate: 2.5
84
+ cap: three-and-two-percent-combined
85
+ requires: { membership: bofa-rewards-preferred-plus }
86
+ note: 2% plus the 25% BofA Rewards Preferred Plus bonus.
87
+ - id: everything-else-bofa-rewards-preferred-plus
88
+ match: { any: true }
89
+ rate: 1.25
90
+ requires: { membership: bofa-rewards-preferred-plus }
91
+ note: 1% plus the 25% BofA Rewards Preferred Plus bonus.
92
+ - id: three-percent-bofa-rewards-preferred-honors
93
+ match: { choice: three-percent }
94
+ rate: 4.5
95
+ cap: three-and-two-percent-combined
96
+ requires: { membership: bofa-rewards-preferred-honors }
97
+ note: 3% plus the 50% BofA Rewards Preferred Honors bonus.
98
+ - id: two-percent-bofa-rewards-preferred-honors
99
+ match: { categories: [grocery.supermarket, warehouse_club] }
100
+ rate: 3
101
+ cap: three-and-two-percent-combined
102
+ requires: { membership: bofa-rewards-preferred-honors }
103
+ note: 2% plus the 50% BofA Rewards Preferred Honors bonus.
104
+ - id: everything-else-bofa-rewards-preferred-honors
105
+ match: { any: true }
106
+ rate: 1.5
107
+ requires: { membership: bofa-rewards-preferred-honors }
108
+ note: 1% plus the 50% BofA Rewards Preferred Honors bonus.
109
+ - id: three-percent-bofa-rewards-premier
110
+ match: { choice: three-percent }
111
+ rate: 5.25
112
+ cap: three-and-two-percent-combined
113
+ requires: { membership: bofa-rewards-premier }
114
+ note: 3% plus the 75% BofA Rewards Premier bonus.
115
+ - id: two-percent-bofa-rewards-premier
116
+ match: { categories: [grocery.supermarket, warehouse_club] }
117
+ rate: 3.5
118
+ cap: three-and-two-percent-combined
119
+ requires: { membership: bofa-rewards-premier }
120
+ note: 2% plus the 75% BofA Rewards Premier bonus.
121
+ - id: everything-else-bofa-rewards-premier
122
+ match: { any: true }
123
+ rate: 1.75
124
+ requires: { membership: bofa-rewards-premier }
125
+ note: 1% plus the 75% BofA Rewards Premier bonus.
126
+ # Issuer text backing each rule; `yarn check-sources` checks it weekly.
127
+ evidence:
128
+ - rule: base_rate
129
+ url: https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/
130
+ quote: 'Unlimited 1% cash back on all other purchases'
131
+ - rule: three-percent
132
+ url: https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/cash-back-category-choices/
133
+ quote: '3% cash back in the category of your choice'
134
+ - rule: three-percent
135
+ url: https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/cash-back-category-choices/
136
+ quote: 'You can change it once each calendar month, or make no change and it stays the same.'
137
+ - rule: three-percent
138
+ url: https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/cash-back-category-choices/
139
+ quote: 'Until you change your category it will be set to Gas & EV Charging Stations.'
140
+ - rule: three-percent
141
+ url: https://www.bankofamerica.com/credit-cards/terms-and-conditions/?campaignid=4079537&productoffercode=8H&locale=en_US
142
+ quote: 'first $2,500 of combined Net Purchases posted to the account each calendar quarter'
143
+ - rule: three-percent
144
+ url: https://www.bankofamerica.com/credit-cards/terms-and-conditions/?campaignid=4079537&productoffercode=8H&locale=en_US
145
+ quote: 'purchases must be made online via a website or a digital application (an app)'
146
+ - rule: three-percent
147
+ url: https://www.bankofamerica.com/credit-cards/terms-and-conditions/?campaignid=4079537&productoffercode=8H&locale=en_US
148
+ quote: 'doctors and hospitals, government services and taxes, insurances, membership organizations, education/schools, and some utilities'
149
+ - rule: three-percent
150
+ url: https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/cash-back-category-choices/
151
+ quote: 'Courier Services (Air and Ground, Freight Forwarders)'
152
+ - rule: three-percent
153
+ url: https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/cash-back-category-choices/
154
+ quote: 'Clubs, Country Clubs, Membership (Athletic, Recreation, Sports)'
155
+ - rule: three-percent
156
+ url: https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/cash-back-category-choices/
157
+ quote: 'parking, public transit, taxis/ride share'
158
+ - rule: two-percent
159
+ url: https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/
160
+ quote: 'Automatic 2% cash back at grocery stores and wholesale clubs'
161
+ - rule: two-percent
162
+ url: https://www.bankofamerica.com/credit-cards/terms-and-conditions/?campaignid=4079537&productoffercode=8H&locale=en_US
163
+ quote: 'Excluded from this Bonus Category are superstores and smaller stores (like drug stores)'
164
+ - rule: three-percent-bofa-rewards-member
165
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
166
+ quote: 'a $100 purchase that earns 3% ($3.00) will actually earn $3.30'
167
+ - rule: two-percent-bofa-rewards-member
168
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
169
+ quote: 'bonus of 10% for the Member tier'
170
+ - rule: everything-else-bofa-rewards-member
171
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
172
+ quote: 'applied after all base and bonus cash rewards have been calculated on a purchase'
173
+ - rule: three-percent-bofa-rewards-preferred-plus
174
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
175
+ quote: 'will actually earn $3.30, $3.75, $4.50 or $5.25 based on your tier'
176
+ - rule: two-percent-bofa-rewards-preferred-plus
177
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
178
+ quote: '25% for the Preferred Plus tier'
179
+ - rule: everything-else-bofa-rewards-preferred-plus
180
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
181
+ quote: '25% for the Preferred Plus tier'
182
+ - rule: three-percent-bofa-rewards-preferred-honors
183
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
184
+ quote: 'will actually earn $3.30, $3.75, $4.50 or $5.25 based on your tier'
185
+ - rule: two-percent-bofa-rewards-preferred-honors
186
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
187
+ quote: '50% for the Preferred Honors tier'
188
+ - rule: everything-else-bofa-rewards-preferred-honors
189
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
190
+ quote: '50% for the Preferred Honors tier'
191
+ - rule: three-percent-bofa-rewards-premier
192
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
193
+ quote: 'will actually earn $3.30, $3.75, $4.50 or $5.25 based on your tier'
194
+ - rule: two-percent-bofa-rewards-premier
195
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
196
+ quote: 'or 75% for the Premier tier'
197
+ - rule: everything-else-bofa-rewards-premier
198
+ url: https://www.bankofamerica.com/bofa-rewards/increasing-bofa-rewards-with-credit-cards/
199
+ quote: 'or 75% for the Premier tier'
200
+ - rule: annual_fee
201
+ url: https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/
202
+ quote: 'Annual Fee $0'
203
+ - rule: welcome_offer
204
+ url: https://www.bankofamerica.com/credit-cards/products/cash-back-credit-card/
205
+ quote: 'Online $200 cash rewards bonus after making at least $1,000 in purchases in the first 90 days'
206
+ todo:
207
+ - New accounts earn 6% in the choice category for the first year (an extra 3%, inside the same $2,500 quarterly cap, with no BofA Rewards bonus on the extra 3%). earnrate can't see when an account was opened, so this isn't modeled.
208
+ - 'The BofA Rewards rules use proposed membership ids: bofa-rewards-member, bofa-rewards-preferred-plus, bofa-rewards-preferred-honors and bofa-rewards-premier. Cardholders who joined Preferred Rewards before May 26, 2026 may have an "extended card benefit" that Bank of America doesn''t define.'
209
+ - With a BofA Rewards tier, a purchase that crosses the $2,500 cap counts the part past the cap at 1% instead of the tier's boosted base rate, so it's slightly understated.
210
+ - "Online shopping counts any online purchase earnrate can't place in an excluded category, including unknown merchants. Bank of America also excludes doctors, government, insurance, schools and other non-retail services, which earnrate can't tell apart."
@@ -106,7 +106,10 @@ function cardProblems(card, catalog, merchantIds) {
106
106
  problems.push(`${where} choice ${choice.id}: default '${d}' is not an option`);
107
107
  }
108
108
  for (const [option, covered] of options) {
109
- for (const c of covered.categories)
109
+ for (const c of [
110
+ ...covered.categories,
111
+ ...(covered.online?.exclude ?? []),
112
+ ])
110
113
  checkCategory(c, `choice ${choice.id}.${option}`);
111
114
  for (const m of covered.merchants)
112
115
  checkMerchant(m, `choice ${choice.id}.${option}`);
@@ -30,6 +30,7 @@ export function pickedCategories(card, held, date) {
30
30
  let picks = chosen?.picks[choice.id];
31
31
  if (picks &&
32
32
  chosen &&
33
+ choice.period !== 'until_changed' &&
33
34
  chosen.period !== periodKey(choice.period, date, null)) {
34
35
  staleChoice = chosen.period;
35
36
  picks = undefined;
@@ -41,6 +42,7 @@ export function pickedCategories(card, held, date) {
41
42
  picked.set(choice.id, {
42
43
  categories: options.flatMap((o) => o.categories),
43
44
  merchants: options.flatMap((o) => o.merchants),
45
+ online: options.flatMap((o) => (o.online ? [o.online] : [])),
44
46
  });
45
47
  }
46
48
  return { picked, staleChoice };
@@ -60,7 +62,11 @@ function applicability(rule, ctx, held, picked) {
60
62
  const covered = picked.get(match.choice);
61
63
  const merchantPicked = ctx.merchantId !== null &&
62
64
  (covered?.merchants.includes(ctx.merchantId) ?? false);
63
- if (!merchantPicked && !inCategory(covered?.categories ?? []))
65
+ const onlinePicked = ctx.channel === 'online' &&
66
+ (covered?.online.some((o) => !inCategory(o.exclude)) ?? false);
67
+ if (!merchantPicked &&
68
+ !onlinePicked &&
69
+ !inCategory(covered?.categories ?? []))
64
70
  return 'no';
65
71
  }
66
72
  if (exclude?.categories && inCategory(exclude.categories))
@@ -75,21 +75,40 @@ export const WelcomeOfferSchema = z
75
75
  export const ChoiceSchema = z.strictObject({
76
76
  id: KebabId,
77
77
  pick: z.number().int().positive(),
78
- period: z.enum(['month', 'quarter', 'calendar_year']),
78
+ // `until_changed`: the pick carries over until the cardholder changes it,
79
+ // so a pick made in an earlier period is still current.
80
+ period: z.enum(['month', 'quarter', 'calendar_year', 'until_changed']),
79
81
  // Option ids that apply when the wallet hasn't picked, e.g. Bilt's dining.
80
82
  default: z.array(KebabId).min(1).optional(),
81
- // Option id → what that option covers: a list of categories, or, when the
82
- // issuer publishes the only qualifying stores, `{ merchants: [...] }`.
83
+ // Option id → what that option covers: a list of categories; or, when the
84
+ // issuer publishes the only qualifying stores, `{ merchants: [...] }`; or,
85
+ // for "online shopping" picks, `{ channel: online, exclude: [...] }`, any
86
+ // online purchase outside the excluded categories.
83
87
  options: z.record(KebabId, z.union([
84
88
  z
85
89
  .array(CategoryId)
86
90
  .min(1)
87
- .transform((categories) => ({ categories, merchants: [] })),
91
+ .transform((categories) => ({
92
+ categories,
93
+ merchants: [],
94
+ online: null,
95
+ })),
88
96
  z
89
97
  .strictObject({ merchants: z.array(KebabId).min(1) })
90
98
  .transform(({ merchants }) => ({
91
99
  categories: [],
92
100
  merchants,
101
+ online: null,
102
+ })),
103
+ z
104
+ .strictObject({
105
+ channel: z.literal('online'),
106
+ exclude: z.array(CategoryId).default([]),
107
+ })
108
+ .transform(({ exclude }) => ({
109
+ categories: [],
110
+ merchants: [],
111
+ online: { exclude },
93
112
  })),
94
113
  ])),
95
114
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "earnrate",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Picks the best credit card from your wallet for a merchant, and explains why",
5
5
  "keywords": [
6
6
  "credit cards",