The original concept: one person's finances as one market
PFEx stands for Personal Financial Exchange. Before the first line of code I had already written the whole setup: accounts as securities, balances as prices, net worth as the index, and a full page of things I chose not to build.
I did not start with "let's build a quick expense tracker". I started with a name, and the name already carried the requirements.
The formal name is 个交所, glossed as "个人金融交易所" (Personal Financial Exchange), in English PFEx. The word formation is the one the Shanghai and Shenzhen exchanges use. The slogan: "一个人,也有大盘。" / "One person, one market." The tone I fixed in a single line: the data wears a suit, the copy wears slippers.
A different question to answer
A conventional bookkeeping app answers "where did my money go" and hands you a transaction list plus a pie chart. I wanted this one to answer something else:
Is my money, as a whole, up or down right now? What is pushing it up? What is dragging it down? Where does it come from, and where does it flow?
A pie chart cannot answer that. The stock market already owns a vocabulary everyone reads without training: candles, volume, moving averages, drawdown, the dragon-tiger board. So my first move was to restate personal finance in that vocabulary.
| Real finance | The exchange world |
|---|---|
| Spending account | A listed security |
| Account balance | Share price |
| Each transaction | A trade |
| Total net worth | The market index |
| External income / expense | Buy (good news) / sell (bad news) |
| Internal transfer | Rebalancing |
| Borrow / repay | Margin financing / short covering |
| Balance calibration | Audit adjustment |
| Monthly recurring budget | ETF |
| One-off earmarked budget | LOF |
| Queue of incomplete entries | Pending order |
| Rule that books on a cycle | Standing order |
| The user's own ledger | The brokerage branch |
That table is not decoration I put on the UI. I wrote the data model against it. TransactionType has seven values, and I made each one declare three separate things: whether it moves net worth, which account it moves, and whether it counts as volume. Calibration is an audit adjustment, so it contributes no volume. A rebalance creates no gain, so net worth is unchanged. Once the rules live in the enum, no screen has to re-explain them.
The whole setup existed before the code. The first commit (8 October) was not a minimal tracker: it landed 464 files in one go, routes, tables and engines among them.
Define the arithmetic before the features
An account candle is a balance candle, and I defined both ends precisely. Open is the balance after the first transaction of the day, close is the balance after the last one, high and low are the day's balance extremes, and volume is the sum of absolute transaction amounts excluding calibration and internal transfers. Moving averages are MA5 and MA20 over the close series.
The index is net worth: the sum of all account balances, with negative balances counted as negative. The index candle is the candle of that series, under the same rules.
I argued with myself here and the refusal won. No base period, no indexation, no invented point system, no second set of prices. What I settled on: if the balance is big, it is big; being real is enough. Percentage change is today close / yesterday close - 1, and when the base is zero or negative I drop the percentage and show only the absolute change, because a percentage would be meaningless there.
Account statuses are the sharpest edge
Accounts carry a listing status like a stock: N (newly listed, within 7 days of creation), normal, ST (special treatment), *ST (delisting risk), suspended (no transactions for 30 consecutive days), delisted (archived).
The hard part is the threshold. Salary arrives at the start of the month and rent leaves at the end; that is a normal cycle. If I used the initial balance or the all-time high as the baseline, the app would cry risk every month. So I made the baseline move: the average of the absolute month-end balances over the previous three months, with the initial balance used only while the account is younger than three months. Negative accounts run the same arithmetic on debt ratio.
Two disciplines come with it. The status is evaluated daily but only committed at month end, so mid-month swings never rewrite state, and the committed changes form a status timeline. Un-hatting uses hysteresis: a positive account has to climb back above 80% to leave ST, and *ST steps down to ST first rather than clearing both caps at once.
Category roles make structure computable
Every category has exactly one role, out of five: main force, retail, hot money, northbound, margin. Main force is salary and mortgage, large and low-frequency, and it sets the trend. Retail is food, transit and coffee, small and high-frequency, and it creates the noise. Hot money is reimbursements and red envelopes, unpredictable short-term spikes. Northbound is money injected from outside. Margin is credit cards and instalments, leverage that amplifies movement.
Only with that layer does "what is pulling the index up and what is beating it down" become something I can compute, which is where the dragon-tiger board and the four-quadrant fund flow (main force and retail, by buy and sell) come from. One role per category is a hard constraint, otherwise the aggregation double counts.
Personas and humour, both with guardrails
The branch persona is inferred from actual booking behaviour in real time: 16 profiles that exist in parallel with no ranking, built from four axes (activity, spread, purpose, control), each with a four-letter code and one operating principle, and with hysteresis so a single odd month cannot rewrite it. It is a behavioural portrait, not an investment rating, and certainly not trading advice.
The copy side follows a different rule. The line-picker is deterministic: it chooses from candidate lines so the same condition always yields the same sentence and the UI does not jitter on refresh. The non-goals are just as firm: the app never jokes about the user being broke, in debt, or at a loss.
A page of non-goals
What I decided not to do mattered as much as what I did, and almost none of it has been reversed since:
- no real securities trading, no investment advice, no social leaderboards;
- no bank sync, no SMS parsing, no accessibility reading; notification based auto-booking was attempted and then switched off end to end because the recognition could not be trusted, the listener service is not even registered now; manual entry is a choice, not a missing feature;
- no account indexation, no cumulative net contribution, no dual pricing, no base-period concept;
- no encouragement to watch the board daily; regular reviews instead;
- no open-ended chat AI: agent conversation is bounded to reading the local ledger and proposing edits.
The privacy floor was there on day one
There was no "ship it plain, encrypt later" step. The whole database is encrypted through PRAGMA key, the 32-byte key lives in the system Keystore, backup files are AES-256-GCM sealed containers with a random nonce, the four languages (zh, en, ja, ko) were in place from the copy layer, everything works fully offline, and the AI uploads nothing without confirmation and never burns quota on its own. These were part of the definition, not compliance work added before submission to the stores.
The concept went through three rounds
The PRD is now marked v4.0, "aligned with the current implementation", and it says outright that it replaces two stitched-together earlier drafts: v3.2 (dynamic baseline) and v3.3 (account-as-security). So the dynamic status baseline stood up first, the account-as-tradable-instrument layer came after, and the two were merged into one honest document.
Deleting the entire snapshot subsystem afterwards is that same logic run to its end: if month-end closes and account balances can be derived from transactions, a snapshot is only a copy of the derivation, and a copy eventually disagrees with what it copies.
Takeaways
- Name it and define the arithmetic first. The name fixed the metaphor, the metaphor fixed the enums, the enums fixed the engines.
- A borrowed vocabulary only works with boundaries. Candles and ST are metaphors here: no ratings, no trading.
- The non-goals list turned out to be the most useful document in the set; it blocked more pits than the features it removed.
- Local-first plumbing on day one is far cheaper than retrofitting it.