fintual-api is a worker that logs into Fintual, fetches investment performance data, and imports the resulting variation transactions into Actual Budget. In its default mode it runs the sync on a cron schedule from inside the container process; a one-shot mode remains for manual diagnostics.
The repo intentionally supports only these flows:
pnpm scheduleruns the sync on the configured cron schedule whenRUN_MODE=scheduleis set (the container default)pnpm onceruns the full sync once- unattended 2FA retrieval via Gmail IMAP + app password
- Node.js 24+
- pnpm
- hk 2.0.1+ (pinned in
mise.tomlfor mise users) - Fintual credentials
- Actual Budget server credentials
- Gmail app password for unattended 2FA
- Install dependencies:
pnpm install- Install hk 2.0.1+ (or run
mise installto use the pinned version), then enable the repository's Git hooks. With Git 2.54+, the recommended one-time setup is:
hk install --globalFor an installation scoped to this clone instead, run hk install.
If mise manages hk, add --mise to either install command so Git resolves
the repository's tool version. When upgrading from hk v1, update any
.mise.local.toml override and rerun the install command. See the
hk v2 upgrade guide.
- Create a local env file:
cp .env.example .env- Fill in your Actual, Fintual, and Gmail values.
Enable 2-Step Verification in Google Account settings, then create a Gmail app password for the mailbox used to receive Fintual 2FA emails.
- Open the 2-Step Verification page (you only need to do this once): 2-Step Verification
- Open App Passwords directly: App Passwords
- Create an app password for any label (for example:
fintual-api), then copy the generated 16-character password. - Paste it into
.envasGMAIL_APP_PASSWORD.
If you prefer a quick copy/paste terminal flow:
read -s "GMAIL_APP_PASSWORD?Paste Gmail app password: "; echo
cat >> .env <<EOF
GMAIL_USER_EMAIL=your@gmail.com
GMAIL_APP_PASSWORD=$GMAIL_APP_PASSWORD
GMAIL_IMAP_HOST=imap.gmail.com
GMAIL_IMAP_PORT=993
EOF
unset GMAIL_APP_PASSWORDSet these values in .env (or your runtime secret manager):
GMAIL_USER_EMAILGMAIL_APP_PASSWORDGMAIL_IMAP_HOST(default:imap.gmail.com)GMAIL_IMAP_PORT(default:993)
fintual-api polls IMAP over TLS and extracts the 6-digit code from matching emails.
Use gcloud CLI to store and retrieve the Gmail app password without committing it:
gcloud secrets create fintual-gmail-app-password --replication-policy=automatic
printf '%s' "$GMAIL_APP_PASSWORD" | gcloud secrets versions add fintual-gmail-app-password --data-file=-
gcloud secrets versions access latest --secret=fintual-gmail-app-passwordExample to materialize a local .env value from Secret Manager:
echo "GMAIL_APP_PASSWORD=$(gcloud secrets versions access latest --secret=fintual-gmail-app-password)" >> .envOptionally type-check the project, then run the sync directly from the TypeScript source:
pnpm typecheck
pnpm onceThe worker will:
- log in to Fintual over HTTP (
initiate_login→ Gmail IMAP 2FA when required →finalize_login_web) and fetch GraphQL performance data - fold the data into a Performance Snapshot, save it to
tmp/fintual-data/balance-2.jsonfor inspection, and pass it to the Actual sync step - import variation transactions into Actual Budget
To capture login and GraphQL traffic for analysis (HAR), use agent-browser ≥ 0.22 and run:
pnpm capture:harDetails and observed endpoints are in docs/fintual-http-capture.md. Output goes to tmp/fintual-capture.har (gitignored).
Fallow runs as a strict zero-debt gate: a changed-file audit plus one combined
project-wide dead-code, duplication, and health scan, all enforced
locally, in git hooks, and in CI. See docs/fallow.md.
hk keeps local commits and pushes aligned with CI:
commit-msgrequires commit subjects to follow the Conventional Commits format.pre-commitchecks staged TypeScript with Oxfmt and Oxlint in parallel plus a fast Fallow audit of the staged diff. Safe fixes are applied and re-staged while unstaged changes are temporarily stashed.pre-pushchecks the files being pushed with Oxfmt and Oxlint while running the full TypeScript, coverage, test, and Fallow gates in parallel.
Run the hooks explicitly when needed:
hk run commit-msg .git/COMMIT_EDITMSG
hk run pre-commit
hk run pre-push
hk check --allhk fix applies fixes without staging them; use hk fix --stage to stage
the fixed files. The pre-commit hook continues to stage fixes automatically.
Breaking change in the v3 major: the image default command changed from a one-shot sync (exit after running once) to a long-lived scheduler process (run until interrupted). The previous invocation contract is not preserved and there is no retro-compatibility shim. See Release guidance.
The published container image runs the in-process cron scheduler by default. The worker runs the synchronization at the configured schedule:
docker run --rm --env-file .env docker.io/samaluk/fintual-api:latestSet RUN_MODE=once for a one-shot diagnostic run:
docker run --rm --env-file .env -e RUN_MODE=once docker.io/samaluk/fintual-api:latestThe schedule comes from environment configuration:
SYNC_CRONdefaults to0 0 22 * * 1-5SYNC_TIMEZONEdefaults toAmerica/SantiagoSYNC_NO_OVERLAP=trueskips a tick while a previous run is still in progress
Mount ./tmp if you want to inspect the generated files locally:
docker run --rm --env-file .env -v "$(pwd)/tmp:/app/tmp" docker.io/samaluk/fintual-api:latestThe local compose file keeps the worker container idle so you can run the sync manually with the exact same Docker environment each time:
docker compose --env-file .compose.env up -d --build
docker exec -it fintual-api-local pnpm onceIf you keep runtime secrets in Secret Manager, fetch GMAIL_APP_PASSWORD at deploy time and inject it into the homelab runtime env instead of storing it in compose files.
Useful commands while debugging:
docker logs -f fintual-api-local
docker exec -it fintual-api-local sh
docker exec -it fintual-api-local pnpm once
docker compose --env-file .compose.env downThis repo publishes docker.io/samaluk/fintual-api from GitHub Releases.
Repository secrets required for publishing:
DOCKERHUB_USERNAMEDOCKERHUB_TOKEN
Published tags:
sha-<commit>latestfrom GitHub Releases- the GitHub Release tag itself, such as
v1.0.0
The intended production model is:
- GitHub Actions publishes the worker image to Docker Hub
- the homelab compose stack pulls the image
- a long-lived worker runs the in-process scheduler and keeps secrets in its runtime environment
- Komodo deploys compose changes from the homelab repo
The scheduler mode is the container default. Configure the sync time with
SYNC_CRON and SYNC_TIMEZONE (for example, 0 0 22 * * 1-5 in
America/Santiago), and keep RUN_MODE=once available for manual
docker exec diagnostics.
The scheduler stops cleanly when the process is interrupted, including SIGTERM,
and each in-flight run's scoped resources are closed. Configure the container
restart policy (for example, restart: unless-stopped) so the worker comes back
after a host restart.
For homelab deployments, store GMAIL_APP_PASSWORD in your secret manager (for example, GCP Secret Manager) and inject it into the worker environment at runtime.
Starting with the v3 major, the image default command runs the in-process scheduler instead of a one-shot sync. This is a breaking change to the container process contract:
- The default command (
pnpm schedule) no longer exits after one sync. It runs the synchronization on the configuredSYNC_CRONschedule until the process is interrupted (for example, SIGTERM). - The previous invocation contract is not preserved: there is no retro-compatibility shim, and images published under the v3 major and later do not accept the old invocation.
- One-shot invocations remain available explicitly via
RUN_MODE=onceorpnpm once, for manualdocker execdiagnostics and CI-style runs.
Publish the breaking change under a new major version tag (for example, v3.0.0);
do not publish it under an existing v2 tag, because latest follows GitHub
Releases and old tags must keep the previous behavior.