@hakimelek/monarchmoney 0.1.0 → 0.3.0

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
@@ -2,14 +2,12 @@
2
2
 
3
3
  Node.js/TypeScript library for accessing [Monarch Money](https://www.monarchmoney.com) data.
4
4
 
5
- Port of the [Python monarchmoney library](https://github.com/hammem/monarchmoney) with full TypeScript types, JSDoc, and a clean modular architecture.
6
-
7
5
  > **Disclaimer:** This project is unofficial and not affiliated with Monarch Money.
8
6
 
9
7
  ## Installation
10
8
 
11
9
  ```bash
12
- npm install monarchmoney
10
+ npm install @hakimelek/monarchmoney
13
11
  ```
14
12
 
15
13
  Requires **Node.js 18+** (uses native `fetch` and `AbortSignal.timeout`).
@@ -17,15 +15,23 @@ Requires **Node.js 18+** (uses native `fetch` and `AbortSignal.timeout`).
17
15
  ## Quick Start
18
16
 
19
17
  ```ts
20
- import { MonarchMoney, RequireMFAException } from "monarchmoney";
18
+ import {
19
+ MonarchMoney,
20
+ EmailOtpRequiredException,
21
+ RequireMFAException,
22
+ } from "@hakimelek/monarchmoney";
21
23
 
22
24
  const mm = new MonarchMoney();
23
25
 
24
- // Login
25
26
  try {
26
27
  await mm.login("your@email.com", "password");
27
28
  } catch (e) {
28
- if (e instanceof RequireMFAException) {
29
+ if (e instanceof EmailOtpRequiredException) {
30
+ // Monarch sent a verification code to your email
31
+ const code = await promptUser("Enter the code from your email:");
32
+ await mm.submitEmailOtp("your@email.com", "password", code);
33
+ } else if (e instanceof RequireMFAException) {
34
+ // TOTP-based MFA is enabled on the account
29
35
  await mm.multiFactorAuthenticate("your@email.com", "password", "123456");
30
36
  }
31
37
  }
@@ -37,13 +43,20 @@ console.log(accounts[0].displayName, accounts[0].currentBalance);
37
43
 
38
44
  ## Authentication
39
45
 
40
- ### Non-interactive
46
+ Monarch's API requires email verification (OTP) for new devices/sessions, even when MFA is disabled. The library handles this with distinct exception types so your app can respond appropriately.
47
+
48
+ ### Login with email OTP handling
41
49
 
42
50
  ```ts
43
- await mm.login("email", "password", {
44
- useSavedSession: true, // load token from disk if available (default)
45
- saveSession: true, // persist token after login (default)
46
- });
51
+ try {
52
+ await mm.login(email, password);
53
+ } catch (e) {
54
+ if (e instanceof EmailOtpRequiredException) {
55
+ // A code was sent to the user's email — prompt them for it
56
+ const code = await yourApp.promptForEmailCode();
57
+ await mm.submitEmailOtp(email, password, code);
58
+ }
59
+ }
47
60
  ```
48
61
 
49
62
  ### With MFA secret key (automatic TOTP)
@@ -56,29 +69,37 @@ await mm.login("email", "password", {
56
69
 
57
70
  The MFA secret is the "Two-factor text code" from **Settings > Security > Enable MFA** in Monarch Money.
58
71
 
59
- ### Interactive CLI
72
+ ### Session persistence & token reuse
73
+
74
+ After a successful login (including email OTP), you can save the token to avoid re-authenticating on every run:
60
75
 
61
76
  ```ts
62
- await mm.interactiveLogin(); // prompts for email, password, MFA code
63
- ```
77
+ // Save token after login
78
+ mm.saveSession(); // writes to .mm/mm_session.json (mode 0o600)
64
79
 
65
- ### Direct token
80
+ // Next time, login() loads the saved session automatically
81
+ await mm.login(email, password); // uses saved token, no network call
82
+
83
+ // Or pass the token directly (skip login entirely)
84
+ const mm = new MonarchMoney({ token: "your-saved-token" });
85
+ ```
66
86
 
67
87
  ```ts
68
- const mm = new MonarchMoney({ token: "your-existing-token" });
88
+ mm.saveSession(); // save to disk
89
+ mm.loadSession(); // load from disk
90
+ mm.deleteSession(); // remove the file
91
+ mm.setToken("..."); // set token programmatically
69
92
  ```
70
93
 
71
- ### Session persistence
94
+ ### Interactive CLI
72
95
 
73
96
  ```ts
74
- mm.saveSession(); // saves to .mm/mm_session.json (mode 0o600)
75
- mm.loadSession(); // loads from disk
76
- mm.deleteSession(); // removes the file
97
+ await mm.interactiveLogin(); // prompts for email, password, email OTP or MFA code
77
98
  ```
78
99
 
79
100
  ## API
80
101
 
81
- All methods return **typed responses** — no `Record<string, unknown>`. Hover over any method in your editor for full JSDoc and type information.
102
+ All methods return **typed responses**. Hover over any method in your editor for full JSDoc and type information.
82
103
 
83
104
  ### Read Methods
84
105
 
@@ -96,6 +117,8 @@ All methods return **typed responses** — no `Record<string, unknown>`. Hover o
96
117
  | `getSubscriptionDetails()` | `GetSubscriptionDetailsResponse` | Plan status (trial, premium, etc.) |
97
118
  | `getTransactionsSummary()` | `GetTransactionsSummaryResponse` | Aggregate summary |
98
119
  | `getTransactions(options?)` | `GetTransactionsResponse` | Transactions with full filtering |
120
+ | `getAllTransactions(options?)` | `Transaction[]` | All matching transactions (auto-paginates) |
121
+ | `getTransactionPages(options?)` | `AsyncGenerator<Transaction[]>` | Async generator yielding pages |
99
122
  | `getTransactionCategories()` | `GetTransactionCategoriesResponse` | All categories |
100
123
  | `getTransactionCategoryGroups()` | `GetTransactionCategoryGroupsResponse` | Category groups |
101
124
  | `getTransactionDetails(id)` | typed response | Single transaction detail |
@@ -131,11 +154,29 @@ All methods return **typed responses** — no `Record<string, unknown>`. Hover o
131
154
 
132
155
  ```ts
133
156
  import {
134
- MonarchMoneyError, // base class for all errors
135
- RequireMFAException, // MFA required — call multiFactorAuthenticate()
136
- LoginFailedException, // bad credentials (includes .statusCode)
137
- RequestFailedException // API/GraphQL failure (includes .statusCode, .graphQLErrors)
138
- } from "monarchmoney";
157
+ MonarchMoneyError, // base class for all errors
158
+ EmailOtpRequiredException, // email verification code needed — call submitEmailOtp()
159
+ RequireMFAException, // TOTP MFA required — call multiFactorAuthenticate()
160
+ LoginFailedException, // bad credentials or auth error (includes .statusCode)
161
+ RequestFailedException, // API/GraphQL failure (includes .statusCode, .graphQLErrors)
162
+ } from "@hakimelek/monarchmoney";
163
+
164
+ try {
165
+ await mm.login(email, password);
166
+ } catch (e) {
167
+ if (e instanceof EmailOtpRequiredException) {
168
+ // e.code === "EMAIL_OTP_REQUIRED"
169
+ // Prompt user for the code sent to their email
170
+ const code = await getCodeFromUser();
171
+ await mm.submitEmailOtp(email, password, code);
172
+ } else if (e instanceof RequireMFAException) {
173
+ // e.code === "MFA_REQUIRED"
174
+ // Prompt for TOTP code or use mfaSecretKey
175
+ } else if (e instanceof LoginFailedException) {
176
+ // e.code === "LOGIN_FAILED", e.statusCode
177
+ console.error("Login failed:", e.message);
178
+ }
179
+ }
139
180
 
140
181
  try {
141
182
  await mm.getAccounts();
@@ -153,34 +194,188 @@ try {
153
194
  ```ts
154
195
  const mm = new MonarchMoney({
155
196
  sessionFile: ".mm/mm_session.json", // session file path
156
- timeout: 10, // API timeout in seconds
157
- token: "pre-existing-token", // skip login
197
+ timeout: 10, // API timeout in seconds
198
+ token: "pre-existing-token", // skip login
199
+ retry: {
200
+ maxRetries: 3, // retry on 429/5xx (default: 3, set 0 to disable)
201
+ baseDelayMs: 500, // base delay with exponential backoff + jitter
202
+ },
203
+ rateLimit: {
204
+ requestsPerSecond: 10, // token-bucket throttle (default: 0 = unlimited)
205
+ },
158
206
  });
159
207
 
160
208
  mm.setTimeout(30); // change timeout later
161
209
  ```
162
210
 
211
+ Retry automatically handles transient failures (429 Too Many Requests, 500, 502, 503, 504) with exponential backoff and jitter. The `Retry-After` header is respected on 429 responses.
212
+
213
+ ## Auto-Pagination
214
+
215
+ `getTransactions()` returns a single page. For large datasets, use the auto-pagination helpers:
216
+
217
+ ```ts
218
+ // Async generator — yields one page at a time (memory-efficient)
219
+ for await (const page of mm.getTransactionPages({ startDate: "2025-01-01", endDate: "2025-12-31" })) {
220
+ for (const tx of page) {
221
+ console.log(tx.merchant?.name, tx.amount);
222
+ }
223
+ }
224
+
225
+ // Or collect everything into a flat array
226
+ const all = await mm.getAllTransactions({
227
+ startDate: "2025-01-01",
228
+ endDate: "2025-12-31",
229
+ pageSize: 100, // transactions per page (default: 100)
230
+ });
231
+ console.log(`${all.length} total transactions`);
232
+ ```
233
+
234
+ Both methods accept the same filter options as `getTransactions()` (date range, category, account, tags, etc.).
235
+
236
+ ## Refresh Progress
237
+
238
+ Track account refresh progress with the `onProgress` callback:
239
+
240
+ ```ts
241
+ await mm.requestAccountsRefreshAndWait({
242
+ timeout: 300,
243
+ delay: 10,
244
+ onProgress: ({ completed, total, elapsedMs }) => {
245
+ console.log(`${completed}/${total} accounts refreshed (${(elapsedMs / 1000).toFixed(0)}s)`);
246
+ },
247
+ });
248
+ ```
249
+
250
+ ## MCP Server (AI Agent Integration)
251
+
252
+ This package includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) server with **30 tools**, making your Monarch Money data accessible to AI assistants like Claude Desktop, Cursor, and any MCP-compatible client.
253
+
254
+ ### Setup
255
+
256
+ 1. Get your Monarch Money auth token by logging in with the library (see [Authentication](#authentication)) and saving `mm.token`.
257
+
258
+ 2. Add to your MCP client config (e.g. Claude Desktop `claude_desktop_config.json`):
259
+
260
+ ```json
261
+ {
262
+ "mcpServers": {
263
+ "monarch-money": {
264
+ "command": "npx",
265
+ "args": ["@hakimelek/monarchmoney"],
266
+ "env": {
267
+ "MONARCH_TOKEN": "your-token-here"
268
+ }
269
+ }
270
+ }
271
+ }
272
+ ```
273
+
274
+ Or run it directly:
275
+
276
+ ```bash
277
+ MONARCH_TOKEN=your-token npx @hakimelek/monarchmoney
278
+ ```
279
+
280
+ ### Available Tools
281
+
282
+ **Read (18 tools):** `get_accounts`, `get_account_holdings`, `get_account_history`, `get_account_type_options`, `get_recent_account_balances`, `get_aggregate_snapshots`, `get_institutions`, `get_budgets`, `get_subscription_details`, `get_transactions`, `get_transactions_summary`, `get_transaction_details`, `get_transaction_categories`, `get_transaction_category_groups`, `get_transaction_tags`, `get_cashflow`, `get_cashflow_summary`, `get_recurring_transactions`
283
+
284
+ **Write (12 tools):** `create_transaction`, `update_transaction`, `delete_transaction`, `create_manual_account`, `update_account`, `delete_account`, `refresh_accounts`, `is_refresh_complete`, `set_budget_amount`, `create_transaction_tag`, `set_transaction_tags`, `create_transaction_category`
285
+
286
+ Every tool has typed parameters with descriptions, so AI agents know exactly what arguments to pass.
287
+
163
288
  ## Project Structure
164
289
 
165
290
  ```
166
291
  src/
167
292
  index.ts — public exports
168
293
  client.ts — MonarchMoney class with all API methods
294
+ mcp.ts — MCP server (30 tools for AI agents)
169
295
  errors.ts — error classes (MonarchMoneyError hierarchy)
170
296
  endpoints.ts — API URL constants
171
297
  queries.ts — all GraphQL query/mutation strings
172
298
  types.ts — TypeScript interfaces for all API responses
173
299
  ```
174
300
 
301
+ ## Testing
302
+
303
+ ```bash
304
+ npm test # run tests once
305
+ npm run test:watch # run tests in watch mode
306
+ npm run test:coverage # run with coverage report
307
+ ```
308
+
309
+ Tests use [Vitest](https://vitest.dev) and do not require real API credentials (fetch is mocked where needed).
310
+
311
+ **Test the API connection** (against the live api.monarch.com):
312
+
313
+ ```bash
314
+ npm run build
315
+
316
+ # Login with email + password (will prompt for email OTP code if required)
317
+ MONARCH_EMAIL=your@email.com MONARCH_PASSWORD=yourpassword npm run test:connection
318
+
319
+ # Use a saved token (skips login)
320
+ MONARCH_TOKEN=your-token npm run test:connection -- --token
321
+ ```
322
+
323
+ Set these in a `.env` file for convenience (see `.env.example`).
324
+
175
325
  ## FAQ
176
326
 
177
327
  **How do I use this if I login to Monarch via Google?**
178
328
 
179
329
  Set a password on your Monarch account at [Settings > Security](https://app.monarchmoney.com/settings/security), then use that password with this library.
180
330
 
331
+ **Why does Monarch ask for an email code every time I login?**
332
+
333
+ Monarch requires email verification for new/unrecognized devices. After login, save the session token with `mm.saveSession()` or store `mm.token` — subsequent runs will reuse it without re-authenticating.
334
+
335
+ ## How This Library Compares
336
+
337
+ There are several unofficial Monarch Money integrations. Here's how `@hakimelek/monarchmoney` stacks up.
338
+
339
+ ### Landscape
340
+
341
+ | | **@hakimelek/monarchmoney** | **monarch-money-api** (pbassham) | **monarchmoney** (keithah) | **monarchmoney** (hammem) |
342
+ |---|---|---|---|---|
343
+ | **Platform** | Node.js / TypeScript | Node.js / JavaScript | Node.js / TypeScript | Python |
344
+ | **npm weekly downloads** | — | ~440 | ~130 | N/A (pip: ~103K/mo) |
345
+ | **Runtime deps** | **1** (speakeasy) | 5 | 7 | 3 |
346
+ | **TypeScript types** | Full (every response) | None | Yes | N/A |
347
+ | **Email OTP flow** | Yes | No | No | No |
348
+ | **MFA / TOTP** | Yes | Yes | Yes | Yes |
349
+ | **Session persistence** | Yes (0o600 perms) | Yes | Yes (AES-256) | Yes |
350
+ | **Interactive CLI login** | Yes | Yes | Yes | Yes |
351
+ | **HTTP client** | Native `fetch` | node-fetch | node-fetch + graphql-request | aiohttp |
352
+ | **Error hierarchy** | 4 typed exceptions | Generic throws | Generic throws | 1 exception |
353
+ | **Read methods** | 20 | 15 | ~20 | ~16 |
354
+ | **Write methods** | 14 | 9 | ~12 | ~10 |
355
+ | **Rate limiting** | Yes | No | Yes | No |
356
+ | **Retry with backoff** | Yes | No | Yes | No |
357
+ | **Auto-pagination** | Yes | No | No | No |
358
+ | **Dual CJS + ESM** | Yes | No | Yes | No |
359
+ | **Refresh progress events** | Yes | No | No | No |
360
+ | **Built-in MCP server** | Yes (30 tools) | No | No | No |
361
+
362
+ ### Where this library wins
363
+
364
+ **Minimal footprint.** One runtime dependency vs 5-7 in the JS/TS alternatives. Native `fetch` means zero HTTP polyfills on Node 18+.
365
+
366
+ **Email OTP support.** Monarch now requires email verification for unrecognized devices, even when MFA is off. This is the only Node.js library that handles the full `EmailOtpRequiredException` → `submitEmailOtp()` flow. Without it, automated scripts break on first login from a new environment.
367
+
368
+ **Typed everything.** Every API response has a dedicated TypeScript interface — 50+ exported types covering accounts, transactions, holdings, cashflow, budgets, recurring items, and mutations. The `monarch-money-api` package has no types at all.
369
+
370
+ **Structured error handling.** Four distinct exception classes (`LoginFailedException`, `RequireMFAException`, `EmailOtpRequiredException`, `RequestFailedException`) with error codes and status codes. Competitors throw generic errors or strings.
371
+
372
+ **Broader write coverage.** Includes `updateTransaction()`, `setBudgetAmount()`, `uploadAccountBalanceHistory()`, `getCashflow()`, `getCashflowSummary()`, and `getRecurringTransactions()` — all missing from `monarch-money-api`.
373
+
374
+ **Clean, flat API.** One class, direct methods, no sub-objects or verbosity levels to learn. Import `MonarchMoney`, call methods, get typed results.
375
+
181
376
  ## Contributing
182
377
 
183
- Contributions welcome. Please ensure TypeScript compiles cleanly (`npm run build`).
378
+ Contributions welcome. Please ensure TypeScript compiles cleanly (`npm run build`) and tests pass (`npm test`).
184
379
 
185
380
  ## License
186
381