earnrate 0.2.5 → 0.2.7
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 +25 -18
- package/data/CHANGELOG.md +56 -0
- package/data/VERSION +1 -1
- package/data/cards/amex-blue-business-plus.yaml +14 -6
- package/data/cards/amex-blue-cash-everyday.yaml +12 -3
- package/data/cards/amex-blue-cash-preferred.yaml +17 -3
- package/data/cards/amex-gold.yaml +19 -3
- package/data/cards/amex-platinum.yaml +19 -3
- package/data/cards/bank-of-america-atmos-rewards-ascent.yaml +10 -2
- package/data/cards/bank-of-america-atmos-rewards-business.yaml +14 -3
- package/data/cards/bank-of-america-atmos-rewards-summit.yaml +4 -1
- package/data/cards/bank-of-america-customized-cash-rewards.yaml +10 -5
- package/data/cards/bilt-obsidian.yaml +4 -3
- package/data/cards/capital-one-kohls-card.yaml +11 -2
- package/data/cards/capital-one-kohls-rewards-visa.yaml +8 -2
- package/data/cards/capital-one-savor.yaml +32 -10
- package/data/cards/capital-one-venture-x.yaml +19 -4
- package/data/cards/capital-one-venture.yaml +19 -5
- package/data/cards/chase-aeroplan.yaml +15 -3
- package/data/cards/chase-freedom-flex.yaml +49 -10
- package/data/cards/chase-freedom-unlimited.yaml +6 -2
- package/data/cards/chase-ihg-one-rewards-business.yaml +18 -2
- package/data/cards/chase-ihg-one-rewards-premier-select.yaml +14 -2
- package/data/cards/chase-ihg-one-rewards-premier.yaml +15 -2
- package/data/cards/chase-ihg-one-rewards.yaml +8 -2
- package/data/cards/chase-ink-business-cash.yaml +7 -3
- package/data/cards/chase-ink-business-preferred.yaml +9 -2
- package/data/cards/chase-prime-visa.yaml +24 -5
- package/data/cards/chase-sapphire-preferred.yaml +59 -6
- package/data/cards/chase-sapphire-reserve.yaml +12 -2
- package/data/cards/chase-southwest-performance-business.yaml +22 -4
- package/data/cards/chase-southwest-plus.yaml +13 -3
- package/data/cards/chase-southwest-premier-business.yaml +22 -5
- package/data/cards/chase-southwest-premier.yaml +13 -3
- package/data/cards/chase-southwest-priority.yaml +18 -2
- package/data/cards/chase-united-business.yaml +5 -2
- package/data/cards/chase-united-club-business.yaml +2 -2
- package/data/cards/chase-united-club.yaml +16 -3
- package/data/cards/chase-united-explorer.yaml +5 -2
- package/data/cards/chase-united-gateway.yaml +2 -2
- package/data/cards/chase-united-quest.yaml +12 -3
- package/data/cards/citi-aadvantage-business.yaml +10 -4
- package/data/cards/citi-aadvantage-executive.yaml +8 -8
- package/data/cards/citi-aadvantage-mileup.yaml +7 -4
- package/data/cards/citi-aadvantage-platinum-select.yaml +1 -1
- package/data/cards/citi-costco-anywhere-visa-business.yaml +5 -2
- package/data/cards/citi-costco-anywhere-visa.yaml +6 -3
- package/data/cards/citi-custom-cash.yaml +43 -12
- package/data/cards/citi-double-cash.yaml +9 -3
- package/data/cards/citi-my-best-buy-visa.yaml +6 -3
- package/data/cards/citi-strata-elite.yaml +12 -2
- package/data/cards/citi-strata-premier.yaml +24 -2
- package/data/cards/sofi-unlimited-2.yaml +17 -3
- package/data/cards/td-bank-nordstrom-credit-card.yaml +12 -3
- package/data/cards/td-bank-nordstrom-visa.yaml +19 -3
- package/data/cards/td-bank-target-circle-card.yaml +13 -4
- package/data/cards/usbank-cash-plus.yaml +29 -7
- package/data/cards/wells-fargo-active-cash.yaml +9 -2
- package/data/cards/wells-fargo-autograph.yaml +31 -7
- package/data/issuers.yaml +32 -0
- package/data/merchants/seed.yaml +7 -0
- package/dist/brand.js +1 -0
- package/dist/cli/bin.js +10 -1
- package/dist/cli/cheatsheet.js +10 -5
- package/dist/cli/explore.js +6 -13
- package/dist/cli/import.js +2 -2
- package/dist/cli/missed.js +65 -0
- package/dist/cli/offers.js +17 -7
- package/dist/cli/run.js +29 -17
- package/dist/cli/todo.js +62 -0
- package/dist/cli/wallet.js +9 -6
- package/dist/data/file-store.js +6 -7
- package/dist/data/integrity.js +4 -0
- package/dist/data/load.js +4 -1
- package/dist/data/store.js +6 -22
- package/dist/data/wallet-edit.js +38 -13
- package/dist/engine/cheat-sheet.js +14 -6
- package/dist/engine/checklist.js +152 -0
- package/dist/engine/compare.js +2 -0
- package/dist/engine/evaluate.js +16 -11
- package/dist/engine/explore.js +6 -38
- package/dist/engine/memberships.js +1 -1
- package/dist/engine/offers.js +2 -1
- package/dist/engine/periods.js +18 -0
- package/dist/engine/recommend.js +21 -20
- package/dist/engine/rule-text.js +47 -0
- package/dist/engine/setup.js +14 -24
- package/dist/engine/simulate.js +7 -6
- package/dist/engine/text.js +13 -1
- package/dist/engine/wallet-change.js +10 -12
- package/dist/engine/wallet-summary.js +80 -65
- package/dist/format/card-text.js +7 -20
- package/dist/format/explore.js +2 -2
- package/dist/format/offers.js +2 -2
- package/dist/format/recommendation.js +2 -2
- package/dist/format/wallet.js +7 -1
- package/dist/import/analyze.js +2 -1
- package/dist/mcp/context.js +38 -13
- package/dist/mcp/inputs.js +1 -1
- package/dist/mcp/schemas.js +9 -0
- package/dist/mcp/serve.js +4 -0
- package/dist/mcp/server.js +8 -2
- package/dist/mcp/tools/account.js +44 -16
- package/dist/mcp/tools/answers.js +58 -19
- package/dist/mcp/tools/wallet.js +64 -25
- package/dist/merchants/cache.js +0 -10
- package/dist/merchants/missed.js +78 -0
- package/dist/merchants/resolve.js +81 -4
- package/dist/schema/card.js +4 -1
- package/dist/schema/issuer.js +8 -0
- package/dist/schema/wallet.js +1 -1
- package/package.json +1 -1
|
@@ -3,8 +3,8 @@ name: Nordstrom Visa Credit Card
|
|
|
3
3
|
issuer: td-bank
|
|
4
4
|
network: visa
|
|
5
5
|
currency: nordy-club
|
|
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.nordstrom.com/browse/nordy-club
|
|
10
10
|
- https://www.nordstrom.com/browse/nordy-club/faq
|
|
@@ -25,7 +25,7 @@ rules:
|
|
|
25
25
|
- id: nordstrom
|
|
26
26
|
match: { merchants: [nordstrom] }
|
|
27
27
|
rate: 1
|
|
28
|
-
note: Card's own share only. Cardmembers start at Influencer status and earn 2 points per dollar at Nordstrom and Nordstrom.com, 1 of which any member earns paying any way.
|
|
28
|
+
note: Card's own share only. Cardmembers start at Influencer status and earn 2 points per dollar at Nordstrom and Nordstrom.com, 1 of which any member earns paying any way. Apple Pay and Google Pay with the card still earn the full rate; through PayPal or another third-party processor, or another contactless method, it's 1 point per dollar, so nothing extra.
|
|
29
29
|
- id: nordstrom-ambassador
|
|
30
30
|
match: { merchants: [nordstrom] }
|
|
31
31
|
rate: 2
|
|
@@ -51,6 +51,10 @@ rules:
|
|
|
51
51
|
match: { categories: [dining] }
|
|
52
52
|
rate: 2
|
|
53
53
|
note: Includes fast food, bars, caterers and third-party delivery. Restaurants inside Nordstrom earn as Nordstrom purchases.
|
|
54
|
+
- id: caterers
|
|
55
|
+
match: { mccs: [5811] }
|
|
56
|
+
rate: 2
|
|
57
|
+
note: Caterers count as dining.
|
|
54
58
|
- id: streaming
|
|
55
59
|
match: { categories: [streaming, tv_internet] }
|
|
56
60
|
rate: 2
|
|
@@ -107,3 +111,15 @@ evidence:
|
|
|
107
111
|
- rule: not_with
|
|
108
112
|
url: https://www.nordstrom.com/becomecardmember
|
|
109
113
|
quote: 'Current and previous Nordstrom credit cardmembers and Nordstrom employees are not eligible for this offer'
|
|
114
|
+
- rule: nordstrom
|
|
115
|
+
url: https://www.nordstrom.com/browse/nordy-club/faq
|
|
116
|
+
quote: "If you use your Nordstrom card via Google Pay or Apple Pay, you'll earn your standard Nordy Club base points"
|
|
117
|
+
- rule: nordstrom
|
|
118
|
+
url: https://www.nordstrom.com/browse/nordy-club/terms-conditions
|
|
119
|
+
quote: 'For purchases made with a third-party payment processor, including those linked to a Nordstrom credit or debit card'
|
|
120
|
+
- rule: nordstrom-rack
|
|
121
|
+
url: https://www.nordstrom.com/browse/nordy-club/terms-conditions
|
|
122
|
+
quote: 'Eligible purchases exclude gift cards, alterations, charitable donations, taxes, shipping fees'
|
|
123
|
+
- rule: caterers
|
|
124
|
+
url: https://www.nordstrom.com/browse/nordy-club/terms-conditions
|
|
125
|
+
quote: 'cocktail lounges, caterers, fast-food locations, and third-party restaurant delivery and takeout services'
|
|
@@ -4,8 +4,8 @@ issuer: td-bank
|
|
|
4
4
|
network: store
|
|
5
5
|
only_at: [target]
|
|
6
6
|
currency: cash
|
|
7
|
-
last_verified: 2026-
|
|
8
|
-
verified_by:
|
|
7
|
+
last_verified: 2026-11-01
|
|
8
|
+
verified_by: claude
|
|
9
9
|
source_urls:
|
|
10
10
|
- https://www.target.com/circlecard
|
|
11
11
|
- https://www.target.com/c/target-circle-card-program-rules/-/N-4tgj0
|
|
@@ -17,12 +17,12 @@ welcome_offer:
|
|
|
17
17
|
dollars: 50
|
|
18
18
|
spend: 50
|
|
19
19
|
months: 2
|
|
20
|
-
note: Paid as $50 in Target Circle Rewards, redeemable through Target Circle, not cash. Spend $50 or more on qualifying purchases in-store or at Target.com with the Target Circle Credit Card within 60 days of approval. Not for the Target Circle Debit Card or mail-in applications. Qualifying purchases exclude alcohol
|
|
20
|
+
note: Paid as $50 in Target Circle Rewards, redeemable through Target Circle, not cash. Spend $50 or more on qualifying purchases in-store or at Target.com with the Target Circle Credit Card within 60 days of approval. Not for the Target Circle Debit Card or mail-in applications. Qualifying purchases exclude alcohol, Target GiftCards, prepaid Visa/Mastercard/Amex cards, CVS pharmacy, clinic and Target Optical.
|
|
21
21
|
rules:
|
|
22
22
|
- id: target
|
|
23
23
|
match: { merchants: [target] }
|
|
24
24
|
rate: 5
|
|
25
|
-
note: An instant 5% discount at checkout in Target stores and on Target.com, after other discounts, on the part of the purchase paid with the card. Not on prescriptions, pharmacy-counter items or clinic services, Target Optical eye exams and protection plans (glasses and contacts do count), Target GiftCards
|
|
25
|
+
note: An instant 5% discount at checkout in Target stores and on Target.com, after other discounts, on the part of the purchase paid with the card. Not on prescriptions, pharmacy-counter items or clinic services, Target Optical eye exams and protection plans (glasses and contacts do count), Target GiftCards, prepaid Visa/Mastercard/Amex cards or sports betting gift cards, Starbucks gift cards bought at in-store Starbucks, Shipt or Target Circle 360 fees, wireless protection plans and carrier deposits, gift wrapping, alcohol in Indiana, taxes, shipping or other fees.
|
|
26
26
|
todo:
|
|
27
27
|
- The debit version (Target Circle Debit Card) gets the same 5% discount but isn't modeled as a separate card.
|
|
28
28
|
- Target considers credit applicants for the Target Mastercard first, which also earns 2% on dining and gas and 1% elsewhere. Not modeled; target.com doesn't say what form those earnings take.
|
|
@@ -49,3 +49,12 @@ evidence:
|
|
|
49
49
|
- rule: welcome_offer
|
|
50
50
|
url: https://www.target.com/circlecard
|
|
51
51
|
quote: 'within 60 days of approval'
|
|
52
|
+
- rule: target
|
|
53
|
+
url: https://www.target.com/c/target-circle-card-program-rules/-/N-4tgj0
|
|
54
|
+
quote: 'Game On, DraftsKings, and other sports betting gift cards'
|
|
55
|
+
- rule: target
|
|
56
|
+
url: https://www.target.com/c/target-circle-card-program-rules/-/N-4tgj0
|
|
57
|
+
quote: 'Wireless protection program purchases and deposits required by mobile carrier'
|
|
58
|
+
- rule: welcome_offer
|
|
59
|
+
url: https://www.target.com/circlecard
|
|
60
|
+
quote: 'Qualifying purchases exclude alcohol, Target GiftCards, Visa, American Express and Mastercard gift cards/prepaid cards'
|
|
@@ -3,8 +3,8 @@ name: U.S. Bank Cash+ Visa Signature Card
|
|
|
3
3
|
issuer: usbank
|
|
4
4
|
network: visa
|
|
5
5
|
currency: cash
|
|
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.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
10
10
|
- https://cashplus.usbank.com/cash-plus/samplemerchants
|
|
@@ -64,14 +64,16 @@ choices:
|
|
|
64
64
|
rules:
|
|
65
65
|
- id: five-percent
|
|
66
66
|
match: { choice: five-percent }
|
|
67
|
+
# Ground transportation leaves out parking and tolls (also cruises and
|
|
68
|
+
# tickets bought through travel agencies); no other 5% option covers them.
|
|
69
|
+
exclude: { mccs: [7523, 4784] }
|
|
67
70
|
rate: 5
|
|
68
71
|
cap: five-percent-combined
|
|
69
72
|
- id: two-percent
|
|
70
73
|
match: { choice: two-percent }
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
exclude: { categories: [gas.superstore] }
|
|
74
|
+
# Fast food is its own 5% option, not part of Restaurants. Gas at grocery
|
|
75
|
+
# stores is left in until the todo below is settled.
|
|
76
|
+
exclude: { categories: [gas.superstore, dining.fast_food] }
|
|
75
77
|
rate: 2
|
|
76
78
|
- id: travel-center
|
|
77
79
|
match: { merchants: [usbank-travel-center] }
|
|
@@ -94,9 +96,29 @@ evidence:
|
|
|
94
96
|
- rule: five-percent
|
|
95
97
|
url: https://cashplus.usbank.com/cash-plus/samplemerchants
|
|
96
98
|
quote: 'This category includes only the merchants listed below'
|
|
99
|
+
- rule: five-percent
|
|
100
|
+
url: https://cashplus.usbank.com/cash-plus/samplemerchants
|
|
101
|
+
quote: 'steam ships, cruise lines, freight trains, parking meters, parking garages and toll stations'
|
|
102
|
+
- rule: five-percent
|
|
103
|
+
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
104
|
+
quote: 'Purchases over $2,000 in the 5% categories will earn 1% cash back.'
|
|
105
|
+
- rule: two-percent
|
|
106
|
+
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
107
|
+
quote: 'Gas station and electric vehicle charging station purchases at discount stores/supercenters, wholesale clubs and grocery stores/supermarkets'
|
|
108
|
+
- rule: two-percent
|
|
109
|
+
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
110
|
+
quote: 'Excludes discount stores/supercenters and wholesale clubs'
|
|
111
|
+
- rule: two-percent
|
|
112
|
+
url: https://cashplus.usbank.com/cash-plus/samplemerchants
|
|
113
|
+
quote: "Excludes most fast food restaurants like McDonald's"
|
|
114
|
+
- rule: travel-center
|
|
115
|
+
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
116
|
+
quote: 'additional 4% cash back for every $1 in eligible Net Purchases spent on prepaid air, car and hotel reservations'
|
|
97
117
|
- rule: annual_fee
|
|
98
118
|
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
99
|
-
quote: '
|
|
119
|
+
quote: '$0 Annual fee'
|
|
100
120
|
- rule: welcome_offer
|
|
101
121
|
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
102
122
|
quote: 'Earn a $200 rewards bonus after you spend $1,000 in eligible purchases within the first 90 days of account opening'
|
|
123
|
+
todo:
|
|
124
|
+
- "Gas at grocery stores (Kroger, Hy-Vee, Meijer) is modeled at 2%, as U.S. Bank's sample merchants page says it may qualify, varying by location; the card page's footnote says 1%, so it may earn 1% at some stations."
|
|
@@ -3,10 +3,11 @@ name: Wells Fargo Active Cash Card
|
|
|
3
3
|
issuer: wells-fargo
|
|
4
4
|
network: visa
|
|
5
5
|
currency: cash
|
|
6
|
-
last_verified: 2026-
|
|
7
|
-
verified_by:
|
|
6
|
+
last_verified: 2026-11-01
|
|
7
|
+
verified_by: claude
|
|
8
8
|
source_urls:
|
|
9
9
|
- https://creditcards.wellsfargo.com/active-cash-credit-card/
|
|
10
|
+
- https://www.wellsfargo.com/credit-cards/active-cash/terms/
|
|
10
11
|
base_rate: 2
|
|
11
12
|
annual_fee: 0
|
|
12
13
|
welcome_offer:
|
|
@@ -23,6 +24,9 @@ evidence:
|
|
|
23
24
|
- rule: base_rate
|
|
24
25
|
url: https://creditcards.wellsfargo.com/active-cash-credit-card/
|
|
25
26
|
quote: '2% cash rewards are earned for every $1 spent in net purchases'
|
|
27
|
+
- rule: base_rate
|
|
28
|
+
url: https://www.wellsfargo.com/credit-cards/active-cash/terms/
|
|
29
|
+
quote: 'For every $1.00 you spend with this Credit Card for net purchases, you earn two cents ($0.02) in Cash Rewards.'
|
|
26
30
|
- rule: annual_fee
|
|
27
31
|
url: https://creditcards.wellsfargo.com/active-cash-credit-card/
|
|
28
32
|
quote: '$0 annual fee'
|
|
@@ -32,3 +36,6 @@ evidence:
|
|
|
32
36
|
- rule: welcome_offer
|
|
33
37
|
url: https://creditcards.wellsfargo.com/active-cash-credit-card/
|
|
34
38
|
quote: 'To qualify for the $100 cash rewards bonus, a total of at least $500 in net purchases'
|
|
39
|
+
- rule: welcome_offer
|
|
40
|
+
url: https://www.wellsfargo.com/credit-cards/active-cash/terms/
|
|
41
|
+
quote: 'opened one within the last 48 months from the date of this application'
|
|
@@ -3,11 +3,12 @@ name: Wells Fargo Autograph Card
|
|
|
3
3
|
issuer: wells-fargo
|
|
4
4
|
network: visa
|
|
5
5
|
currency: WFR
|
|
6
|
-
last_verified: 2026-
|
|
7
|
-
verified_by:
|
|
6
|
+
last_verified: 2026-11-01
|
|
7
|
+
verified_by: claude
|
|
8
8
|
source_urls:
|
|
9
9
|
- https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
10
10
|
- https://www.wellsfargo.com/credit-cards/autograph-visa/streaming/
|
|
11
|
+
- https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
11
12
|
base_rate: 1
|
|
12
13
|
annual_fee: 0
|
|
13
14
|
welcome_offer:
|
|
@@ -18,8 +19,9 @@ welcome_offer:
|
|
|
18
19
|
rules:
|
|
19
20
|
- id: dining
|
|
20
21
|
match: { categories: [dining] }
|
|
22
|
+
exclude: { merchants: [doordash, uber-eats] }
|
|
21
23
|
rate: 3
|
|
22
|
-
note: Restaurants, bars (drinking places), fast food and caterers, by Visa merchant code.
|
|
24
|
+
note: Restaurants, bars (drinking places), fast food and caterers, by Visa merchant code. Wells Fargo says bakeries, food at grocery stores and third-party delivery services may not earn 3X.
|
|
23
25
|
- id: travel
|
|
24
26
|
match: { categories: [travel] }
|
|
25
27
|
rate: 3
|
|
@@ -31,43 +33,65 @@ rules:
|
|
|
31
33
|
note: Passenger rail, taxis, limousines, ferries, tolls and parking. Intercity bus lines aren't on Wells Fargo's list.
|
|
32
34
|
- id: gas
|
|
33
35
|
match: { categories: [gas, ev_charging] }
|
|
36
|
+
exclude: { categories: [gas.superstore, gas.grocery_store] }
|
|
34
37
|
rate: 3
|
|
35
|
-
note: Gas stations, automated fuel dispensers and EV charging stations, by Visa merchant code.
|
|
38
|
+
note: Gas stations, automated fuel dispensers and EV charging stations, by Visa merchant code. Fuel or charging at superstores, warehouse clubs, grocery stores, car washes and auto repair shops may not earn 3X.
|
|
36
39
|
- id: streaming
|
|
37
40
|
match: { categories: [streaming, tv_internet] }
|
|
38
41
|
rate: 3
|
|
39
|
-
note: Cable and pay TV, digital goods (books, movies, music) and subscription services. Subscriptions bundled with another product or billed by a third party (an app store, a car) may not count. An internet-only bill may not code as pay TV.
|
|
42
|
+
note: Cable and pay TV, digital goods (books, movies, music) and subscription services. Subscriptions bundled with another product or billed by a third party (an app store, a cable, phone or internet provider, an online retailer, a car) may not count. An internet-only bill may not code as pay TV.
|
|
40
43
|
- id: phone
|
|
41
44
|
match: { categories: [phone] }
|
|
42
45
|
rate: 3
|
|
43
|
-
note: Landline and cell phone providers.
|
|
46
|
+
note: Landline and cell phone providers. Phones, accessories, add-ons like insurance or extra data, and plans bundled or billed by a third party (an internet provider, a car) may not count.
|
|
44
47
|
# Not modeled: cell phone protection.
|
|
45
48
|
# Issuer text backing each rule; `yarn check-sources` checks it weekly.
|
|
46
49
|
evidence:
|
|
47
50
|
- rule: base_rate
|
|
48
51
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
49
52
|
quote: '1 rewards point will be earned per $1 spent on other net purchases'
|
|
53
|
+
- rule: travel
|
|
54
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
55
|
+
quote: 'Airlines Hotel/motels Campgrounds Timeshares Cruise lines Travel agencies Discount travel sites Vehicle/auto rentals'
|
|
50
56
|
- rule: dining
|
|
51
57
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
52
58
|
quote: 'Dining: eating places and restaurants, drinking places, fast food restaurants'
|
|
59
|
+
- rule: dining
|
|
60
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
61
|
+
quote: 'Grocery stores and other miscellaneous places that serve food or operate restaurants on site Third-party delivery services'
|
|
53
62
|
- rule: travel
|
|
54
63
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
55
64
|
quote: 'Travel: airline, hotel/motel, timeshare, or vehicle/auto rental, cruise lines'
|
|
56
65
|
- rule: transit
|
|
57
66
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
58
|
-
quote: 'Transit: passenger railway, taxis, limousines, ferries, toll bridges and highways'
|
|
67
|
+
quote: 'Transit: passenger railway, taxis, limousines, ferries, toll bridges and highways, parking lots and garages'
|
|
68
|
+
- rule: transit
|
|
69
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
70
|
+
quote: 'Ferries Parking lots and garages Limousines Taxis Passenger railway Toll bridges and highways'
|
|
59
71
|
- rule: gas
|
|
60
72
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
61
73
|
quote: 'Gas: gas stations, automated fuel dispensers, and electric vehicle charging stations'
|
|
74
|
+
- rule: gas
|
|
75
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
76
|
+
quote: 'Gas or electric vehicle charging purchases made at the following locations may not earn bonus Points'
|
|
77
|
+
- rule: gas
|
|
78
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
79
|
+
quote: 'Auto repair stores Superstores Car washes Warehouse/membership clubs Grocery stores'
|
|
62
80
|
- rule: streaming
|
|
63
81
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
64
82
|
quote: 'Popular streaming services: cable and other pay television, digital goods, books'
|
|
65
83
|
- rule: streaming
|
|
66
84
|
url: https://www.wellsfargo.com/credit-cards/autograph-visa/streaming/
|
|
67
85
|
quote: 'You may earn 3X on these streaming services'
|
|
86
|
+
- rule: streaming
|
|
87
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
88
|
+
quote: 'Streaming services that a third party bills, such as a digital platform; a cable, telecommunications, or internet provider'
|
|
68
89
|
- rule: phone
|
|
69
90
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
70
91
|
quote: 'Phone plans: landline and cell phone providers considered telecommunication services'
|
|
92
|
+
- rule: phone
|
|
93
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
94
|
+
quote: 'Phone-plan services that a third party bundles or bills, such as an internet provider or in-vehicle service'
|
|
71
95
|
- rule: annual_fee
|
|
72
96
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
73
97
|
quote: '$0 Annual Fee'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# The banks behind the cards: the name shown for each, and other words people
|
|
2
|
+
# search with ("bofa", "amex"). Every card's `issuer` must be one of these.
|
|
3
|
+
|
|
4
|
+
- id: amex
|
|
5
|
+
name: American Express
|
|
6
|
+
search: [amex, american express]
|
|
7
|
+
- id: bank-of-america
|
|
8
|
+
name: Bank of America
|
|
9
|
+
search: [bofa, boa, bankofamerica]
|
|
10
|
+
- id: capital-one
|
|
11
|
+
name: Capital One
|
|
12
|
+
search: [capitalone]
|
|
13
|
+
- id: chase
|
|
14
|
+
name: Chase
|
|
15
|
+
- id: citi
|
|
16
|
+
name: Citi
|
|
17
|
+
search: [citibank]
|
|
18
|
+
- id: column
|
|
19
|
+
# Column N.A. issues the Bilt card; people know it as Bilt.
|
|
20
|
+
name: Bilt
|
|
21
|
+
search: [bilt]
|
|
22
|
+
- id: sofi
|
|
23
|
+
name: SoFi
|
|
24
|
+
- id: td-bank
|
|
25
|
+
name: TD Bank
|
|
26
|
+
search: [td]
|
|
27
|
+
- id: usbank
|
|
28
|
+
name: U.S. Bank
|
|
29
|
+
search: [us bank]
|
|
30
|
+
- id: wells-fargo
|
|
31
|
+
name: Wells Fargo
|
|
32
|
+
search: [wf, wellsfargo]
|
package/data/merchants/seed.yaml
CHANGED
|
@@ -104,6 +104,13 @@
|
|
|
104
104
|
category: streaming,
|
|
105
105
|
}
|
|
106
106
|
- { id: max, name: Max, aliases: [hbo max], category: streaming }
|
|
107
|
+
- { id: peacock, name: Peacock, aliases: [peacock tv], category: streaming }
|
|
108
|
+
- {
|
|
109
|
+
id: showtime,
|
|
110
|
+
name: Showtime,
|
|
111
|
+
aliases: [paramount plus with showtime],
|
|
112
|
+
category: streaming,
|
|
113
|
+
}
|
|
107
114
|
- { id: spotify, name: Spotify, category: streaming }
|
|
108
115
|
- {
|
|
109
116
|
id: apple-tv,
|
package/dist/brand.js
CHANGED
|
@@ -5,6 +5,7 @@ export const MCP_SERVER_NAME = 'earnrate';
|
|
|
5
5
|
export const CONFIG_DIR = '.earnrate'; // under the home directory
|
|
6
6
|
export const WALLET_ENV_VAR = 'EARNRATE_WALLET';
|
|
7
7
|
export const MERCHANT_CACHE_ENV_VAR = 'EARNRATE_MERCHANT_CACHE';
|
|
8
|
+
export const MISSED_ENV_VAR = 'EARNRATE_MISSED_MERCHANTS';
|
|
8
9
|
export const LLM_ENV_VAR = 'EARNRATE_LLM';
|
|
9
10
|
export const LLM_MODEL_ENV_VAR = 'EARNRATE_LLM_MODEL';
|
|
10
11
|
export const LLM_MAX_CALLS_ENV_VAR = 'EARNRATE_LLM_MAX_CALLS';
|
package/dist/cli/bin.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { styleText } from 'node:util';
|
|
3
|
+
import { CLI_NAME, WALLET_ENV_VAR } from "../brand.js";
|
|
3
4
|
import { localToday } from "../engine/periods.js";
|
|
4
5
|
import { run } from "./run.js";
|
|
5
|
-
if (process.argv[2] === 'mcp') {
|
|
6
|
+
if (process.argv[2] === 'mcp' && /^(-h|--help)$/.test(process.argv[3] ?? '')) {
|
|
7
|
+
process.stdout.write(`Usage: ${CLI_NAME} mcp
|
|
8
|
+
|
|
9
|
+
Runs earnrate as an MCP server over stdio, for an AI assistant to start. It
|
|
10
|
+
uses the same wallet file as the command line (${WALLET_ENV_VAR}, or
|
|
11
|
+
~/.earnrate/wallet.yaml).
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
else if (process.argv[2] === 'mcp') {
|
|
6
15
|
const { serveMcp } = await import("../mcp/serve.js");
|
|
7
16
|
await serveMcp(process.env);
|
|
8
17
|
}
|
package/dist/cli/cheatsheet.js
CHANGED
|
@@ -15,6 +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
|
+
--every-card Pick from every card earnrate knows, even with a wallet.
|
|
18
19
|
--member <name> A membership you have, like amazon-prime or costco. Repeatable.
|
|
19
20
|
--valuation <profile> flat or points-heavy, instead of your wallet's values.
|
|
20
21
|
--date <YYYY-MM-DD> The day to check. Default: today.
|
|
@@ -30,6 +31,7 @@ export async function runCheatSheet(argv, io) {
|
|
|
30
31
|
valuation: { type: 'string' },
|
|
31
32
|
date: { type: 'string' },
|
|
32
33
|
cards: { type: 'string' },
|
|
34
|
+
'every-card': { type: 'boolean' },
|
|
33
35
|
member: { type: 'string', multiple: true },
|
|
34
36
|
json: { type: 'boolean' },
|
|
35
37
|
help: { type: 'boolean', short: 'h' },
|
|
@@ -45,7 +47,9 @@ export async function runCheatSheet(argv, io) {
|
|
|
45
47
|
io.stdout(CHEATSHEET_HELP);
|
|
46
48
|
return 0;
|
|
47
49
|
}
|
|
48
|
-
const problem =
|
|
50
|
+
const problem = values['every-card'] && values.cards !== undefined
|
|
51
|
+
? 'Pick --cards or --every-card, not both.'
|
|
52
|
+
: (valuationProblem(values.valuation) ?? dateProblem(values.date));
|
|
49
53
|
if (problem !== null) {
|
|
50
54
|
io.stderr(`${problem}\n`);
|
|
51
55
|
return USAGE_ERROR;
|
|
@@ -58,17 +62,18 @@ export async function runCheatSheet(argv, io) {
|
|
|
58
62
|
if (loaded === null)
|
|
59
63
|
return RUNTIME_ERROR;
|
|
60
64
|
const date = values.date ?? io.today;
|
|
61
|
-
if (loaded === 'none') {
|
|
65
|
+
if (loaded === 'none' || values['every-card']) {
|
|
66
|
+
const saved = await savedSettings(io);
|
|
62
67
|
const sheet = catalogCheatSheet(catalog, date, {
|
|
63
68
|
filters: ALL_PERSONAL_CARDS,
|
|
64
|
-
memberships: values.member ??
|
|
69
|
+
memberships: values.member ?? saved.memberships,
|
|
65
70
|
valuations: values.valuation !== undefined && isValuationProfile(values.valuation)
|
|
66
71
|
? VALUATION_PROFILES[values.valuation]
|
|
67
|
-
:
|
|
72
|
+
: saved.valuations,
|
|
68
73
|
});
|
|
69
74
|
io.stdout(values.json
|
|
70
75
|
? `${JSON.stringify(sheet, null, 2)}\n`
|
|
71
|
-
: `${formatCheatSheet(sheet)}
|
|
76
|
+
: `${formatCheatSheet(sheet)}${loaded === 'none' ? `\n${NO_WALLET_HINT}\n` : ''}`);
|
|
72
77
|
return 0;
|
|
73
78
|
}
|
|
74
79
|
const wallet = values.valuation !== undefined && isValuationProfile(values.valuation)
|
package/dist/cli/explore.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import { CLI_NAME } from "../brand.js";
|
|
3
3
|
import { loadCatalog, walletPath } from "../data/load.js";
|
|
4
|
-
import { ALL_PERSONAL_CARDS, exploreCards,
|
|
4
|
+
import { ALL_PERSONAL_CARDS, exploreCards, } from "../engine/explore.js";
|
|
5
5
|
import { isValuationProfile, profileText, VALUATION_PROFILES, withValuationProfile, } from "../engine/profiles.js";
|
|
6
|
-
import { resolveSpendLines, simulateAdditions, simulateCatalog, spendLineFor,
|
|
6
|
+
import { resolveSpendLines, simulateAdditions, simulateCatalog, spendLineFor, typicalSpend, TYPICAL_SPEND_NOTE, } from "../engine/simulate.js";
|
|
7
7
|
import { formatAdditions, formatCatalogSimulation, formatExploration, } from "../format/explore.js";
|
|
8
8
|
import { BOTH_CHANNELS, cardList, channelOf, dateProblem, membershipProblem, splitAmount, valuationProblem, } from "./args.js";
|
|
9
9
|
import { cardsToUse, RUNTIME_ERROR, savedSettings, USAGE_ERROR, } from "./run.js";
|
|
@@ -141,10 +141,9 @@ export async function runBest(argv, io) {
|
|
|
141
141
|
io.stderr(`${settings}\n`);
|
|
142
142
|
return USAGE_ERROR;
|
|
143
143
|
}
|
|
144
|
-
const category = findCategory(catalog, text);
|
|
145
144
|
const result = exploreCards(catalog, {
|
|
146
|
-
merchant:
|
|
147
|
-
category,
|
|
145
|
+
merchant: text,
|
|
146
|
+
category: null,
|
|
148
147
|
amount,
|
|
149
148
|
channel: channelOf(values),
|
|
150
149
|
date: values.date ?? io.today,
|
|
@@ -160,13 +159,7 @@ function parseSpend(catalog, args) {
|
|
|
160
159
|
const match = /^(.+)=\$?(\d+(?:\.\d+)?)$/.exec(arg);
|
|
161
160
|
if (!match?.[1] || match[2] === undefined)
|
|
162
161
|
return `"${arg}" isn't <what>=<dollars a month>, e.g. groceries=600.`;
|
|
163
|
-
|
|
164
|
-
const category = findCategory(catalog, what);
|
|
165
|
-
lines.push({
|
|
166
|
-
merchant: category === null ? what : null,
|
|
167
|
-
category,
|
|
168
|
-
monthly: Number(match[2]),
|
|
169
|
-
});
|
|
162
|
+
lines.push(spendLineFor(catalog, match[1].replace(/[-_]/g, ' '), Number(match[2])));
|
|
170
163
|
}
|
|
171
164
|
if (lines.length === 0)
|
|
172
165
|
return `Give your monthly spending, e.g. ${CLI_NAME} simulate groceries=600 dining=300 other=1000.`;
|
|
@@ -203,7 +196,7 @@ export async function runSimulate(argv, io) {
|
|
|
203
196
|
}
|
|
204
197
|
const typical = positionals.length === 0;
|
|
205
198
|
const spend = typical
|
|
206
|
-
?
|
|
199
|
+
? typicalSpend(catalog)
|
|
207
200
|
: parseSpend(catalog, positionals);
|
|
208
201
|
if (typeof spend === 'string') {
|
|
209
202
|
io.stderr(`${spend}\n`);
|
package/dist/cli/import.js
CHANGED
|
@@ -12,7 +12,7 @@ import { cardValuation } from "../engine/valuation.js";
|
|
|
12
12
|
import { analyzeStatement, } from "../import/analyze.js";
|
|
13
13
|
import { parseStatement } from "../import/statement.js";
|
|
14
14
|
import { displayPath, readWallet, RUNTIME_ERROR, USAGE_ERROR, } from "./run.js";
|
|
15
|
-
import { dollarsAndCents } from "../engine/text.js";
|
|
15
|
+
import { currencyName, dollarsAndCents } from "../engine/text.js";
|
|
16
16
|
const IMPORT_HELP = `Usage: ${CLI_NAME} import <statement.csv> --card <card-id> [--apply]
|
|
17
17
|
${CLI_NAME} import <statement.csv>:<card-id> [<statement.csv>:<card-id> ...]
|
|
18
18
|
|
|
@@ -64,7 +64,7 @@ function valuationNote(catalog, wallet, cardIds, profile) {
|
|
|
64
64
|
}
|
|
65
65
|
if (values.size === 0)
|
|
66
66
|
return 'Cash back counted as cash.';
|
|
67
|
-
const parts = [...values].map(([id, cents]) => `${String(cents)}¢ for ${catalog
|
|
67
|
+
const parts = [...values].map(([id, cents]) => `${String(cents)}¢ for ${currencyName(catalog, id)}`);
|
|
68
68
|
const list = parts.length > 1
|
|
69
69
|
? `${parts.slice(0, -1).join(', ')} and ${parts.at(-1) ?? ''}`
|
|
70
70
|
: (parts[0] ?? '');
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { CLI_NAME, MISSED_ENV_VAR } from "../brand.js";
|
|
4
|
+
import { loadCatalog } from "../data/load.js";
|
|
5
|
+
import { categoryPhrase, shortDate } from "../engine/text.js";
|
|
6
|
+
import { missedPath, readMissed } from "../merchants/missed.js";
|
|
7
|
+
import { displayPath, USAGE_ERROR } from "./run.js";
|
|
8
|
+
const MISSED_HELP = `Usage: ${CLI_NAME} missed [--clear]
|
|
9
|
+
|
|
10
|
+
Stores earnrate didn't recognize when you asked about them here (the command
|
|
11
|
+
line or the local MCP server), most asked about first. They're kept only on
|
|
12
|
+
this computer. To suggest one for earnrate's data, open an issue at
|
|
13
|
+
https://github.com/cwasley/earnrate/issues.
|
|
14
|
+
|
|
15
|
+
Set ${MISSED_ENV_VAR}=off to stop keeping the list, or to a path to keep it
|
|
16
|
+
somewhere else.
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--clear Delete the list.
|
|
20
|
+
-h, --help Show this help.
|
|
21
|
+
`;
|
|
22
|
+
export async function runMissed(argv, io) {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = parseArgs({
|
|
26
|
+
args: argv,
|
|
27
|
+
options: {
|
|
28
|
+
clear: { type: 'boolean' },
|
|
29
|
+
help: { type: 'boolean', short: 'h' },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
io.stderr(`${e instanceof Error ? e.message : String(e)}\n${MISSED_HELP}`);
|
|
35
|
+
return USAGE_ERROR;
|
|
36
|
+
}
|
|
37
|
+
if (parsed.values.help) {
|
|
38
|
+
io.stdout(MISSED_HELP);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
const file = missedPath(io.env);
|
|
42
|
+
if (file === null) {
|
|
43
|
+
io.stdout(`The list is off (${MISSED_ENV_VAR}=off).\n`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
if (parsed.values.clear) {
|
|
47
|
+
await rm(file, { force: true });
|
|
48
|
+
io.stdout(`Deleted ${displayPath(file)}.\n`);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
const missed = await readMissed(file);
|
|
52
|
+
if (missed.length === 0) {
|
|
53
|
+
io.stdout('No unrecognized stores yet.\n');
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
const catalog = await loadCatalog();
|
|
57
|
+
const lines = missed
|
|
58
|
+
.sort((a, b) => b.count - a.count || b.last_seen.localeCompare(a.last_seen))
|
|
59
|
+
.map((m) => {
|
|
60
|
+
const kinds = m.categories.map((c) => categoryPhrase(catalog, c));
|
|
61
|
+
return `- ${m.name}: ${String(m.count)} ${m.count === 1 ? 'time' : 'times'}, last ${shortDate(m.last_seen)}${kinds.length === 0 ? '' : ` (taken as ${kinds.join(', ')})`}`;
|
|
62
|
+
});
|
|
63
|
+
io.stdout(`${lines.join('\n')}\n\nKept in ${displayPath(file)}. Suggest additions at https://github.com/cwasley/earnrate/issues.\n`);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
package/dist/cli/offers.js
CHANGED
|
@@ -3,9 +3,10 @@ import { CLI_NAME } from "../brand.js";
|
|
|
3
3
|
import { loadCatalog, walletPath } from "../data/load.js";
|
|
4
4
|
import { compareOffers } from "../engine/offers.js";
|
|
5
5
|
import { isValuationProfile, withValuationProfile } from "../engine/profiles.js";
|
|
6
|
+
import { currencyName } from "../engine/text.js";
|
|
6
7
|
import { formatOffers } from "../format/offers.js";
|
|
7
|
-
import { valuationProblem } from "./args.js";
|
|
8
|
-
import { cardsToUse, RUNTIME_ERROR, USAGE_ERROR } from "./run.js";
|
|
8
|
+
import { cardList, valuationProblem } from "./args.js";
|
|
9
|
+
import { cardsToUse, RUNTIME_ERROR, savedSettings, USAGE_ERROR, } from "./run.js";
|
|
9
10
|
const OFFERS_HELP = `Usage: ${CLI_NAME} offers [--cash | --points] [--no-fee] [--business] [options]
|
|
10
11
|
|
|
11
12
|
Ranks the welcome offers on cards you don't have by first-year value: the
|
|
@@ -18,6 +19,8 @@ Options:
|
|
|
18
19
|
--no-fee Cards with no annual fee.
|
|
19
20
|
--business Business cards instead of personal ones.
|
|
20
21
|
--held Include cards already in your wallet.
|
|
22
|
+
--cards <ids> Cards you have, instead of your wallet, comma separated.
|
|
23
|
+
--member <name> A membership you have, in place of the saved ones.
|
|
21
24
|
--valuation <profile> flat or points-heavy, instead of your wallet's values.
|
|
22
25
|
--all List every offer, not just the top 10.
|
|
23
26
|
--json Print the full comparison as JSON.
|
|
@@ -34,6 +37,8 @@ export async function runOffers(argv, io) {
|
|
|
34
37
|
'no-fee': { type: 'boolean' },
|
|
35
38
|
business: { type: 'boolean' },
|
|
36
39
|
held: { type: 'boolean' },
|
|
40
|
+
cards: { type: 'string' },
|
|
41
|
+
member: { type: 'string', multiple: true },
|
|
37
42
|
valuation: { type: 'string' },
|
|
38
43
|
all: { type: 'boolean' },
|
|
39
44
|
json: { type: 'boolean' },
|
|
@@ -61,12 +66,18 @@ export async function runOffers(argv, io) {
|
|
|
61
66
|
}
|
|
62
67
|
const catalog = await loadCatalog();
|
|
63
68
|
const found = await cardsToUse(catalog, walletPath(io.env), io, {
|
|
64
|
-
cards:
|
|
65
|
-
members: [],
|
|
69
|
+
cards: cardList(values.cards),
|
|
70
|
+
members: values.member ?? [],
|
|
66
71
|
});
|
|
67
72
|
if (found === null)
|
|
68
73
|
return RUNTIME_ERROR;
|
|
69
|
-
const loaded = found === 'none'
|
|
74
|
+
const loaded = found === 'none'
|
|
75
|
+
? await savedSettings(io).then((saved) => ({
|
|
76
|
+
valuations: saved.valuations,
|
|
77
|
+
memberships: values.member ?? saved.memberships,
|
|
78
|
+
cards: [],
|
|
79
|
+
}))
|
|
80
|
+
: found;
|
|
70
81
|
const wallet = values.valuation && isValuationProfile(values.valuation)
|
|
71
82
|
? withValuationProfile(loaded, values.valuation)
|
|
72
83
|
: loaded;
|
|
@@ -80,7 +91,6 @@ export async function runOffers(argv, io) {
|
|
|
80
91
|
io.stdout(`${JSON.stringify(comparison, null, 2)}\n`);
|
|
81
92
|
return 0;
|
|
82
93
|
}
|
|
83
|
-
|
|
84
|
-
io.stdout(formatOffers(comparison, currencyName, values.all ?? false));
|
|
94
|
+
io.stdout(formatOffers(comparison, (id) => currencyName(catalog, id), values.all ?? false));
|
|
85
95
|
return 0;
|
|
86
96
|
}
|