Budgeting-EU-to-CAD-USD-Automatic · port 5004
Euro spending, in Canadian and US dollars
A local Flask app. It converts every purchase at the rate that applied on the day it was spent, reads a purchase off a screenshot without the image leaving the machine, and says what your card’s foreign fee will actually cost. No account, no API key, no bank connection.
Reading a purchase off a screenshot
Upload a card app’s transaction detail, a payment confirmation, or a row of online banking. The engine is a local ONNX model in its own optional requirements file — no picture of what you bought is sent anywhere. Without it installed the upload still keeps the image and you type the amount.
Nothing reaches the ledger unseen. The reader fills a form and the form is what saves. That is not caution for its own sake: a misread booking €5,230 instead of €52.30 surfaces a month later in a total nobody can explain, which is far worse than a screenshot that could not be read at all.
How it decides, and what it refuses
| Rule | Why |
|---|
What the engine actually returns
The strings the tests are built from came off real screenshots, mangled exactly like this. Inventing tidier input would test a parser for a problem this one does not have — and reading the real output is what found three of the bugs below.
What it will cost, and the words “real time”
The phrase promises more than the situation can use. Both free sources republish the ECB’s once-a-day fixing; there is no free intraday EUR/CAD tick. Chasing one would be false precision anyway, because a card is not settled at the rate at the moment you tap — Visa converts on the day the transaction reaches the network, and the fee follows.
So the figure carries the day it was published and the minute it was fetched, and the page says plainly that it is a daily reference rate rather than implying a precision that does not exist.
Reachable from somewhere else
It binds 127.0.0.1 with no password because it holds a
spending history. Hosting it is not a deployment task with a security
appendix — it is a change of threat model, so the app will not let
you do it by accident.
That last line is a raise, not a warning. The process exits and says what to set. A warning printed into a log nobody reads is how a financial ledger ends up on the open internet with no password on it.
What is done about what
| Threat | What is done about it |
|---|
Where the code is
Where the tests are
The same figures as a table
| Module | Lines | Statements | Cover | Responsibility |
|---|
Getting data in
Apple Wallet and Google Wallet have no export API.
They are sandboxed on the device, so nothing on a desktop can read them. That is a real constraint, not a gap here. So there are five honest routes:
What it does
Money is integer cents, everywhere
Floats are the wrong type for money and it shows fast. At the real rate
of 1.6033, €50.00 is exactly 8016.5 cents: Decimal
half-up gives CA$80.17, and naive float arithmetic gives CA$80.16. The
card fee is an integer too — basis points, not 0.025 —
because a float there sits in the middle of a figure somebody is about
to be charged.
Bugs found, and how
Each of these was in working, green-suite code. The “found by” column is the useful part — it says where to look next time.
| What was wrong | Found by | State |
|---|
The part that is easy to get wrong
The ECB publishes on business days only. There are no weekend rows at all, and the longest gap in the series is five days.
| Spent on | Rate from | Lag | Why |
|---|
Both the rate date and the lag are columns in the export, so the arithmetic can always be checked. It never reaches forward either: 5 April is one day from the 7th and three from the 2nd, but the 7th had not happened when the money was spent.
A transaction and a total are different questions
A purchase carries a date, so converting it at a rate that did not exist yet is hindsight. A month-to-date total carries none — “today” there just means “now” — so the honest rate is the latest published at or before now. Conflating the two blanked the CAD and USD headline every weekend.
The fee, measured rather than assumed
The foreign-transaction fee belongs to the card, not the purchase — the same euro coffee costs 2.5% more on a CIBC Visa than on a euro account — so it is stored per card and each purchase is attributed to whichever card paid.
It declines to answer below three priced purchases rather than dressing the published figure up as an observation. A balance says how it was reached too: statement figures are exact, euro purchases converted at the day’s rate are estimates, and the two are counted apart rather than blended into one number that looks exact.
Endpoints
| Method | Route | Does |
|---|
data/ holds the ledger, the rate cache and
data/receipts/ with every screenshot uploaded. It is
gitignored, along with *.db and *.csv. The
repository is public, so that ignore file is what stands between a real
ledger and the internet — a feature that writes anything derived
from a transaction needs a line in it, in the same commit. A folder of
pictures of what you bought is the most personal thing the app holds.