Owner request and answers of 2026-10-24: each user connects their own SJ Prio
Mastercard through Enable Banking (restricted production, BankID); only the
card holder sees the purchases; a fourth bottom destination "Kortköp"
(revises D-048); "Anslut kort" in Settings with the card artwork; one- and
two-tap linking to household expenses; web as a later plan.
FR-22, §9.12 (CP-1 to CP-14), server schema 4 (migration 0004), Room 2,
E-36 to E-44, S-23 to S-27, W-21 to W-23, OQ-7, OQ-8. PLAN-K0 describes the
test app of 2ce317f and the remaining run and report; K1 server, K2 Android,
K3 web. HANDOFF and prompts/README updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
PLAN-K0: Card purchases spike (local Enable Banking test app)
Status: the test app landed 2ce317f (docs/spikes/k0/app.py, docs/spikes/k0/README.md). What is left is running it with the owner (and the partner) against the real SJ Prio Mastercard and writing the report. This plan describes the app as built and the remaining work (D-176).
1. Goal
Fast feedback on the real bank before any Android, server or web work: try the Enable Banking flow on the MacBook with the owner's own SJ Prio Mastercard, see real data on the owner's own screen, and answer every K0 TODO(spec) of SPEC §9.12 in a report with aggregates only. The answers go into SPEC v0.36 (a docs agent) before K1 starts.
2. Read first
- SPEC §9.12 (every
TODO(spec)marked K0, CP-1 to CP-14), FR-22, §8.7 (no personal content in logs), §10.5.2 (the/cards/callbackblock), D-158 to D-176. docs/spikes/k0/README.mdanddocs/spikes/k0/app.py(the app as built).- Enable Banking's official documentation (API reference, quick start, FAQ), read at run time (
CLAUDE.md"Fast-moving APIs"). CLAUDE.md(personal data, reports are deliverables, stay inside the repository).
3. Already established
- The app (as built,
2ce317f): one command,python3 docs/spikes/k0/app.py(standard library only; the RS256 JWT is signed by shelling out toopenssl), serveshttp://localhost:8765/and opens the browser. Options--port,--no-browser,--local-dir,--api-base. Sections: 1 Status; 2 Banker (GET /aspsps?country=SE, SJ/SEB Kort/Eurocard first,maximum_consent_validity,auth_methods,required_psu_headers); 3 Anslut (POST /auth,valid_until= now + the ASPSP's maximum capped at 180 days less 5 minutes,psu_typepersonal, a randomstate, the bank in a new tab, BankID by QR); 4 Klistra in (the redirect address pasted back,statechecked,POST /sessions); 5 Konton (GET /sessions/{id}, identifiers masked to 4 characters); 6 Hämta: "Uppdatera" (PSU headers,date_from, optionalstrategy=longest,continuation_keyfollowed), "Bakgrundshämtning" (no PSU headers: the 4-per-day path), "Saldo"; 7 Köp (dates, text, amount, currency, direction, status,entry_reference, compared with the previous fetch); 8 Analys (aggregates only); 9 Koppla från (DELETE /sessions/{id}). History choices 7, 30, 90, 180, 365, 730 days. - Key, config and state live in
docs/spikes/k0/.local/(git-ignored by the root.gitignore):<application-id>.pem, optionalconfig.json,state.json(settings, bank list, session id, account, the last two fetches, the aggregate fetch log). A re-run reuses the session untilvalid_until, so no new BankID. stdout shows method, path template and status only. - Enable Banking's production control panel refuses
http://localhostredirect URLs ("uses unsupported scheme"). The registered redirect URL ishttps://kvitt.suorra.duckdns.org/cards/callback; it does not load yet (no K1 server), but the address bar keepscodeandstate, which the owner pastes into section 4. - Documented facts already in the spec (§9.12): the PSU header names and "either all required PSU headers … or none"; "many ASPSPs have the limit of 4 times a day for data fetches when PSU is not online";
maximum_consent_validityin seconds;entry_reference"unique and immutable", "in most cases … provided only for booked transactions";transaction_id"may change";WRONG_TRANSACTIONS_PERIODandstrategy=longest. - Real purchase data may show on the owner's screen. It never goes into git, a log, a report, a chat message or a file outside
.local/. The application id is not a secret but stays out of every committed file.
4. Owned paths
docs/spikes/k0/** (the app; .local/ stays git-ignored and uncommitted) and docs/spikes/K0-enable-banking.md (new). Nothing else: not SPEC.md, docs/decisions.md, server/**, infra/**.
5. Deliverables
docs/spikes/K0-enable-banking.md, aggregates only, recording the commit it was taken at, with one answer per K0 item of §9.12 (each: what was measured, the answer, the proposed spec text):- The SJ Prio ASPSP's exact
nameandcountry,auth_methods,psu_types,beta,required_psu_headers(CP-1, CP-5). maximum_consent_validity(seconds and days) and thevalid_untilthe session returned (CP-6).- History depth: the earliest date reached for 90, 180 and 365 days asked right after BankID, and for the same asks a day later (CP-3); what
strategy=longestreturned and whetherWRONG_TRANSACTIONS_PERIODappeared (CP-7). - Background fetch without PSU headers: accepted or refused (status and error code), and how many in 24 hours before a refusal, if any (CP-4).
- User-present fetch from the Mac's address: accepted with which headers (CP-5).
- Accounts per session, their
cash_account_typevalues, and the account-pick rule (CP-2); whether the account uid is the same after a second BankID (renewal). - Field presence over the fetched transactions:
entry_reference(booked and pending),transaction_date,value_date,booking_date,creditor.name,remittance_information, other text fields; which field holds the shop name (CP-7 merchant rule; describe without quoting any text). - Pending behaviour: counts of
PDNG, whether a pending row reappears asBOOKwith or without the sameentry_reference, the days between, whether amounts or texts change (CP-8); whether the ±3-day amount and first-token rule would have matched every case. - Currency: any non-SEK
transaction_amount.currency; for a known foreign purchase, what the card reports (CP-7). - Credit rows: counts and kinds (refund, bill payment, other), described without amounts (CP-8).
- The partner's card: whether it could be linked to the owner's application in the control panel and fetched (CP-13); if not, the exact refusal.
- Amount format (decimal string, separator, sign) (CP-7).
- Anything that contradicts §9.12, with the proposed revision.
- The SJ Prio ASPSP's exact
- A short "Results (owner's phone)" section is not needed (nothing runs on the phone), but a "Results (owner's run)" section lists each owner step with its yes/no.
- Fixes to
app.pyfound while running (iterate in minutes; stdlib only, at most one JWT library if openssl fails), committed with the report.
6. Steps
- Worktree: from
/Users/jhag/projects/kvitt,git worktree add .claude/worktrees/agent-k0 -b agent-k0 main; in itgit reset --hard main;tools/install-git-hooks.sh. Copydocs/spikes/k0/.local/from the main checkout if the owner already put the key there (never commit it). - Read §2. Check the app starts:
python3 docs/spikes/k0/app.py --no-browserandcurl -s localhost:8765/ | head(no key yet: section 1 says so). - Relay the owner steps of §8 one at a time through the orchestrator; run the app for the owner (it opens the browser on the Mac).
- After each owner step, read the aggregates only, never
state.jsonitself:python3 -c "import sys; sys.path.insert(0,'docs/spikes/k0'); import app; print(app.analysis_text(app.load_state()))"from the worktree root (checked on an empty state, 2026-10-24). Nothing else is printed from.local/; where the analysis lacks a K0 item of §5, extendanalysis_text(aggregates only) rather than reading the state. - A day later (owner step 9): repeat "Uppdatera" and "Bakgrundshämtning" without a new BankID to see the reused session, the history depth later and the background path over 24 hours.
- Write the report;
git statusmust list nothing underdocs/spikes/k0/.local/. Commit onagent-k0,git rebase main. Never merge, push or remove the worktree. - At the end ask the owner (through the orchestrator) whether to keep the session for K1's tests or to press "9 Koppla från" (which deletes the stored transactions from
state.json); the key stays in.local/for K1, which copies it toinfra/secrets/cards/(PLAN-K1).
7. Gate
None: K0 is not part of any gate. Before committing: python3 -m py_compile docs/spikes/k0/app.py; git status --porcelain shows no file under .local/; grep the report for anything that looks like an amount with kr, a date of a purchase or the application id (none may appear). Report what was run.
8. HUMAN steps (the orchestrator relays them in chat, one at a time, exact labels as on screen; steps already done are confirmed with the owner and skipped)
- In a browser on the Mac open
https://enablebanking.com, open the Control Panel and sign up with your email address; confirm the email. When Enable Banking shows its Terms of Service, read and accept them: this is the licence step Kvitt needs before using the service (answer yes or no in chat). - Control Panel › API applications › "Add a new application": environment Production (restricted production: your own accounts only), application name Kvitt, redirect URL exactly
https://kvitt.suorra.duckdns.org/cards/callback(only this one;http://localhostis refused), keep the private key generated in the browser, fill the required contact fields, then register. Expected: the browser downloads a file<application id>.pem(yes/no). - Move that
.pemfile from Downloads into the folder/Users/jhag/projects/kvitt/docs/spikes/k0/.local/(create the folder.localif it is missing: in Finder press Cmd+Shift+. to show hidden folders). Keep the file name. Expected: the test app's section 1 shows "key found" (the agent checks). - Recommended before the first connection, so the one-time code in the bank's redirect never reaches NPM's log: NPM › Hosts › Proxy Hosts › the Kvitt row › Edit › Advanced, add at the end of Custom Nginx Configuration the
location = /cards/callback { … }block of SPEC §10.5.2 (the agent pastes it in chat), Save. Expected: the row is still Online (yes/no). - Control Panel › the Kvitt application › "Activate by linking accounts": country Sweden, the bank listed as SJ Prio Mastercard, sign with BankID (scan the QR with the BankID app on your phone). Expected: your card account is listed as linked (yes/no).
- With your partner: on the same page link her SJ Prio Mastercard, she signs with her own BankID. Expected: a second linked account (yes/no); if it fails, say the exact message.
- Test app (the agent starts it, the browser opens
http://localhost:8765/): "2 Banker" › "Lista banker", keep the highlighted SJ Prio entry, "Välj bank". "3 Anslut": Historik 90 dagar, "Anslut med BankID"; a bank tab opens, scan the QR with BankID. Expected: the tab ends on a page that does not load (yes/no). Copy the whole address from that tab's address bar, paste it in "4 Klistra in", "Använd adressen". Expected: "5 Konton" shows your card account (yes/no; how many accounts). - "6 Hämta" › "Uppdatera". Expected: "7 Köp" lists your recent purchases and the newest ones look right (yes/no). Then "Bakgrundshämtning". Expected: it fetches, or shows an error (say which: fetched or error).
- The next day: open
http://localhost:8765/again (the agent starts the app), "Uppdatera" and "Bakgrundshämtning" without a new BankID. Expected: both work without BankID (yes/no each). - If step 6 worked: repeat steps 7 and 8 with your partner's BankID for her card (yes/no each).
9. Out of scope
- Anything in
server/**,android/**,web/**,contracts/**,infra/**(K1 to K3), and NPM changes beyond the optional step 4. - Writing real data anywhere outside
docs/spikes/k0/.local/; readingstate.jsonother than throughanalysis_text. - Editing
SPEC.mdordocs/decisions.md(the report proposes the text).
10. Report
- Branch
agent-k0, head commit after rebasing; what was run (py_compile, thegit statusand grep checks). - One line per K0 item of §5 with the answer, and the proposed SPEC v0.36 text for every
TODO(spec)of §9.12 (CP-1 to CP-8, CP-13), plus proposed decision entries numbered from the brief. - The owner steps done, with their yes/no; anything still to do, worded for relay.
- Whether the session is kept for K1 and where the key is.
11. Numbering
Proposed decisions from the brief's range; the spec version the findings go into is v0.36.