Seedelf-Wallet

Post-release roadmap

roadmap.md is closed: every chunk in it is done, through 1.1.0, the transaction view. This file is what comes after it.

Why a second file. The roadmap’s job was to get a wallet built, in order, to a line where it could ship. That job is finished. From here the work is a short sequence with a set of candidates hanging off it — things already promised, the last of Cardano parity, then a pass over how all of it looks and feels. The roadmap stays the record of what happened and still takes the handoff notes; this one is the record of what might, and it’s what a new chunk is chosen from.

How this file works

The order

The owner’s call (2026-10-02): feature parity first, the look and feel after it. The wallet can be feature-complete bar the dApp additions, and it’s a better thing to put a style pass in front of once it is.

  1. ✅ A clean dependabot report — done 2026-10-02, all 25 cleared in Cargo.lock and the happy-path requirements.txt, no source change.
  2. Owed — promises the repo has already made.
  3. ✅ Feature parity: what’s left — several accounts (chunk 18), language (chunk 19), NFT images (chunk 20, 2026-10-04). The wallet is feature-complete for Cardano.
  4. ✅ Public-side completeness — the governance items, reopened for the public account. Before the UX pass (the owner, 2026-10-02), so the pass gets a finished wallet to look at. Built 2026-10-04, chunk 21: be your own DRep and vote, in the wallet’s own screens and through CIP-95 for sites such as GovTool. The owner registered and voted on preprod; what’s left by hand is O6.
  5. 🚧 The UX and UI pass — round three, chunk 23 (web-wallet/style-flow-3): 2 and 3 have landed, and two adversarial usability reviews and their fixes with them (usability-review.md: its opening says what round one fixed, and §9 what round two did and what’s left for the owner), then a blind task-completion test and its fixes (blind-task-usability-test.md: §9 its ten changes, §11 what the fix round did). The owner writes the rest of its list.
  6. ✅ The documentation review — brought forward, ahead of the pass (the owner, 2026-10-04): chunk 22. So the pass doesn’t need a second review after it, it keeps the docs current as it goes — a renamed control or a moved screen updates flows.md in the same commit, the way it updates the e2e tests.

Then the data layer: 1.4.0’s focus (the owner, 2026-10-08), together with whatever UX and UI fixes users send in (none came for 1.4.0). ✅ Chunk 26: archive/plans/chunk-26-data-layer.md, then 26b (the wallet’s side) and 26c (the server), and 1.4.0 gives the store build its origin.

It was parked on 2026-10-02, while Koios was good enough: “it will require some things that go beyond just making the wallet work as expected.” Mainnet’s slow Koios reads made it next.

Then Lovejoin token pools, 1.6.0’s focus (the owner, 2026-10-13). Lovejoin PR #163 lets a launch pool one native token, so a token can be mixed and come back through the mixer the way ADA does. Seedelf users launch the pools from their private balance; we launch none. The classic ADA pool stays exactly as it is. 🚧 Chunk 34: plans/chunk-34-token-pools.md, with two of its designs still under discussion: pool spam (D1) and bootstrap (D2). Beside it, what the owner noticed using the wallet: ✅ chunk 35, archive/plans/chunk-35-small-fixes.md, the collateral (a way in from the Public tab and Lovejoin’s public mix, and a Settings page that kept waiting); and how a Lovejoin mix is shown, which went to chunk 34’s Screens.

dApp additions run alongside all of it and are not counted in parity.

Where Cardano parity stands

Measured against Lace 2.4.2 (_reference/lace at e431933, pulled 2026-10-02), the reference since chunk 11a. Parity holds: the three gaps found on 2026-10-02 closed in chunks 18–20, and the rest is a deliberate no.

Lace’s surface Seedelf Wallet  
Portfolio: tokens, NFTs, search, sort Tokens, with search, sort, NFTs split out, logos from the bundled registry, and an NFT’s image when asked for matched
Send, several recipients, a note Send on both sides, up to 20 recipients, CIP-20 note on a public send matched — and it pays Seedelfs as well as addresses
Receive, QR, your ADA Handles All three, plus the handle-in-Seedelf warning matched
Activity, detail per transaction Activity on both sides, with CSV (after Eternl’s) matched
Staking: one pool, pool browser, rewards, stop All four; Stop staking returns the 2 ₳ deposit matched
Governance: DRep delegation, DRep browser Both, from a DRep list that ships with the wallet; and be your own DRep: register, vote on live actions, a profile, retire (chunk 21) matched, and ahead — Lace leaves registering and voting to GovTool over CIP-95, which the connector here offers too
dApp connector (CIP-30), authorized dApps, sign tx, sign data All, off until turned on, nothing injected before that matched — and it has private CIP-30 too
dApp explorer The dApps page; a small catalogue matched in kind; see dApp additions
Swap center Minswap’s aggregator matched — and swaps can run privately, through a one-time account
Collateral Settings → Collateral, 5 ₳ reserved locally, as Lace does it matched
Contacts / address book Contacts, offering addresses and Seedelf names matched
App lock, lock timeout, default open mode All three; tab by default, side panel by choice, no popup matched
Fiat value Eight currencies or nothing, mainnet only matched
Review a transaction before signing Transaction details everywhere: the CBOR decoded by us, plus the raw bytes ahead — Lace resolves and prices the transaction; what we add is the bytes themselves, decoded and raw
Several accounts, folders, account center Several accounts, one at a time, with a picker, names, and each account’s own staking and collateral matched in substance — done, P1; folders and an account centre are not planned
Language (i18n) English, Spanish, Japanese P2 done, chunk 19, Lace’s way
NFT images Show image in an NFT’s details, one at a time, from IPFS through Blockfrost’s gateway; then its avatar too, until the lock matched — done, P3, chunk 20. Lace loads every NFT’s image unasked; ours is asked for, and says first who sees it
Notification center None kept in mind, not this round — the data layer it needed is live since 1.4.0
Several pools per account One pool one pool per account is the model — and Lace is coming the same way: migrate-multi-delegation at tip is a multi→single migration
Hardware wallets None can’t be done — see the proof
Earn / RealFi USDr staking, CIP-99 claims None no — likely AML/KYC, which doesn’t align with Seedelf
Bitcoin, Midnight, mobile None Cardano only for now; others maybe much later

Dark only, by the owner’s call — there is no light theme and no theme picker, and that isn’t a gap. One recovery phrase, likewise: not a gap, not wanted.

Step 0 · a clean dependabot report

The owner, 2026-10-02: a pre-step, before chunk 18 — “I do like to maintain good dependabot reports.”

Done, 2026-10-02: all 25 alerts cleared, in two files and no source change. Cargo.lock and the happy-path scripts’ requirements.txt. What was triaged, and what each one turned out to be:

What Alerts Where What actually ships it Now
urllib3 ×6, cbor2 ×3, idna, requests 11 seedelf-contracts/happy-path-scripts/seedelf/backend/requirements.txt Nothing. By-hand contract test scripts, pinned at 2024 versions. urllib3 2.8.0, cbor2 5.9.0, idna 3.20, requests 2.34.2
openssl 8 Cargo.lock, via reqwest → hyper-tls → native-tls, from seedelf-koios and seedelf-display The CLI binaries only. Absent from the WebAssembly — cargo tree -p seedelf-wasm --target wasm32-unknown-unknown -i openssl finds nothing. 0.10.73 → 0.10.80 (and openssl-sys 0.9.109 → 0.9.117)
rustls-webpki 4 Cargo.lock Nothing at all — but not for the reason first recorded; see below. 0.103.3 → 0.103.15
rpassword (low) 1 Cargo.lock, a direct dependency of seedelf-cli The CLI only — its password prompt. 7.4.0 → 7.5.0 (7.5.4 since #280: 7.5.0 doesn’t build on macOS)
rand (low) 1 Cargo.lock, via group ← blstrs The extension’s WebAssembly — the only flagged crate that reaches what users install. 0.8.5 → 0.8.6

npm had no alerts at all: the extension’s own JavaScript dependencies were clean.

Since step 0, two new advisories matched lock entries that never ship, and 1.2.0’s release branch bumped both, in the lockfiles only: GHSA-68fv-2mgg-jv7q (high) on source-map-js 1.2.1, a dev dependency (vite → postcss) the bundle never holds, now 1.2.2; and GHSA-2mjx-qc3c-rqvc (RUSTSEC-2026-0285) on rustls 0.23.28, locked through the same never-built reqwest → hyper-rustls chain as rustls-webpki below, now 0.23.45. Neither reached what users install, and npm audit is clean again. What’s still open is the CLI’s h2 and spin (Kept in mind), which Dependabot doesn’t raise.

rustls-webpki was not a stale lock entry, as the triage first read it. It arrives as an unactivated optional dependency of reqwest (reqwest → hyper-rustls → rustls → rustls-webpki), and Cargo.lock records a package’s optional dependencies whether or not a feature turns them on. cargo tree is feature-aware, which is why it finds no path — the code genuinely never builds, so the four alerts never reached anything. But cargo update bumps it rather than dropping it, and it cannot be dropped while reqwest is a dependency. It was bumped.

What proves the bump safe ran, and passed. cargo test --workspace --locked: 494 tests across 51 binaries, 0 failures (8 ignored, the live-network ones), including the two that a dependency change must not move — frozen_v1_vectors (the derivation vectors in seedelf-crypto/tests/vectors/seedelf_key_v1.json: a phrase still derives the same Seedelf key) and spec_constants_are_frozen (constants_test.rs’s pinned script hashes still match what’s deployed). cargo fmt --all -- --check and cargo clippy --locked -p seedelf-crypto -p seedelf-wasm --all-targets -- -D warnings are clean, the CLI release-builds and reports its version against the new openssl, and the WebAssembly builds and passes all 41 of its JS tests.

The module did not grow meaningfully. Measured A/B — the same source built from the pre-bump lock and the bumped one, rand being the only flagged crate that reaches wasm32: 2,638,180 → 2,638,557 bytes raw, +377 bytes (0.014%), with gzip (781 KB) and brotli (566 KB) unchanged and the timings inside the noise. The rebuild is byte-identical to the measured module, so build.sh’s reproducibility (launch review #61) still holds.

Two things the re-measure turned up, neither caused by the bump:

compile.sh’s cbor2 is a separate install from the pinned file (root CLAUDE.md) and was left alone — but the bump was checked against it anyway, because its one call bakes the seed into the validators and a change there would move the script hashes. cbor2.dumps(bytes.fromhex("acabcafe")).hex() is 44acabcafe on 5.9.0 — the one valid CBOR encoding of those four bytes, so neither the seed nor the hashes built from it can move with a cbor2 version. The machine’s system cbor2 is already 5.9.0.

The pinned Python set was installed and exercised, not just edited. A fresh venv resolves it with pip check clean, and on it the backend’s own crypto still agrees with itself: a register verifies, a re-randomized (g^d, u^d) verifies under the same x, a Schnorr proof through fiat_shamir_heuristic verifies, and the datum round-trips through cbor2.

The one fixed on merit rather than for a green badge: rpassword’s advisory is partial password reveal when input is interrupted, and rpassword is what the CLI prompts for a wallet password with. Low severity, and precisely on-point for this project.

Still a decision, and deliberately not taken here: openssl arrives only through reqwest’s default native-tls feature. On rustls instead, those eight alerts stop recurring rather than being patched each release, and the CLI loses its OpenSSL build dependency. That’s a feature-flag change across seedelf-koios and seedelf-display with the live Koios tests as the check — bigger than a pre-step, so it stays an open question. ❓

Owed

Promises the repo has already made. These come before anything new.

# Item Status
O1 1.1.0 goes live, and is recorded. Dropped (the owner, 2026-10-04): the docs don’t track the store. Anything a dashboard asks that the docs don’t cover still goes in store/README.md. —
O2 The by-hand mainnet checks (from the launch review’s Still open): exclude_protocols with VyFinance and MuesliSwap, one swap through each route in MAINNET_PROTOCOLS, and Stop on a swap — done by the owner on 2026-10-07 (chunk 24) for every route but Splash and Spectrum: a Stop cancelled a Minswap V2 and a Minswap V1 order, and no quote or swap went through an excluded DEX. Splash and Spectrum are dead in the water (the owner, 2026-10-08), so their runs, and what they would confirm (a Splash order’s twelve typed fields, C02 and C05; a partial fill’s walk back), are nice to have and very low priority: Kept in mind. No re-run is asked (the owner spends no real money on tests, 2026-10-08): the first real swaps on 1.3.0 and later are the live check of the dry-run cancel (C05), which shows as a pause on that route if it refuses a live order falsely, and the first Danogo swap checks that the collateral is Minswap’s (C21): one that pauses with “it puts up this session’s own funds as collateral” means loosening C21 in a patch. Checked from chain data on 2026-10-08: in 12 real mainnet Danogo swaps, every output at Danogo’s script sits at a spent pool’s own address, one each (C22); and Koios’s tx_info gives each input’s tx_hash and tx_index, which the walk back from a partial fill relies on (C02). The owner’s, since they spend real ADA. 🚧 owner’s
O3 ✅ Chunk 25: a chain being sent opens in the transaction view, from its row on the Lovejoin page (Its transactions: each one, where it is), and Home’s banner links a public mix’s first transaction, “the first of 14”, until the last lands. Not all of it, though: a chain’s last four transactions, and all of a chain of four or fewer, leave the list and the view once sent, before they land (1.3.0’s release review, C28). Whether a sent chain stays viewable until its last lands, or is audited afterwards, is the owner’s question. A chain in flight is invisible to the transaction view. Its transactions move to seedelf.session.chain.<network>.<index>, which BUILT_KEYS doesn’t name, so the audit view wants that prefix scan. And Home’s banner watches the chain’s last transaction, so it shows a hash the user never saw — it may want to say “the chain” instead. Both from chunk 17’s handoff note. ✅
O4 A2, the contract round trip: a Minswap V2 order paying straight to a Seedelf. Designed in flows.md; waiting on a batcher test. Chunk 24’s research (2026-10-06) found better candidates: SundaeSwap V3 first (its order pays a Fixed destination with an inline datum, which the pool enforces; audited; its orders never expire; its API answers an extension), then WingRiders V2. No aggregator takes a receiver, so each needs the wallet’s own order builder. Splash can’t: its executor drops the payout’s datum. Preprod’s V3 has no scooper, so its test is mainnet’s. ⛔ blocked
O5 ✅ The restore scan, chunk 27 (archive/plans/chunk-27-restore-scan.md): a restore looks, on the network it’s made on, for the one-time accounts (24301') the phrase used that the device has no record of, and Find leftovers, on the dApps page, looks again on any network. Each that holds something, or has an order open, comes back as a session found, brought back by hand (Cancel the order, Bring it back, or Bring everything back), and Remove wallet and Forgot password now count private sessions’ money among what the phrase brings back (since 1.5.0’s release review, what their accounts hold, not what a site’s session left open at the site: C17). Swaps, site sessions and Lovejoin’s mix sessions share the index sequence, so one scan covers all three. After a restore, or with the same phrase in another browser, what a private session’s one-time account still holds doesn’t show, and Remove wallet and Forgot password say so (independent review M5) — that warning is the promise. The design is in keys-and-accounts.md: one scan of the indices, at a restore or from a button, never on an open. ✅
O6 Chunk 21 by hand: retire live, which also confirms the ledger drops delegations to a retired DRep (what flows.md’s “the rewards stay withdrawable” rests on); a profile update live; and GovTool over CIP-95. Registering with the account’s own vote, and a vote, ran on preprod on 2026-10-04. The owner’s. ⬜ owner’s
O7 ✅ Chunk 28 (archive/plans/chunk-28-activity-blind-spots.md): Public activity lists by the account’s payment keys (credential_txs), the ones the balance reading asks about, so it lists what the balance counts, whatever an address’s staking part, and tells the server nothing new; the data layer has the route too, so its API is deployed before the wallet that asks it is uploaded. And a public mix’s transactions are named from Lovejoin’s mix box among their inputs or outputs: Into Lovejoin for its deposit, Lovejoin mix for each mix, after a restore too. The public Activity’s two blind spots. It lists by stake address (activity.ts, account_txs) while the balance reads by payment key, so a transaction touching only an enterprise address of the account, or its key under someone else’s stake key, is counted and spent but never listed, nor in the CSV. And a Lovejoin mix from the public account lists its deposit and every mix as plain Sent, since an account transaction’s kind comes only from the Seedelf history (chunk 16’s Not done). ✅
O8 Dates and numbers aren’t localised. Dates ✅ chunk 25: dateLocale() (en → en-GB, es → es-ES, ja → ja-JP) in every date and time. Numbers stay en-US’s on purpose (ten calls): the amount fields write “1,234.5” and read a Spanish decimal comma apart (format.ts), so a Spanish screen showing “1.234,5” would contradict its own fields. Localising them means the fields too. Settled (the owner, 2026-10-07): numbers stay as they are. ✅
O9 Tell Lovejoin what we found (outward-facing, so the owner’s): its config/network.{mainnet,preprod}.json and offchain/src/seedelf/addresses.ts swap Seedelf’s preprod and mainnet reference UTxOs; its mainnet config has mix_script_hash: null (ours, c145c10f…1fad, is confirmed against mainnet transactions); and its withdraw writes no datum while its app offers any script address as a Seedelf one — the deployed wallet.ak lets anyone spend a UTxO with no datum, so a box withdrawn to the Seedelf contract from Lovejoin’s own app can be taken by anyone. That fix is theirs: a fresh register when the destination is the Seedelf contract. ⬜ owner’s
O10 The data layer’s submit part, gated on the node’s sync (1.4.0’s release review): while home’s node catches up after a restart, the reads go to Koios on their 503s but a submit still goes home, where a valid spend of a newer UTxO is refused as “spent”, and a Lovejoin chain’s first step stops on it. The API should answer a submit 503 while Ogmios’s /health says the node isn’t synced, so the wallet goes to Koios at once. A wallet-side fix (asking Koios after every “spent”) was taken back: it stopped chains whose resends home rightly calls spent. 26c. Built for 1.4.1 (web-wallet/patch-1.4.1): the API reads Ogmios’s /health every 2 s, and submittx and ogmios answer 503 behind while the node’s tip is over 3 minutes old or Ogmios can’t reach it (seedelf-data’s README). No wallet change: a 503 already sent both to Koios. Live since the API’s deploy from fd412c0, PR #296’s merge commit. ✅

Feature parity: what’s left

Three items, all done (2026-10-04). The wallet is feature-complete for Cardano, bar dApp additions.

P1 · Several accounts

✅ Done, 2026-10-02: chunk 18, the owner’s first pick after the release. Branch web-wallet/several-accounts, plan archive/plans/chunk-18-several-accounts.md, handoff note in roadmap.md.

What landed. A picker in the top bar, hidden with one account (chunk 23 made it Home’s Public heading); Settings → Public accounts to switch, name and look for another; discovery on a restore and on demand, one Koios account_addresses request per account probed, stopping at the first never used; each account with its own staking, collateral and locked UTxOs; and the private side kept apart per account (public:<n> history classes). The two things that needed deciding and were decided in the building:

The owner’s correction, 2026-10-02, and the rule it set: “a user may want to use the wallet to send between accounts too. We are making a lot of assumptions about accounts not being linked when they in fact can and in some cases that was encouraged.” So the wallet never forbids a link the user chooses — paying one of your own accounts from another goes through, with the form naming the account and saying what it reveals, as every other entry in privacy.md’s Known links is handled. What it avoids is making a link for them: coin selection keeping accounts’ money apart where a choice that doesn’t merge them pays, and a site never being handed a second account behind their back. A first attempt refused the send outright; that was over-reach and was removed. Privacy by default means the private option is the default, not the only one.

And accounts are reached by number, not only found (the owner, 2026-10-02): sequential discovery stops at the first unused account, so a custom index like 1337 could never be found, and use refused what discovery hadn’t seen, so one could never be started either. Settings → Public accounts takes an account number — Check it (one Koios request) and Add it (no request, used or not).

What is deliberately not in it: folders and an account centre (a picker and a name are what the parity gap was), a Seedelf key per account (decided against), reading several accounts at once or showing a total across them (the wallet doesn’t put them together on its own), and an account per site: one dApp account for every site is the model.

The original brief, kept for the reasoning:

The owner’s reason (2026-10-02): a user restoring a phrase may hold funds on accounts other than 0' and want to move them into Seedelf, and some people run several accounts as a form of privacy in the first place — so a wallet that can only see one is both losing their money and working against the habit they came with.

The groundwork is in: every function already takes the account index, and a picker would discover accounts in order the way BIP44 does, stopping at the first never used (keys-and-accounts.md).

Decided (the owner, 2026-10-02): the Seedelf key stays on account 0 — one private balance for the whole phrase. The derivation would allow one key per account (info = "seedelf-key" || u32_be(account) in derivation.rs), and it isn’t needed: the stealth addressing already unlinks the move-ins. Two public accounts paying the same Seedelf create re-randomized registers (g^d1, u^d1) and (g^d2, u^d2), which can’t be tied to each other or back to (g, u) without d, so nothing on chain links the accounts. Account 0 is, in effect, the nonce.

What stealth addressing does not cover, and this is P1’s real work: co-spending. A contract UTxO’s creating transaction is public. The registers hid who the money went to, not where an input came from — so one later private spend that takes a UTxO originating from account 0 together with one from account 1 ties those two accounts to one owner, in the open.

The machinery for this already exists (privacy review §2.3): each private UTxO gets a HistoryClass, seedelf-core’s build::Histories keeps different classes apart where a choice that doesn’t merge them pays, and the UTxOs screen tags each UTxO’s origin.

But today every move-in shares one class. MADE_PRIVATE is a single constant — { id: "public", origin: "own" } in shared/histories.ts — so money made private from account 0 and from account 1 would be the same history, and selection would co-spend them freely. P1 has to make it per-account (public:<n>), which means:

Treat that as a correctness-of-privacy item, not a nicety: without it, several accounts would quietly undo the separation the feature exists to respect.

Also to settle: discovery cost against the Koios budget, and what the dApp connector offers a site when there are several accounts — both settled in the building, above.

P2 · Language

Built, chunk 19 (plan): all 2,471 strings are out of source; at its merge en.json held 2,128 keys, es.json 2,128 and ja.json 1,940, and 195 derived accuracy-critical keys are back-translated and recorded. It cost +567 kB raw / +134 kB gzip of bundle — the three locale files plus i18next, which has no dependencies of its own. English is byte-identical everywhere, which is how the 1,186 existing tests passed unchanged (1,190 with the four this chunk added). At its merge, dates and numbers were not translated: 19 toLocale*/Intl calls pinned en-GB/en-US, which the plan names as a known gap with its fix. Chunk 25 closed it for dates (dateLocale()); numbers stay en-US’s on purpose, the owner’s call (O8). Each of the costs this entry predicted is settled below; what it did not settle is who reads the Spanish and Japanese (settled on 2026-10-03: accepted as recorded), and the plan’s Still open says plainly that nobody has. A whole-locale review followed (2026-10-03/04): one reviewer per language read every entry, the corrections and a second reader’s check are in, and it found the worker had never switched language (about 300 messages English for everyone) — fixed, with the code that read its own English made language-proof first. 298 keys were critical after it, 195 at the build, and the set grows with every chunk: i18n/critical-keys.json is today’s. The plan’s section has the rest.

The original brief, kept for the reasoning:

Copy what Lace does, including its languages (the owner, 2026-10-02): English, Spanish and Japanese — “probably a very large chunk of the Cardano user base”. It’s a good fit for our rules because every locale is bundled — nothing is fetched, so no new host and nothing phones home:

What it costs us, specifically:

P3 · NFT images

✅ Done, 2026-10-04: chunk 20. Branch web-wallet/nft-images, plan archive/plans/chunk-20-nft-images.md, handoff note in roadmap.md.

What landed. Show image in an NFT’s details, and nowhere else. One Koios asset_info request for its metadata (CIP-68’s datum, or CIP-25’s label 721), then one fetch from IPFS through Blockfrost’s gateway, ipfs.blockfrost.dev, the only one of about twenty public gateways that still served files (ipfs.io has gone service-worker-only). Before the click, the details say who sees what, and on the private side that either party could tie this IP address to the UTxO holding the NFT. The first click asks Chrome for that one host, a part of the dApp connector’s optional https://*/*, so installing asks for nothing new; when that access, given for connecting sites, covers it already, nothing is asked. The image comes as data, with no cookies and no cache, and stays in the page’s memory until the wallet locks, as the NFT’s avatar too. Only IPFS is fetched: an image on any other server is an address to copy, because anyone can send you an NFT whose image is on their server. A Seedelf’s token is never asked about.

The original brief, kept for the reasoning:

The owner’s design (2026-10-02): click to show, and the image downloads to that browser. No image host or proxy of ours — serving image data for every asset needs serious hardware, and a proxy would be one more service that sees what a wallet holds.

Public-side completeness

The owner, 2026-10-02: after the accounts, and before the UX pass. Some of what sat under Not planned is worth doing for the public side, because it’s what makes this a full wallet rather than a private balance with a wallet attached — and the pass should get a finished wallet to look at rather than one with governance still arriving.

The order, with one correction. The owner’s sequence was accounts, then voting, then DRep, on the reasoning that “DRep is part of staking too since it’s required”. Both halves are right, about different things:

Which makes them one chunk, not two: be your own DRep and vote. Registration on its own is a half-feature (a credential nobody uses), and voting on its own can’t be built. ✅ Chunk 21 (2026-10-04), branch web-wallet/governance, plan archive/plans/chunk-21-governance.md.

# Item  
1 P1, several accounts ✅ done, chunk 18
2 Be your own DRep, and vote on governance actions. Register the account’s DRep credential, then vote Yes, No or Abstain on a live action. Needs a list of open governance actions, which is a new read and so a new Koios cost to state. The owner’s calls (2026-10-04): the wallet’s own screens first, then CIP-95 in the connector so GovTool works too; no profile unless one is asked for; the account delegates its vote to itself on registering, by default. ✅ chunk 21
— Staking per account needed no slot: it fell out of P1 for free, as expected — each account has its own stake key (2/0 under its own index), so several accounts means stake spread across several pools, with no change to staking.ts. That is exactly Lace’s model after its multi→single migration — the outcome people wanted from multi-delegation, without multi-delegation, and without touching one-pool-per-account. ✅ done via P1

What doesn’t change: a vote or a registration is a public act by a public key. None of it reaches the private side, and none of it weakens the rule that money made private has no stake key behind it. A Seedelf address has no staking part, so the private balance has no voice to cast and never will.

The data layer

✅ Chunk 26, 1.4.0’s focus (the owner, 2026-10-08): archive/plans/chunk-26-data-layer.md is the plan, and 26c the record of the server; since 1.4.0 the store build reads https://mainnet.seedelf.logicalmechanism.io first. Below is the shape it builds on: decided on 2026-10-02, when the data layer was parked, and still holding. Being parked was the honest call at the time: a service is not a feature. It goes beyond making the wallet work as expected, into something that has to stay up, stay paid for, and stay unexploited for as long as the wallet is in the store.

The owner’s calls (2026-10-08). The plan has the rest.

Decided in shape (the owner, 2026-10-02): a db-sync wrapper in Rust, for the web wallet. Not a general API — queries written for this wallet. The CLI stays on Koios, which already works for it, so this is one client, not two. giveme.my needs no fork: the owner runs it, so it can be adjusted directly if the collateral side ever wants the same treatment.

Clearnet, decided (2026-10-08), because Koios is slow and its rate limit bites. Tor is out of scope here, and that’s a consequence, not a compromise: the one client is a Chrome extension, Chrome doesn’t resolve .onion (RFC 7686 special-use, deliberately unsupported), and the CLI — the thing Tor could have served — isn’t a client. The root README’s Tor exploration stays a CLI and general-infrastructure aspiration. A user who routes their whole machine through Tor still reaches a clearnet endpoint over Tor, so what the wallet owes them is a configurable endpoint and nothing leaking around it.

What it fixes

The constraint that has to hold

The ownership check stays in the wallet. Matching a contract UTxO to a Seedelf needs the secret scalar, so the server must never be asked “which of these are mine” — it serves contract UTxOs and the wallet matches locally, as it does now. A service that answered that question would be a service that knows every user’s private balance.

DoS protection on clearnet — the owner’s question

The wallet makes two credential_utxos queries, and they are opposite in every way that matters here. That split is the whole design.

  The contract scan Account UTxOs
Whose data The contract’s whole UTxO set — byte-identical for every user The user’s own payment credentials (batched 75 a request)
Cacheable Completely Not at all
Cost per request Near zero once cached A real query, and the slow one today
What it reveals to us “This IP uses Seedelf”, and when Which credentials a user is asking about

In order of leverage:

  1. Not being general is the main defence. Koios is PostgREST: any filter, any order, any depth — which is exactly why it can neither cache your query nor bound its cost. A small fixed set of endpoints with fixed query shapes means no caller can compose an expensive request. The worst available is a cheap question asked often, which is a rate-limiting problem rather than a database one.
  2. The expensive query is shared, so cache it. One db-sync read per block (~20 s) serves every user the contract set from memory, with its block height and an ETag. Someone hammering that endpoint gets cached bytes: the cost is bandwidth, not db-sync. The query that hurts most under Koios is the one that caches perfectly — that’s the single biggest win available, and it’s also the cheapest thing to defend.
  3. Deltas, which the wallet already asks for. After a first read it sends only block_height=gt.<last>, so steady state is small responses, cached the same way.
  4. The per-user endpoint takes the bounds instead: an index on the payment credential, a cap on credentials per request (the wallet already batches 75), a concurrency cap per connection, and a per-IP token bucket — a counter in memory, not a log: never written to disk, never joined to what was asked. Rate limiting needs the IP for a few seconds; it doesn’t need a record.
  5. A hard budget with a circuit breaker, and Koios as the fallback. A monthly egress and compute ceiling that degrades to “ask Koios” rather than failing. Neither a surprise bill nor an outage should brick a wallet, and keeping Koios in the picture means the service is never a single point of failure.

What not to do: a third-party edge. Cloudflare or a managed WAF in front is the obvious answer and it undoes the point — it terminates TLS, so a third party sees every request and every IP. That’s the Koios linkage with extra steps and a worse story, because we’d have chosen it. Volumetric protection, if it’s ever needed, wants to be something we run, or an upstream that sees only encrypted bytes.

What the service still learns, said plainly. The contract endpoint learns that an IP uses Seedelf. The account endpoint learns which payment credentials that IP asks about — the same thing Koios learns today, moved to us. The local ownership check is what keeps the private balance out of it entirely. So the no-log promise and the open source carry the account endpoint, and those are a policy and an audit trail, not a proof — which is worth saying in the privacy docs in exactly those words.

What was still open, answered (2026-10-08)

The UX and UI pass

Round three, after parity and the public-side work (the owner’s call). Chunks 12 and 14 were rounds one and two; 11a was only half-Lace. It runs the way both of those did: the owner tests the built wallet and sends findings; each goes in a table with what was decided, and batches land with tab and side-panel screenshots to check before the next rebuild.

Parity and the public side have landed, so the list is the owner’s to write now. Chunk 23 began it with two adversarial usability reviews of the built wallet, each followed by a fix round: usability-review.md is the second review: its opening says what the first round fixed, and §9 what the second did and which calls it left the owner (renaming Make public, opening on Public, Swap on Home, UTxOs behind Advanced, two decimals, a password on the wallet’s own sends, the splash below). A blind task-completion test followed (blind-task-usability-test.md): fresh testers given goals, not paths, through 29 runs of the built wallet. Its §9 ranks ten changes, and §11 says what the fix round did with each, and which calls it made or left for the owner. The owner has since kept Home opening on Private, and Swap and Mix behind dApps (§11’s pass two). Still the owner’s: renaming Make public, polling for money coming in and a password on the wallet’s own sends (§11’s Left for the owner), and from the usability review’s §9, UTxOs behind Advanced, two decimals and the splash below. Carried candidates:

dApp additions

Not counted in parity, and ongoing. The dApps page’s catalogue is the one part of the wallet that grows after feature-complete: each entry is a claim that the dApp works privately, through a one-time account with the money coming back, so each needs its own test run before it ships. Minswap is the first, Lovejoin is in, and A2 (O4) is the next shape of it.

✅ Chunk 24 (web-wallet/dapp-additions, record archive/plans/chunk-24-dapp-additions.md): more of the high-volume DEXes through Minswap’s router (the owner, 2026-10-06). SundaeSwap V3 is routed, its orders’ owner and destination read (and, since 1.3.0’s release review, every order’s cancel run through its script before the swap is signed); Danogo’s swaps against its pools are routed; SplashStable is out, its validator drained; routes are direct, one DEX a path; and Stop cancels every order itself, since Minswap’s cancel-tx and pending-orders couldn’t be relied on. Every route but Splash and Spectrum ran on mainnet. Minswap stays the only aggregator: Steelswap refuses an extension’s origin, and DexHunter makes the stake key a signer.

Minswap’s own site never offers Seedelf Wallet. Its wallet list is fixed (its WalletProvider enum), and inside a frame it connects only to Eternl, through Eternl’s bridge; answering as Eternl would be impersonation. Asking Minswap to list Seedelf Wallet would open its site to the public connector and private CIP-30 (chunk 15). ⬜ owner’s

The documentation review

✅ Chunk 22, 2026-10-04, on web-wallet/doc-review; the record is archive/plans/chunk-22-documentation-review.md. Brought forward ahead of the UX pass (the owner, 2026-10-04) rather than run after it, so the pass keeps the docs current as it goes instead of needing a second review (development.md).

What it settled, and what holds from here:

Left for later: the privacy docs get the data layer’s paragraph when there is a data layer — done in 1.4.0: privacy.md’s Network and the policy say what the service learns, and that the no-log promise is a policy and an audit trail rather than a proof.

Kept in mind

✅ Chunk 26 took the second candidate for the private side (the owner, 2026-10-08; in 1.4.0). Your own rows and the feed’s cursor are kept across a lock, sealed with the vault, so an unlock asks only for what changed. The public side still reads afresh after an unlock, from the data layer first on mainnet: keeping its last balances across a lock is still the owner’s call.

Left by the chunks and the reviews, moved here when plans/ was archived (2026-10-04) so they’re tracked somewhere live. None is a promise; each is a candidate, smallest first where it’s obvious.

Not planned

Shorter than it was: the owner has reopened the governance items for the public side (see above). What stays out:

Why hardware wallets can’t cover Seedelf

Not declined for effort — it doesn’t work, and the reason is one line of the derivation.

The Seedelf secret scalar comes from the BIP39 seed (derivation.rs, v1, frozen):

seed = BIP39 seed of the phrase          PBKDF2-HMAC-SHA512, 2048 rounds
okm  = HKDF-SHA-256(ikm = seed, salt = "seedelf-wallet-v1",
                    info = "seedelf-key" || u32_be(account), L = 64)
x    = int_be(okm) mod r
  1. A hardware wallet’s one guarantee is that the seed never leaves it. So outside the device there is no seed, and without the seed there is no x.
  2. Inside the device there is no path either. A Cardano app would have to implement Seedelf’s HKDF and then the BLS12-381 Schnorr proof over it (z = r + c·x, with c binding the one-time key hash). The Cardano apps sign Ed25519 over a transaction hash; none exposes arbitrary BLS12-381 scalar work, and none ever will on our ask.
  3. So hardware could only ever hold the public account’s CIP-1852 keys — half the wallet — while the private side still needs the phrase in the browser. A wallet that is half secured by a device is a worse thing to explain than one that isn’t.
  4. And it’s structural. The v1 derivation is frozen, because every existing phrase depends on it; there’s no redesign that reaches a device without being a v2 scheme no device implements either.

The same argument rules out air-gapped QR signing for the private side, for the same reason.

Open questions for the owner

Settled on 2026-10-02: P1’s key (account 0, one private balance), that it’s chunk 18, and — in the building — that connected sites use one chosen dApp account and that discovery asks Koios about one account at a time; P2’s languages (English, Spanish, Japanese); P3’s design (click to show); the data layer’s shape, and that it’s parked until the sequence is done; public-side completeness going after the accounts and before the UX pass; a documentation review; dark only; one phrase; and no analytics, AML/KYC or on-ramp. The web wallet README is tidied.

Settled on 2026-10-03: who reads the Spanish and Japanese privacy strings — accepted as recorded (the owner). The critical keys are back-translated, and docs/i18n/verified-critical-{es,ja}.json says in its own reviewer line that no native speaker has read them. A review pass went over every translation on 2026-10-03 (Claude again, no native speaker), and a fluent reader can be found later. The fallback (English-only for those strings) stays available, since the keys are an identified set.

Settled on 2026-10-04: the DRep correction — registering and voting are one chunk, chunk 21; and whether a plan says it’s live or finished — finished plans move to archive/plans/, so plans/ holds live specs only, and what still holds moves into the design docs first. Also that the documentation review runs before the UX pass, and that the docs don’t date releases or describe the store.

What’s left:

  1. rustls instead of native-tls? Raised in step 0: it would stop the eight openssl alerts recurring rather than patching them each release, and drop the CLI’s OpenSSL build dependency. A feature-flag change, not a pre-step.
  2. Spanish “Configuración” for Settings, in place of “Ajustes”. The whole-locale review chose it and it’s applied everywhere; the glossary marks it as the review’s choice, the owner’s to overrule.
  3. Is the web wallet in the bug bounty’s scope? SECURITY.md scopes it to “the code of the latest tagged release” of the repository. Every tag is the CLI’s, and the web wallet’s releases are never tagged, but a tag holds the whole repository: since the wallet merged into main, the latest CLI tag also holds the wallet’s code as it stood that day, which needn’t be any version in the store. Read literally, the wallet code in scope moves with every CLI tag, whatever version people have installed. In (at the commit a release’s handoff note names, or what’s in the store) or out? Either way, SECURITY.md gets one line that says so.
  4. A chain through Lovejoin once it’s all sent (1.3.0’s release review, C28). ❓ Since chunk 25 a chain’s transactions open from its row while it’s sent, but its last four, and all of a chain of four or fewer (a seed; a mix one wave deep of up to three boxes from the public account, or two from the private balance, whose chain ends with the return), leave the list and the view once sent, before they land. Your call of 2026-10-01 was that “audit is about the whole chain, afterwards, and belongs in Activity or the chain’s own row rather than in a dialog held open before Send”. Keep a sent chain viewable until its last transaction lands (a record of its own, and a look on chain or at Home’s watch to drop it), or audit it afterwards in Activity, where a public mix’s deposit and mixes read as Into Lovejoin and Lovejoin mix since chunk 28 (O7)?
  5. The reviews’ calls left for the owner, unchanged since they were written and re-checked for 1.4.1: the usability review’s (which side a transaction in flight holds, HM-3, and the calls to confirm: ST-3’s red Stop staking and Retire, GV-2, LJ-3, CW-3) and the blind test’s (its three open calls and a dozen to confirm or reverse, and a site-signed transaction’s change not counted as on its way). ✅ DX-8 (the usability review), chunk 29: every estimate is held to what was asked (quotesTheAsk), the amount, the pair and its paths’ sum, after nine live mainnet quotes gave amount_in back exactly. The wallet doesn’t check that Minswap’s quote is for the amount typed (quoteOf takes amount_in as it comes). A strict check is 3 lines, but refuses every swap if Minswap ever rounds amount_in, which only a live quote shows.
    • ❓ Should a restore keep running the restore scan by itself? (1.5.0’s release review, C07.) Every restore, Forgot password’s too, runs it beside its first reading of the public account: a batch of 20 one-time accounts the wallet volunteers, the kind chunk 18 declined for public accounts, which nothing on the Restore screen mentions (the policy does; Find leftovers’ note says what the button asks, not that a restore already asked it, and only Home’s and After a restore’s notes about an unfinished look, or Home’s about sessions found, imply it afterwards). Keeping it finds a session’s money for a user who wouldn’t know to look; saying so on the Restore screen, before Restore wallet, costs a critical string; leaving it to Find leftovers, which Home’s Wallet restored note would point to, is the most private default, but that money stays unseen until it’s pressed, and Remove wallet’s and Forgot password’s notes, the policy and the e2e restore’s request count change with it.