Seedelf-Wallet

Privacy

The protocol-level analysis lives in the root README: stealth addresses, re-randomization, and the known de-anonymizing attacks. This doc covers what the web wallet has to do on top of that.

The screens call the Seedelf balance the private balance and the Cardano account the public account; Make private is a move-in and Make public a withdrawal (flows.md, Names). This doc keeps the older names.

The owner’s rule (2026-09-27): the most private by default, with the ability to turn it down where it costs the user. Default never means only: a control turns a choice down where it costs fees, time or lookups. And the wallet never forbids a link the user chooses (2026-10-02): it says what a payment or a connection reveals, and only avoids making a link on the user’s behalf. The privacy-by-default review of 2026-09-27 tested the wallet against that rule, and the owner declined five of its proposals, so they shouldn’t come back as findings: a longer default Lovejoin wait (§3.5: it stays 1 to 6 hours), the currency off by default (§3.8: it stays US dollars), balances hidden by default (§3.9), a switch or a confirmation before money with different histories is spent together (§4.2), and a setting for whether new sites start on a private session or the public account (§4.3: the connect window preselects neither). archive/plans/privacy-review.md is the record; what’s still open is in post-release-roadmap.md. This doc says what the wallet does now.

What is hidden and what isn’t

Hidden, from people reading the chain:

Visible:

Rules the wallet enforces

These are not user settings:

  1. A new one-time signing key for every Seedelf spend.
    • The CLI draws it at random (transfer.rs, sweep.rs, util/mint.rs).
    • The web wallet derives it inside WebAssembly from the Seedelf key and a fresh random seed, so it survives a worker restart between review and Send without ever reaching JavaScript. A new seed gives a new key (see architecture.md).
  2. Seedelf spends take their collateral from the shared giveme.my service, never from a user UTxO. A user’s own collateral would tag every private spend with their address.
    • The one exception is a private session’s return merged into its funding change (chunk 16). It puts up the session’s own 5 ₳ collateral, which the same transaction spends. It tags nothing new: the transaction already spends the session’s UTxOs.
    • Its proof is still bound to a new one-time key (rule 1), never the session’s key, which a connected site can ask to sign.
  3. Re-randomization scalars (d) are toxic waste.
    • They are full-size and come from a secure random source.
    • They are never stored or logged.
    • They exist only inside seedelf-crypto.
  4. Registers are only built by seedelf-crypto, never assembled in TypeScript. This guarantees the same d is applied to both points and that the points are torsion-free. A mistake here locks funds permanently (see the root CLAUDE.md, “Core protocol invariants”).
    • A payment to someone’s Seedelf goes under a fresh re-randomization of their register, never the register as found.
    • The wallet refuses to pay a register whose points don’t decode, lie outside the prime-order subgroup, or are the identity. A payment to any of those is locked for good, or anyone can take it.
  5. A Seedelf is linked to whatever pays for it, so the first one is minted before any move-in (see flows.md).
    • By default the Cardano account pays (chunk 8b), and the Cardano card says to create a Seedelf before moving money in.
    • A stealth mint from the Seedelf balance is the other choice. It hides the payer only when that balance came from other people’s Seedelf payments. For it, the mint service passes only Seedelf UTxOs to WebAssembly, and WebAssembly refuses any it doesn’t own. Its coin selection takes received money first (see Co-spending).
    • This rule used to say “stealth mint from the Seedelf balance, never paid by the Cardano account”. That was wrong whenever your own move-in funded the balance: the mint spends that deposit and ties the account, the name and the mint’s change together.
  6. The Cardano account is never a one-time account. Each one-time account is used for a single session. One-time accounts use a reserved account index (24301'), never a low index like 1' that a restored Lace wallet may already use: sharing payment keys with a real account would link every one-time address back to the user.
  7. No analytics or telemetry. The wallet talks to Koios and giveme.my, on mainnet to its own data layer first (Network) and to CoinGecko for ADA’s price unless the currency is set to nothing, to Minswap’s aggregator only when the user swaps, and to Blockfrost’s IPFS gateway only when the user asks to see an NFT’s image, and to nothing else. Every request goes without cookies or a referrer (credentials: "omit", referrerPolicy: "no-referrer", privacy review §2.14), and none is kept in Chrome’s cache on the disk (cache: "no-store"). The data layer’s requests ask with cache: "default" instead, for a POST, so Chrome reuses its CORS preflight (in memory, up to 2 hours, keyed by the POST’s URL, query included, never its body), and the API answers every request with Cache-Control: no-store, so nothing it says is kept either (1.4.0, the owner’s call). A GET needs no preflight and stays no-store: its address can name an ADA Handle or a DRep ID, and only no-store keeps that off the disk whatever a server says (the release review). The query of a POST to the data layer can carry a block height the wallet last read, and past 1,000 rows an outpoint of the account’s: in memory alone, for up to 2 hours.
    • giveme.my and the data layer are ours: Logical Mechanism, who make Seedelf Wallet (and Lovejoin), run them. giveme.my sees the IP address and every Seedelf spend, and the data layer what Koios would (see Network). Koios, CoinGecko, Minswap and Blockfrost are other people’s.
    • CoinGecko is the one default that asks a third party for a convenience: the currency is US dollars unless the user changes it (the owner’s call, 2026-09-27; privacy review §3.8). “Nothing” asks no one.
    • Lovejoin is a contract, reached through Koios: nothing goes to it as a service. Its makers see each box brought back through giveme.my, as any Seedelf spend.
    • It asks the network the user has chosen, and the other only to finish what’s on its way there (the worker’s runs, architecture.md).
  8. A site is given the public account, or a private session’s one-time account, never the private balance (the dApp connector, chunks 15 and 15c). The Seedelf key and the Seedelfs are never offered over CIP-30. A private session’s funding is public, though, and leads back into the private balance (see A site connected to a private session). The connector is off until the user turns it on, and until then nothing is added to any web page.

The wallet can’t prevent these, so it should make them visible to the user instead of hiding them.

Holding and staking

Seedelf funds sit at an address with no staking part, so they earn no staking rewards. That’s the cost of not linking a user’s Seedelfs through a staking key. Since chunk 13, money that sits can stay in the Cardano account, staked with a pool, and move into Seedelf when it should move privately.