This project automates (so you don’t have to click through portals every day):
- Daily balance updates for each loan group (AA/AB/1-01/…)
- Payment-posted transactions in Monarch (one per loan group allocation), categorized as Transfer
It’s designed to run unattended (Docker/Unraid), with email MFA handled via Gmail IMAP + App Password. 🤖📬
Shoutout to the maintainers of the unofficial Monarch Money Python library — their work made the Monarch-side integration possible. 🙌
This guide is linear: Prereqs → Configure → Choose a runtime (Python or Docker) → Preflight → Dry-run → Run.
At a high level, each scheduled run:
- Logs into your StudentAid servicer portal with Playwright 🔐
- Handles MFA via email (Gmail IMAP) when prompted 📬
- Scrapes current balances per loan group (AA/AB/1-01/…) and recent payment allocations
- Updates Monarch:
- Balances: updates your mapped manual accounts
- Payments: creates transactions (one per allocation) ✅
- Records a small local history DB (
data/state.db) so it won’t duplicate payments across runs 🧾
- Monarch auth (choose one):
- Cookie auth (preferred): run
bootstrap-monarch-authonce to savedata/monarch_session.picklefrom a browser login, or setMONARCH_COOKIE_STRINGtosession_id=...; csrftoken=....- If you prefer split env vars, set
MONARCH_COOKIE_SESSION_ID+MONARCH_COOKIE_CSRFTOKEN.
- If you prefer split env vars, set
- Email/password option: set
MONARCH_EMAIL+MONARCH_PASSWORDif your Monarch account uses password login.- If you currently use Google / “Continue with Google” to access Monarch, you’ll need to set a password in Monarch → Settings → Security first.
- Cookie auth (preferred): run
- The first successful Monarch login writes
data/monarch_session.pickle; later runs reuse that saved session until it expires.- To test the saved-session path by itself, temporarily clear
MONARCH_COOKIE_STRINGandMONARCH_COOKIE_SESSION_ID/MONARCH_COOKIE_CSRFTOKENfrom.env, then runpreflightorsync --dry-run --dry-run-check-monarch.
- To test the saved-session path by itself, temporarily clear
- Gmail IMAP for MFA
- You’ll need IMAP enabled + 2‑Step Verification + a Google App Password.
- If you want the step-by-step Gmail label/filter setup (recommended), see Gmail IMAP setup 🏷️
Copy env.example → .env and fill in values (Monarch + Gmail IMAP + your loan groups).
✅ For most users, .env is the only file you need to edit.
Required:
SERVICER_PROVIDER,SERVICER_USERNAME,SERVICER_PASSWORDLOAN_GROUPS(comma-separated:AA,AB,...or1-01,1-02,...)- Monarch auth (
MONARCH_COOKIE_STRING, split cookie vars, orMONARCH_EMAIL+MONARCH_PASSWORD) - Gmail IMAP (
GMAIL_IMAP_USER+GMAIL_IMAP_APP_PASSWORD)
Advanced (optional):
config.example.yamlis an advanced override file. You can pass--config config.example.yaml, but most users don’t need YAML at all.
Tip: If you’re not sure what to put in LOAN_GROUPS, you can have the tool log into your servicer portal and list what it discovers:
docker compose run --rm --build studentaid-monarch-sync list-loan-groupsor (Python):
.venv/bin/python -m studentaid_monarch_sync list-loan-groups --headfulTip: list common provider slugs (non-exhaustive):
docker compose run --rm --build studentaid-monarch-sync list-servicersBefore the first real sync, run this once to create/map one Monarch manual account per loan group and save a stable mapping under data/ (so later account renames won’t break anything).
- Docker (recommended):
docker compose run --rm --build studentaid-monarch-sync setup-monarch-accounts --apply- Python:
.venv\Scripts\python -m studentaid_monarch_sync setup-monarch-accounts --applyAfter this, your scheduled runs can just execute sync.
Pick one of the following and run it end-to-end. For most people (especially NAS/Unraid), Docker is the recommended option 🐳✅
This repo includes a docker-compose.yml service that runs the sync as a run-once container.
- Install Docker Desktop and make sure
docker composeworks in a terminal. - From the repo folder, create the persistent
data/folder (stores logs, SQLite state, and Playwright session):
mkdir data- Preflight (inside the container):
docker compose run --rm --build studentaid-monarch-sync preflight- Dry-run only:
docker compose run --rm studentaid-monarch-sync sync --dry-run --payments-since 2025-01-01- Dry-run + read-only Monarch duplicate check:
docker compose run --rm studentaid-monarch-sync sync --dry-run --dry-run-check-monarch --payments-since 2025-01-01- Run for real (writes to Monarch):
docker compose run --rm studentaid-monarch-sync sync --payments-since 2025-01-01Docker Desktop doesn’t include a built-in scheduler. The usual pattern is: use your host OS scheduler to run the sync command. To keep the scheduled command simple (and easy to update later), you can schedule a small wrapper script from this repo.
- Windows (Task Scheduler):
- Create a task that runs daily or weekly.
- Program/script:
C:\Program Files\PowerShell\7\pwsh.exe(orpowershell.exe) - Add arguments (example):
-NoProfile -File .\scripts\docker_sync.ps1 run --payments-since 2025-01-01
-
Start in:
C:\path\to\repo(the folder that containsdocker-compose.yml) -
macOS (launchd):
- Create a LaunchAgent that runs:
cd /path/to/repo && bash ./scripts/docker_sync.sh run --payments-since 2025-01-01- Linux (cron/systemd):
- Use cron or a systemd timer to run the same command on your desired timeframe:
cd /path/to/repo && bash ./scripts/docker_sync.sh run --payments-since 2025-01-01- Put the repo on persistent storage (or copy just
docker-compose.yml,.env, and createdata/). - Create the persistent
data/folder:
mkdir -p data- Test-run once (recommended):
bash ./scripts/docker_sync.sh setup-accounts
bash ./scripts/docker_sync.sh preflight
bash ./scripts/docker_sync.sh dry-run --payments-since 2025-01-01- Schedule it (e.g., Unraid User Scripts plugin) with a daily command like:
cd /path/to/repo && bash ./scripts/docker_sync.sh run --payments-since 2025-01-01Keep ./data persistent so sessions and the SQLite idempotency DB survive restarts.
Requires Python 3.10+ now, because the updated Monarch dependency needs it.
Install
- Windows:
python -m venv .venv
.venv\Scripts\python -m pip install -r requirements.txt
.venv\Scripts\python -m pip install -e .
.venv\Scripts\python -m playwright install chromium- Linux / macOS:
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -m pip install -e .
.venv/bin/python -m playwright install chromium(Optional) Run unit tests 🧪
.venv/bin/python -m pytest -qPreflight (fast fail, no Playwright) ⚡
.venv\Scripts\python -m studentaid_monarch_sync preflightDry-run (recommended first run) 🧪
.venv\Scripts\python -m studentaid_monarch_sync sync --dry-run --headfulDry-run + read-only Monarch check
.venv\Scripts\python -m studentaid_monarch_sync sync --dry-run --dry-run-check-monarch --headful --payments-since 2025-01-01 --max-payments 1Run for real (writes to Monarch) ✅
.venv\Scripts\python -m studentaid_monarch_sync sync --payments-since 2025-01-01 --max-payments 10(Optional) Schedule it on Windows 🗓️ If you want this to run daily on Windows, use Task Scheduler:
- Open Task Scheduler → Create Task…
- General:
- Name:
Monarch Student Loan Sync - Select Run whether user is logged on or not
- Check Run with highest privileges (helps with browser automation)
- Name:
- Triggers:
- New… → Daily (pick your time)
- Actions:
- New… → Start a program
- Program/script:
C:\path\to\repo\.venv\Scripts\python.exe - Add arguments:
-m studentaid_monarch_sync sync --payments-since 2025-01-01
- Start in:
C:\path\to\repo
- Settings (recommended):
- Allow task to be run on demand
- If the task fails, restart every: 5 minutes (up to a few times)
Tip: run the exact command once in PowerShell first to confirm it works before scheduling.
- Logs into your servicer portal (typically
https://{provider}.studentaid.gov) with Playwright - Browser sessions use browser-compatibility defaults (automation flags disabled, realistic viewport/locale/user-agent, randomized interaction delays) to reduce anti-automation detection
- When the portal prompts for MFA, selects Email, then polls Gmail IMAP for the code
- Scrapes per-loan balances + payment allocation details
- Non-posted payments (pending, scheduled, processing, cancelled) are automatically detected and skipped — only fully posted payments are synced
- Pushes updates into Monarch via the unofficial Monarch API client
- Stores a small SQLite state DB so runs are idempotent (no duplicate payment transactions)
- Includes an extra duplicate guard against Monarch itself: date + amount + merchant (so even if you reset SQLite, we won’t spam duplicates).
- Optional: you can enable a more specific duplicate check that also uses the portal’s payment confirmation/reference as a search term (see Config notes (Monarch payments)).
This makes MFA automation reliable and keeps old/stale codes out of your inbox.
-
Enable IMAP in Gmail
- Gmail → Settings (gear) → See all settings → Forwarding and POP/IMAP → Enable IMAP → Save.
-
Enable 2‑Step Verification
- Google Account → Security → 2‑Step Verification → turn it on.
-
Create a Google App Password
- Google Account → Security → App passwords
- Choose Mail (or “Other”) and generate an app password.
- Put the generated 16‑character password into
.envasGMAIL_IMAP_APP_PASSWORD. - If you don’t see “App passwords”, it usually means 2‑step isn’t enabled yet or your account/admin policy forbids it.
-
Create a Gmail label/folder (optional but strongly recommended)
- Gmail sidebar → Labels → Create new label
- Example:
StudentAid MFA - Set
.envGMAIL_IMAP_FOLDERto that label name (Gmail exposes labels as IMAP folders).- Nested labels may look like
StudentAid/MFA.
- Nested labels may look like
-
Create a Gmail filter to auto-label MFA emails
- Gmail search bar → Show search options → create filter.
- Suggested filter (broad, works across servicers):
- From:
studentaid.gov - Subject:
code
- From:
- Actions:
- Apply the label:
StudentAid MFA - (Optional) Never send it to Spam
- Apply the label:
-
Recommended
.envvaluesGMAIL_IMAP_SENDER_HINT:studentaid.gov(or a specific one like<provider>.studentaid.gov)GMAIL_IMAP_SUBJECT_HINT:code(broad; subjects vary by servicer)- Example sender we’ve seen:
NoReply@<provider>.studentaid.gov- Other servicers are likely similar (
<something>@<provider>.studentaid.gov), but we don’t rely on that—use the hints above.
- Other servicers are likely similar (
Standard dry-run does not call Monarch (it only prints what it would do based on the portal + local SQLite). If you want to validate the real behavior end-to-end (including the duplicate guard), run:
.venv\Scripts\python -m studentaid_monarch_sync sync --dry-run --dry-run-check-monarch --headful --payments-since 2025-01-01 --max-payments 1This logs into Monarch in read-only mode and prints each payment allocation as:
SKIP (duplicate)if Monarch already has a txn with the same date + amount + merchantCREATEif it would create a new txn
If a sync run fails, the CLI will automatically create a zip under data/ containing:
data/debug/*(screenshots/HTML/text snapshots)- your configured log file (default:
data/sync.log)
The log file now includes the full exception traceback for run failures, so you can see the exact line and error without needing to reproduce the issue interactively.
You can attach that zip when asking for help.
Debug bundles include *.txt snapshots of the portal’s rendered page text. You can parse these offline into JSON:
python3 scripts/parse_portal_text_snapshot.py loans --groups AA,AB --file data/debug/loan_details_not_loaded.txt
python3 scripts/parse_portal_text_snapshot.py payments --file data/debug/payment_detail_0_error.txtSee Quick start → Runtime A: Docker (recommended) for the Unraid scheduling command and persistence notes.
- StudentAid servicer portals are web portals; UI changes may break selectors. The code is structured so selectors live in one place, and failures should save screenshots/logs for debugging.
- Email MFA automation is sensitive—use a dedicated mailbox if possible.
- Non-posted payment handling: payments with status pending, scheduled, processing, or cancelled are automatically skipped. Only fully posted (electronic/regular) payments produce Monarch transactions. If a single row fails to parse, it is skipped with a warning rather than aborting the whole run.
- Anti-automation / 403 detection: if the portal returns an HTTP 403 Access Denied (common in headless runs on some servicers), the tool detects it immediately, saves a debug snapshot, and retries once with a fresh session. If it persists, see the 403 troubleshooting entry below.
- State self-heal:
data/state.db(SQLite idempotency DB) is backed up todata/state.db.bak. Ifstate.dbis corrupted/unreadable, the script will restore from the backup (or recreate it if no backup exists).data/servicer_storage_state_*.json(Playwright cookies/localStorage) is backed up to*.bak. If it becomes invalid JSON, the script quarantines it and falls back to a fresh session.- Even if SQLite is lost, the Monarch-side duplicate guard (date + amount + merchant) prevents duplicate payment transactions.
- Monarch auth failed / 401
- Re-copy
MONARCH_COOKIE_STRINGfrom an authenticated Monarch browser request. - If
data/monarch_session.pickleis stale, delete it and retry preflight. - If you want a quick read-only check before a real run, use
sync --dry-run --dry-run-check-monarch.
- Re-copy
- MFA email not found
- Confirm Gmail IMAP is enabled and
GMAIL_IMAP_FOLDERmatches the label name. - Set broad hints:
GMAIL_IMAP_SENDER_HINT=studentaid.govandGMAIL_IMAP_SUBJECT_HINT=code. - Make sure the filter applies the label and the email isn’t in Spam.
- Confirm Gmail IMAP is enabled and
- HTTP 403 Access Denied / portal blocks the headless browser
- Some servicers (notably Nelnet) occasionally return a bare
HTTP 403 Access Deniedpage to headless browsers that look like automation. The tool detects this and retries once with a fresh session automatically. - If it keeps failing, try the following in order:
- Run
sync --headful --manual-mfaonce to establish a fresh, trusted browser session stored underdata/servicer_storage_state_*.json. Subsequent headless runs reuse that session. - Add
--fresh-sessionto force discarding any stale stored session before retrying. - If running in Docker on a cloud/datacenter host, try from a residential IP address — some portal WAFs block datacenter IP ranges.
- Run
- A
data/debug/access_denied_403_*.pngscreenshot is saved automatically so you can confirm what the portal returned. - See also GitHub issue #9 for community discussion.
- Some servicers (notably Nelnet) occasionally return a bare
monarch.payment_merchant_name: merchant name to use when creating payment transactions, and the value used by the duplicate guard.- If your existing loan-account payments show up as US Department of Education, set this to that (recommended).
monarch.duplicate_guard_use_reference/MONARCH_DUPLICATE_GUARD_USE_REFERENCE(optional): when the portal provides a payment confirmation/reference, use it as a Monarch search term during duplicate detection to reduce false positives for same-day identical payments.
This is the most reliable way to bootstrap Monarch auth now.
If you do not want to copy cookie values at all, run:
.venv/bin/python -m studentaid_monarch_sync bootstrap-monarch-authIt opens Monarch in a browser, waits for you to log in once, then saves a reusable data/monarch_session.pickle.
- Log into Monarch in your browser normally.
- Open DevTools → Network tab.
- Click any authenticated request to Monarch’s API (often
graphql). - In Request Headers, copy the cookie value after
Cookie:. Do not include theCookie:label itself. - Minimum needed cookies are
session_idandcsrftoken. Extra cookies likecf_clearance,__cf_bm,ajs_*, andosano_*are not used by this project. - Put the copied cookie string into
.envasMONARCH_COOKIE_STRING=session_id=...; csrftoken=...(keep it secret).- Or split them:
MONARCH_COOKIE_SESSION_ID=...+MONARCH_COOKIE_CSRFTOKEN=...
- Or split them:
Run -h at any time to see the full help:
.venv\Scripts\python -m studentaid_monarch_sync -h
.venv\Scripts\python -m studentaid_monarch_sync sync -h--env-file: Path to a dotenv file (default:.env). If present, it will be loaded before reading config/env vars.
--config: Path to YAML config (optional; default:config.yamlif you use one).--dry-run: Do not write to Monarch. Prints intended balance updates + intended payment transactions.--dry-run-check-monarch: Only meaningful with--dry-run. Logs into Monarch read-only and printsSKIP (duplicate)vsCREATEusing the duplicate guard (date + amount + merchant).--headful: Run Playwright with a visible browser window (useful for debugging / monitoring).--fresh-session: Do not reuse the stored browser session (cookies/localStorage). Useful if the portal gets into weird redirects (e.g.dark.<servicer>.studentaid.gov).--manual-mfa: In headful mode, pause and let you enter the MFA code manually in the browser (safer while debugging).- Requires
--headful.
- Requires
--print-mfa-code: Print the full MFA code to stdout (debug).- Requires
--headful. Avoid using this in unattended/logged environments (e.g., containers) since stdout is typically collected.
- Requires
--slowmo-ms: Playwright “slow motion” delay (ms) applied to browser actions (debug).--step-debug: Save step-by-step screenshots underdata/debug/to tighten selectors and understand failures.--step-delay-ms: Extra delay (ms) after each step screenshot in step-debug mode (so you can watch the browser).--max-payments: Max payment detail entries to scan from Payment Activity (default: 10).--payments-since: Only consider payments on/after this date (YYYY-MM-DD). Also used to stop scanning older payment history early (faster runs).
--config: Path to YAML config (optional; default:config.yamlif you use one).