The guide
Run it
Empty book to reconciled first statement, to the writer running locally, to always-on in your own cloud. Every command below is copy-paste correct.
Onboard: empty book to reconciled
A fresh book boots green and empty. Accounts arrive by evidence — connect a rail or drop a statement, and the book proposes what it found for you to complete and settle. The Getting Started checklist computes its progress from the book itself; no separate onboarding state.
Create the book. The template folder is a book: boot the writer against it (see below) and the empty state offers two entry points — connect a rail, or drop a document.
Bring in a source. Connecting a rail or dropping a statement creates pending accounts with model-proposed config. Complete them and settle; manual creation exists but is the fallback.
Complete each discovered account with two things:
- A history horizon — the earliest document,
a date you choose, or today. Sets the rail's
sinceand what an opening entry must cover. - An opening balance from the first statement. Its
printed opening figure becomes the
openingtransaction; its printed closing balance becomes the first anchor. Investment accounts open one transaction per lot, basis declared when known, refinable later.
Settle the first batch. A clean statement settles in one tap. Because the opening came from the statement's own printed figure and its closing balance is the first anchor, the account reconciles the moment that first batch lands.
History deepens later. Dropping an earlier document replaces the opening entry with real rows and a new opening at the earlier horizon, gated by a conservation check. Extending backward is normal intake, not a special operation.
See how a row is admitted for what runs underneath.
Run locally
The same writer that runs in the cloud runs on your laptop — only the environment differs. You need uv (Python 3.12+) and Node for the UI build.
Install. From the engine directory, sync the pinned
dependencies and build the UI bundle once (it lands in ui/dist,
where the writer looks for it):
uv sync
(cd ui && npm ci && npm run build)
Create your book from the template — it's a git repo you own, so initialize it and make the first commit:
cp -R template ~/mybook
git -C ~/mybook init \
&& git -C ~/mybook add -A \
&& git -C ~/mybook commit -m "my book from template"
Set the environment and serve. Generate the owner token once and keep it — the API and the PWA both authenticate with it:
export MINTMARK_BOOK_PATH="$HOME/mybook"
export MINTMARK_API_TOKEN=$(openssl rand -hex 32)
echo "$MINTMARK_API_TOKEN"
export CLAUDE_CODE_OAUTH_TOKEN=… # your Claude OAuth token
uv run mintmark serve
The UI is at http://127.0.0.1:8000/; the API lives under
/api and expects Authorization: Bearer
$MINTMARK_API_TOKEN. Commits the writer makes land in
~/mybook — git push from there (or add a
remote) is your backup and sync.
The environment that matters
| Variable | When | Meaning |
|---|---|---|
| MINTMARK_BOOK_PATH | required | Path to the book checkout the writer owns. |
| MINTMARK_API_TOKEN | required | Single-owner bearer token; every /api call and the PWA
present it. The writer refuses to serve without it. |
| CLAUDE_CODE_OAUTH_TOKEN | for the agent | Claude subscription OAuth token — never an API key. Absent, the model tasks (tagging, grouping, chat, drafting an extractor) are unconfigured; the rest of the book still runs. |
| MINTMARK_RAIL_<RAIL_ID>_<NAME> | per rail | One credential per connected feed, the id and name upper-cased with
hyphens turned to underscores. A SimpleFIN rail main needs
MINTMARK_RAIL_MAIN_ACCESS_URL. A missing value marks that rail
unconfigured and names what it wanted. |
| MINTMARK_HOST | optional | Listen address. Defaults to 127.0.0.1; set
0.0.0.0 to reach the writer from another device. |
The PWA on your phone
The UI installs as a PWA. To reach the writer on your laptop from your phone, serve on all interfaces and browse to your machine's LAN address:
export MINTMARK_HOST=0.0.0.0
uv run mintmark serve
# find your machine's address, e.g. on macOS:
ipconfig getifaddr en0
On the phone, open http://<that-address>:8000/ over the
same network, Add to Home Screen, and paste the API token when asked. It is
the same engine and the same bundle — only the listen address changed.
Prefer the container? The engine ships as one image
(docker build -t mintmark:latest .) that runs
mintmark serve as its default command; mount your book at
/book and pass the same variables with -e. That is
exactly the shape the cloud deployment uses next.
Deploy your own
The writer owns one working checkout. Put it on your own infrastructure and point your phone at it. The image carries no credentials and no book data — every deployment detail is an environment variable, and the book is a git repo you control. The shape is host-agnostic; below is one concrete path, Cloud Run.
The shape. Build, push to a registry, map secrets into the environment, deploy. On an ephemeral platform the book is cloned on cold start from a remote you control, and the invariant is at most one writer — never two, though the platform may run zero between uses.
Build, push, and deploy the writer. The load-bearing
flag is --max-instances=1 — the writer lock, so the
platform never runs two writers against one book. The reference path scales
to zero: the writer sleeps between uses and wakes in a few seconds on the
first request, and an always-allocated instance bills ~$45–50/month
doing nothing against ~$1–3 for a personal book — so warmth is
opt-in, not the default. Substitute your own project and region (this
assumes the Artifact Registry repo already exists — see the reference
below):
PROJECT=your-gcp-project
REGION=us-west1
REPO=$REGION-docker.pkg.dev/$PROJECT/mintmark
IMAGE=$REPO/mintmark:latest
docker build -t "$IMAGE" . \
&& docker push "$IMAGE"
gcloud run deploy mintmark \
--image="$IMAGE" \
--region="$REGION" \
--project="$PROJECT" \
--max-instances=1 \
--port=8000 \
--set-env-vars="\
MINTMARK_BOOK_PATH=/book,\
MINTMARK_HOST=0.0.0.0,\
MINTMARK_PORT=8000" \
--set-secrets="\
MINTMARK_API_TOKEN=mintmark-api-token:latest,\
CLAUDE_CODE_OAUTH_TOKEN=claude-oauth-token:latest,\
MINTMARK_BOOK_REMOTE=mintmark-book-remote:latest"
MINTMARK_BOOK_REMOTE is a git URL to a private book repo you
own; the entrypoint clones it on cold start. Authenticate it with a scoped
token embedded in the URL or a mounted deploy key — any git remote
works. Once it's up, open the service URL on your phone, install the PWA from
/, and supply the API token. Same engine, same bundle; only the
environment changed.
Schedule the rails. Scaled to zero, the writer polls nothing on its own — each rail's schedule becomes an external Cloud Scheduler job that pings the run-rail endpoint on its cadence, and that ping is the wakeup. The cadence is unchanged; the scheduler is what fires it.
If you'd rather not cold-start, add
--min-instances=1 --no-cpu-throttling back and pay for the
warmth — the writer behaves identically either way.
The full command-by-command reference — creating the Artifact
Registry repo, minting each secret with gcloud secrets create,
and adding one --set-secrets entry per rail — lives in
docs/DEPLOY.md in the engine repo. This page teaches the shape;
that file is the exact path.
← How the system works · Why it's built this way · Source on GitHub →