mintmark

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.

CreateA book from the template — green and empty.
Connect or dropA rail feed, or a statement PDF.
CompleteFinish each discovered account.
SettleOne tap clears a clean batch.
ReconciledBalances hit the statement's anchor.

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 since and what an opening entry must cover.
  • An opening balance from the first statement. Its printed opening figure becomes the opening transaction; 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

VariableWhenMeaning
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 →