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.
web-wallet/<topic> from main, a plan in plans/, and a handoff note in roadmap.md when it lands. Tick it here and link its plan. When it lands, what still holds moves into the design docs and the plan moves to archive/plans/ (the owner, 2026-10-04), so plans/ only ever holds live specs; plans/README.md says how.web-wallet/* tag exists for 1.0.0 or 1.1.0: a release is recorded by its handoff note, which names the commit and the zip’s SHA-256 (1.1.0’s names neither yet: the owner has them). The version is decided when enough has accumulated to be worth one — a 1.x.0 bump for features, 1.x.y for a fix. If a tag is ever cut it must carry the slash, or release.yml ships signed CLI binaries off it (development.md).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.
Cargo.lock and the happy-path requirements.txt, no source change.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.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.
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.
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:
wasm/bench.mjs was broken and had to be fixed to measure anything. It called wasm.draftTransfer, which chunk 14 reshaped to take a payments list and the crypto review (5882024) then replaced, with every draft/finish pair, by one build* call that measures in the wallet. It is not in CI’s test glob (wasm/tests/*.test.mjs), which is why it rotted unnoticed. Fixed as part of this step; the same rename is still stale in three other places — wasm/README.md’s API table, architecture.md’s builder list, and extension/tests/fixtures/record-transfer.mjs, which is broken in the same way. For the documentation review. Fixed there (chunk 22), along with record-mint.mjs and record-withdraw.mjs, which step 0 missed and were broken the same way.wasm-release at 1,223 KB raw / 424 KB gzip, measured 2026-09-24; the module is 2,577 KB raw / 781 KB gzip today. Chunks 15–17 (CIP-30, Lovejoin, the transaction view) grew it, not this change. Re-recording it means measuring all three of its rows, which is more than a pre-step — for the documentation review, and the store zip’s size is worth a look at the same time. Re-recorded there (chunk 22, 2026-10-04): 2,632 KB raw / 797 KB gzip for wasm-release, against 7,100 KB / 1,474 KB for the plain release profile; chunks 18–20 added nothing to the module, and chunk 21 a little.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. ❓
Promises the repo has already made. These come before anything new.
| # | Item | Status |
|---|---|---|
| O1 | — | |
| 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 swaptx_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. 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. |
✅ |
| 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. 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. 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. |
✅ |
Three items, all done (2026-10-04). The wallet is feature-complete for Cardano, bar dApp additions.
✅ 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:
Preferences.dappAccount, withDappKeys in dapp.ts). It replaced a first build, the same day, that bound each site to the account it connected with and refused it elsewhere.Koios.usedStakeAddresses would answer for twenty in one request, and that request would tell Koios those twenty stake addresses are one wallet’s. Separate requests are less linkage, not none — Koios still sees them from one IP seconds apart, and privacy.md says so in those words.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:
originOf reading the prefixed form; it compares part === "public" exactly today.public — those records are already written, so the old form has to keep working.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.
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:
contract/i18n/src/translations/ — Lace ships en, es, ja.translation.language.name and translation.language.code, so the languages are a single source of truth and adding one is: write the JSON mirroring en.json’s keys, register it, and i18next and the picker pick it up (their translations/index.ts says exactly this at the top).en, with the system locale used when it’s one we ship (getSystemLanguage()).What it costs us, specifically:
tests/words.test.ts has to move with them. It parses source files for a lowercase “seedelf” in anything a person reads; once the text is in JSON it must check the locale files’ values instead — and Seedelf stays Seedelf in every language, so it can check them all, not just English.fallbackLng does this, and it matters most for the privacy notes, which the rules say may never be dropped.words.test.ts already pins the name: same sentence count, a negation where English has one, placeholders intact, Seedelf still capitalised, and never silently equal to the English or empty.✅ 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.
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.
✅ 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.iofirst. 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.
seedelf-data/ in this repo, with its own Cargo workspace.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.
TIMEOUT_MS is 45 s because one credential_utxos over an account with real history “can take tens of seconds on the public tier” (found on mainnet, 2026-09-28).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.
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:
block_height=gt.<last>, so steady state is small responses, cached the same way.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.
connect-src entry: no install warning, and nothing disabled on update./health per part, and per-part breakers that answer 503 so the wallet moves to Koios;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:
components/Hint.tsx and moved the transaction view’s six paragraphs behind icons; the owner’s wider point was that “a lot of the paragraphs we have could be like that”. Which ones is theirs to pick. Privacy callouts and warnings stay where they are — those are decisions, not explanations. ✅ Chunk 23: the owner said do it, and 23 went by a rule now in development.md; which others should go is theirs to say.select holds up past about a dozen accounts. ✅ Chunk 23: every window names it with several accounts; the select stays.worker.accounts.tooMany says “Remove one from the list”, and there’s no way to remove an account from it; worker.accounts.badIndex counts accounts from 0 while the screens count from 1. And retiring a DRep reads four ways in Spanish (“Retirarte como DRep”, “Da de baja tu DRep”, “retiras tu DRep”, a bare “Retira un DRep”) and two in Japanese (退任 for your own, 引退 for another’s) — the glossary should record one each once the strings agree (chunk 22’s review). ✅ Chunk 23.koios.staking.notDelegated, settings.staking.rewardsWait) since chunk 21 renamed the page Staking and governance. ✅ Chunk 23.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
✅ 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:
tests/fixtures/ and wasm/*.mjs too.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.
web+cardano payment links (CIP-13), so a link can open Send with the recipient filled in. ⬜_locales), and each language’s description and screenshots go in on the dashboard at the upload, from store/README.md.✅ 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.
since keeps answering 503 (not a 400, which starts again from a snapshot) reads Koios every time and keeps the cursor;tx_status or utxo_info on the public side: a chain’s resend refused as spent (sentAlready), a maybe-sent Seedelf spend’s mempool check (whose utxo_info names the wallet’s own contract UTxOs, as it did to Koios), and a site’s or a swap’s own transactions, which touch only key addresses.Server: Caddy and no HSTS or CORS headers; the wallet reads them as a lost connection, which is safe, and the deploy ships the binary alone, so a Caddyfile change is the owner’s by hand;query_typed for one round trip a statement, one pass for every DRep’s delegators, single-flight for answers kept a while, and live tests over the heaviest keys.credential_utxos ever measures slow.READ_ON): each asks credential_txs again only of the groups the page before moved on, and only the page that finds something asks tx_info. So a press lists nothing only after ten such pages in a row (about 200 rows: more than one of the largest public mixes since the pages were read), and the next press goes on from there. It never skips one, and always ends.ScriptSpend stages no invalid_hereafter, only the account builders do. It waits on a live check that giveme.my signs one.network_id or inputs are the other network’s, and the DRep key signs only over inputs the wallet finds (payment_signs in cip30.rs), so a vote or a DRep certificate can’t be signed over another network’s transaction: that half is closed. The stake key still signs over inputs the wallet can’t find, on purpose, for a site’s zero withdrawal (an_input_the_wallet_cant_find_is_unknown). So a site connected while the wallet is on preprod could have a mainnet delegation, vote delegation or deregistration signed, a deregistration’s 2 ₳ refund paid where the site likes; the prompt shows each certificate and where a refund goes. Refusing the account’s own certificates while any input is unknown, zero withdrawals still signed, would close it: about 5 lines and a test in seedelf-wasm. The owner’s call.crossCheck. A review the user abandons still shows Koios the Seedelf name, or the planned deposit.swaps.review.noBox) without the amount (its return now says why, 1.4.1). Of that list, #21 was settled by the independent review’s owner call 4. The h2 and spin items are still open: CLI only, and RustSec’s rather than Dependabot’s, so step 0 never saw them and bumped neither. h2 0.4.11 (reqwest) and 0.3.26 (warp’s hyper 0.14) are inside RUSTSEC-2026-0258, patched in 0.4.16, and 0.3.26 stays until warp leaves hyper 0.14; spin 0.9.8 (warp → multer) is yanked, with 0.9.9 current. Neither reaches the WebAssembly. They wait for the CLI’s next change: cargo update -p h2@0.4.11 -p spin, then step 0’s checks.topUpBuild, which only a site’s session takes), would mend it, from the session’s page.act) ends a session that runs itself only with its own return, so a swap or a mix whose account the same phrase in another browser emptied never moves again in this browser: its page waits for good, its Stop reads Cancelling for good, and its runs read the account every minute while the wallet is unlocked. The money is safe: the other browser brought it into the private balance, which is the phrase’s in both. An order this browser placed after the other’s scan can be cancelled only from a fresh restore’s found session (chunk 32’s 5 ₳). Not built: closing a running session from reads alone risks stopping a live one. A session the restore scan found ends by itself since 1.5.0 (C02); the runner was left as it is.MIX_FEE_ESTIMATE is 0.95 ₳ (seedelf-core/src/lovejoin.rs) against the 0.8225 ₳ a mainnet mix measured. sess.skip.warn.noBox), on the return’s warning, collateral or not.getCollateral answers null (chunk 12). That’s what Lace does now: Lace 2.x answers null too, and sets a collateral only from its account settings; only Lace 1.x up to 1.19 opened a window from the site’s request, paid the 5 ₳ and handed the site the new UTxO (chunk 31 looked; Eternl’s behaviour isn’t documented). CIP-30 marks getCollateral deprecated. A window would mean a password prompt and a payment started by a site, so it waits for a site that needs it. registry.mainnet.json, about 153 KB with 32 tokens); files would be smaller (chunk 12). cache: "no-store" (SERVICE_FETCH). Koios sends no caching headers, so nothing it answered was ever reused; CoinGecko’s 30 s of reuse was never relied on (the wallet keeps its own five minutes).stillConnected checks the session, not the public account, so changing The account sites use while a request waits has it signed with the new account’s keys over a transaction inspected for the old one. Harmless as it stands (the new keys own nothing in it, and signDappData refuses the address), but the request should be declined; recording the account with the request is the fix (chunk 22’s review).Tie that carries the account’s index, and words for it (the release review, C05’s public half; the UX pass’s).atStake reads the record alone). And the Lovejoin page’s Chains list keeps a mix’s chain a lock cut before any of it went as “Stopped after 0 of N transactions”, beside the chain the runner then sends through Lovejoin, until that record goes. Each needs a word of its own; none affects money.submitTx refusal no longer carries the node’s own reason (ValueNotConservedUTxO, say). Koios’s sentence around it is in the user’s language, and a site hears only English, so it hears the kind of refusal or that it wasn’t sent (the release review). KoiosError would have to carry the node’s text apart.orders.json already); VyFinance (a 56-byte owner, its datum by hash, 313 order scripts); CswapV1 (closed source).allow_multi_hops: false). Some pairs quote far better through two pools (200,000 MIN → iUSD: 766 iUSD against 379, 2026-10-06), but a later leg’s order is placed by the first leg’s batcher or by Minswap’s adapter (0e56d46a…, owned by Minswap’s key), which the session can neither check nor cancel. Turning it on needs that worked out first.liveOrders’ address_utxos, and since 1.3.0’s release review the walk back through tx_info to the session’s own order, C02) is tested on a synthetic mainnet record (tests/splash-remnant.test.ts), and the Rust side cancels a real Splash order. No partial fill is recorded, so what one leaves isn’t compared with a real one. Very low priority: Splash and Spectrum are dead in the water (the owner, 2026-10-08).REMNANT_HOPS).swaps.pause.warn.pricePools).npm run dreps writes what one Koios server holds (1.3.0’s release review, C37).account_reward_history on Koios, and a 22nd public route on the data layer for mainnet. Still open from it: a CSV of the rewards, for taxes, as Activity has one.Shorter than it was: the owner has reopened the governance items for the public side (see above). What stays out:
proposal_voting_summary, one more request an action).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
x.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.The same argument rules out air-gapped QR signing for the private side, for the same reason.
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:
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.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.quotesTheAsk), the amount, the pair and its paths’ sum, after nine live mainnet quotes gave amount_in back exactly. 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.