In short
The author of the millfolio project explained how to set up batch processing of financial records using an on-device model. The key idea is to compute the model’s predictions once and store the result, rather than invoking the AI with every user request.
The millfolio project uses a two-tier architecture: the front-end model generates a small program based on the schema, while the local model reads the user’s actual files directly on the device. The main problem is that the local model does not scale with the volume of data. It’s impossible to run thousands of transactions accumulated over a couple of years through AI for every query: even a fraction of a second per record turns into minutes of inference, and the next query starts the whole process over again.
A model’s prediction is something that is calculated once and saved, not run at the moment of a query. All tags are assigned during the indexing phase and stored alongside the records.
Tags are defined in a plain text file named categories.txt—one rule per line; this file is the single source of truth. The types are ordered by cost:
@tag. The rule applies if the specified tag is already present in the string. Cycles are safe: tags are only added, so the computation converges.At the time of the request, all three types look the same: the generated program filters by .tags, and the response for thousands of records boils down to comparing strings rather than calling the model.
The front-end model is provided with tag names and notes on the scope of application, but not keywords. The actual merchant strings remain on the device.
AI tags must be computed at least once—this involves backfilling all existing records plus new arrivals. The work is managed by an on-device orchestrator: a disk-based queue that survives restarts and processes one task at a time. Indexing and backfilling AI tags do not compete with each other, but both take a back seat to interactive queries.
Within a task, classification is batch-processed and deduplicated:
1: yes, 2: no, …—a single call classifies an entire batch;A small log tracks how much of each rule has already been applied, linked to the monotonically increasing insertion sequence. The finished tag never re-processes old records—new ones are processed incrementally. Between batches, the backfiller “sleeps,” and the length of the pause serves as a priority setting.