The unified dashboard: collapsing private and transparent into one wallet view
Sections
The 2026-04-17 commit message — wallet-ui: merge privacy view into unified dashboard + rescan endpoint — is one of the smallest functional commits in the Vanta repo and one of the most consequential UX decisions. Up to that point the wallet had a /dashboard page (transparent UTXOs) and a separate /privacy page (shielded notes). Two pages. Two balance numbers. Two transaction feeds. Users — including, embarrassingly, me — would forget which page they were on and wonder why a transaction “didn’t arrive” when it had simply landed on the other page.
The fix was to collapse them. This post is about why that decision matters more than it looks, what the new layout does, and the discipline a privacy chain needs to keep when it ships a wallet.
Why two pages was wrong
The original split came from a literal reading of the architecture. The chain has a transparent layer (the L1 UTXO set) and a privacy layer (the L2 SMT of commitments). The wallet had two stores backing two pages. Easy mental model for a developer.
For a user, this is the wrong frame. A user has money, and the money is in different states. Transparent and shielded are states the money happens to be in, like “in checking” vs “in savings.” A bank doesn’t make you flip between two browser tabs for those. The user thinks “what’s my balance” and “what came in lately” — not “let me check my transparent feed and my shielded feed.”
Worse, two pages teaches people that the privacy/transparent boundary is something they have to think about. It is — sometimes. But mostly the wallet should handle the boundary and present the actual account. The boundary should be visible inside the data (each note or UTXO has a privacy badge), not in the URL.
What the unified dashboard does
The new wallet-ui/src/pages/dashboard.tsx is the page. The structure is roughly:
- One balance, prominently — labelled “Private Balance” because that’s what 99% of the value should be once the chain is mature, with the small print being “X VANTA transparent” if the user has any.
- L2 status card — SMT root, commitment count, nullifier count, last block height. Auto-refreshing every 5 seconds. This is the chain’s privacy view, surfaced as a number on the dashboard, not hidden in a settings page.
- Quick actions — send, receive, sync.
- One activity feed — interleaving transparent transactions and shielded notes by timestamp. Each row has a privacy badge (
ShieldCheckicon for shielded,EyeOfffor transparent) and the badge is the indicator of which state the value is in.
The L2 status card auto-fetches on a setInterval:
useEffect(() => {
fetchL2Status()
const id = setInterval(fetchL2Status, 5000)
return () => clearInterval(id)
}, [])
5 seconds is a deliberate cadence. The chain produces blocks every 60 seconds. SMT root updates land at most once per block, on average. 5 seconds gives the user a perception of “this is live” without hammering the L2 sidecar’s REST endpoint.
The rescan endpoint
The other piece of the commit is a /api/sync endpoint (and the matching sync() action in the Zustand store) that triggers a re-scan of L1 + L2 against the wallet’s keys. The rescan reads:
- the L1 transparent UTXOs the wallet’s addresses control
- the L2 encrypted-note inbox, trial-decrypting against the wallet’s secret to find shielded notes addressed to it
Before this endpoint, “my balance is wrong” was an unrecoverable error state — the user would have to restart the wallet. With the rescan endpoint, “my balance is wrong” is a button click. The button reports { newNotes, scannedToIndex, balance, unspentCount } so the user sees something concrete: “found 2 new notes.”
This is the kind of feature that’s invisible until you don’t have it, at which point support tickets stack up. Shipping it alongside the unified dashboard was the right pairing — the dashboard makes the user expect their balance to be live; the rescan endpoint backstops them when it isn’t.
The badge discipline
The activity feed shows transparent and shielded events together. Each row gets a privacy badge:
ShieldCheck(purple) — shielded transactionEyeOff(purple) — incoming shielded noteHash— transparent transactionLayers— L2-only event (commitment landed but not yet associated with a wallet note)
The colour discipline is consistent across the wallet: purple is the privacy-feature colour, used for L2 elements and shielded states. Transparent elements use the default text colour. The viewer doesn’t have to read a label to know which is which.
This is small. It also took longer than I expected to settle on. Earlier drafts had transparent transactions in green and shielded in purple, on the theory that “green = good, purple = brand colour.” That backfired immediately — green coded as “fine, no need to look closer” and purple as “interesting, look closer,” when on Vanta the desired hierarchy is the opposite (privacy is the default, transparent is the exception).
The current discipline: shielded is the unmarked default; transparent is marked by being non-shielded. A row without a special badge isn’t transparent; it’s shielded. A row with a transparent badge is the exception. Visual weight matches the expected long-run distribution.
Why this is hard
Privacy-coin wallets have shipped with two-pane “shielded vs transparent” UX for years, and it’s mostly not their fault. The frame leaks from the chain when the chain treats shielded as a separate pool. On Zcash, you literally have shielded addresses (zs1...) and transparent addresses (t1...) — two different address families — and a wallet has to render that.
Vanta dodges that frame because the chain treats commitments and UTXOs as two states of the same value, with a single address family (vnt1...) on top. That gives the wallet room to present a unified view. The wallet has to take the room, which is the part the dashboard collapse is doing.
The principle: the wallet’s frame should match the chain’s frame, not the wallet’s data model. The data model has commitments and UTXOs and an L2 sidecar and an L1 RPC. The frame the user sees should be “money in, money out, what’s it doing.” The data model is the wallet’s problem.
What I’d ship next
Three things on the list, in priority order.
- Per-note privacy decay indicator. Coinbase rewards land transparent for one confirmation before they private-decay (the “fast privacy decay” pattern in
papers/01-executive-summary.md). The wallet should show, on each row, whether a note is in its decay window. If it is, the user gets a “wait one block before spending” hint. Today the wallet doesn’t surface this — a power-user can read it from the L2 status, but no normal user will. - “Send” with no privacy choice. The send flow today asks “transparent or private?” — even though the answer is almost always private. Make private the default; offer a “transparent send” advanced option behind a disclosure. Most users will never need to know transparent sends exist.
- Address book scoped to the wallet. Privacy-respecting wallets often skip address books because of the linkability concern. Vanta can do this in the wallet, since the wallet is the only thing that knows which addresses the user has interacted with. Address book entries don’t leak to the chain. This was the user-facing thing missing the longest.
The dashboard collapse is the foundation; these are the next-step UX wins it enables.
The architectural lesson
The dashboard refactor is small but it’s an example of the larger principle that runs through the whole wallet: the privacy boundary is in the data, not the URL. Two URLs implies two domains of knowledge a user has to manage. One URL, with private/transparent as a property of each row, implies the wallet manages this and presents one cohesive thing.
I want this principle to extend. The settings page shouldn’t have a “privacy” tab. The send flow shouldn’t have a “privacy” toggle as a primary control. The receive page shouldn’t ask the user to choose between a shielded and a transparent address. Privacy is the default; transparent is the exception; everything else is the wallet’s job to handle.
Some of those changes are shipped. Some are on the list. The dashboard collapse was the one that mattered most because it landed first and it set the discipline for everything else.
Further reading
wallet-ui/src/pages/dashboard.tsx— the unified dashboardwallet-ui/src/pages/privacy.tsx— the old privacy view (kept for a while as a deep-dive page)wallet-ui/src/stores/privacy-store.ts— the Zustand store the dashboard pulls from- The vanta wallet HTTP API — the L1 service the dashboard calls
- The vanta sidecar architecture — the L2 service the dashboard calls
- Vanta Desktop: a Tauri wallet that ships its own full node — where this dashboard ends up living for end users