A client-side Bitcoin privacy analyzer. See your wallet as individual coins, name where they came from, and check a payment before you send it β in plain English.
Quick start Β· Features Β· How it works Β· Trust model Β· Development
Bitcoin has no single balance. Your wallet holds separate coins, each with its own history. When you pay someone, the wallet picks coins automatically β and if it picks two that came from different parts of your life, everyone watching the blockchain learns they belong to the same person. Forever.
flowchart LR
S["π¦ Salary coin<br/>0.20 BTC"]:::salary --> T{{"One payment<br/>of 0.23 BTC"}}:::tx
D["π© Donation coin<br/>0.05 BTC"]:::donation --> T
T --> R["Recipient"]:::out
T --> C["Change back to you"]:::out
T -. "now publicly linked" .-> L["π¬ Salary β· Donation<br/>same owner"]:::leak
classDef salary fill:#1e3a5f,stroke:#3987e5,color:#fff
classDef donation fill:#123f31,stroke:#199e70,color:#fff
classDef tx fill:#27272a,stroke:#f59e0b,color:#fff
classDef out fill:#18181b,stroke:#52525b,color:#e4e4e7
classDef leak fill:#450a0a,stroke:#ef4444,color:#fecaca
Most wallets hide this behind automatic coin selection. Satlas shows it to you before it happens β without holding your keys, replacing your wallet, or sending your data to a server.
| π Watch-only No private keys, no signing, no broadcasting |
π₯οΈ No backend All analysis runs in your browser via WASM |
π Wallet-agnostic Works beside Sparrow, BlueWallet, Electrumβ¦ |
π§Ύ Explainable Every conclusion says why, and how sure it is |
git clone https://github.com/CapThunder19/Satlas.git
cd Satlas/web
pnpm install
pnpm wasm # build the Rust engine to WebAssembly
pnpm dev # β http://localhost:5173- Click Explore with a demo wallet (or paste your own xpub / zpub / descriptor)
- Open Check a payment and type
0.23 - Watch Satlas warn you that it would link Salary and Donation β then click Show me for a safer way
| Verdict | Meaning | |
|---|---|---|
| π’ | Safe to send | Reveals nothing an observer couldn't already infer |
| π‘ | Mostly fine | Some history exposed, but no labelled coins newly linked |
| π΄ | Careful | Connects coins that were previously separate |
Satlas never dresses a heuristic up as a fact.
| Strategy | What it models |
|---|---|
| Biggest coins first | The default in most wallets |
| Oldest coins first | FIFO |
| Smallest coins first | Consolidation |
| Avoid change | Exact-match subset, no change output |
| Best for privacy | Stay inside one already-linked group |
| I'll pick the coins | Manual coin control |
- π·οΈ BIP-329 labels β export to / import from Sparrow and other compatible wallets
- π Real-wallet scanning β parallel gap-limit scan, retries, rate-limit backoff, automatic failover
- πΎ Remember this wallet β cached in IndexedDB, reopens instantly, refreshes in the background
- π οΈ Self-hosted backend β point Satlas at your own Esplora / mempool instance
- π§ͺ Demo wallet β synthetic history for trying it with zero setup
flowchart LR
U(["π€ xpub /<br/>descriptor"]):::you --> D
subgraph B["π₯οΈ Your browser"]
D["β Derive addresses<br/><sub>π¦ Rust Β· WASM</sub>"]:::rust --> S["β‘ Scan history<br/><sub>TypeScript</sub>"]:::ts
S --> A["β’ Coins + heuristics<br/><sub>π¦ Rust Β· WASM</sub>"]:::rust
A --> V["β£ Coins Β· Map<br/><sub>React</sub>"]:::ts
V --> P["β€ Simulate payment<br/><sub>π¦ Rust Β· WASM</sub>"]:::rust
V <--> I[("IndexedDB<br/><sub>labels Β· cache</sub>")]:::store
end
S <-->|"HTTPS Β· addresses only"| E[("Esplora API<br/><sub>mempool.space<br/>blockstream.info<br/>or your own</sub>")]:::ext
classDef you fill:#1c1917,stroke:#f59e0b,color:#fde68a
classDef rust fill:#3b1d12,stroke:#d95926,color:#fff
classDef ts fill:#132a45,stroke:#3987e5,color:#fff
classDef store fill:#18181b,stroke:#71717a,color:#e4e4e7
classDef ext fill:#1c1917,stroke:#f59e0b,color:#fde68a
π§ Rust compiled to WebAssembly Β· π¦ TypeScript / React Β· everything inside the box runs locally
sequenceDiagram
autonumber
actor You
participant UI as React UI
participant Core as satlas-core (WASM)
You->>UI: Amount 0.23 BTC
UI->>Core: simulateSpend(wallet, amount, strategy)
Core->>Core: Select coins (biggest first)
Core->>Core: Which groups & labels do the inputs span?
Core->>Core: Is the change output identifiable?
Core-->>UI: π΄ Careful β links Salary β· Donation
UI->>Core: simulateAll(every strategy)
Core-->>UI: "Best for privacy" β π’ Safe to send
UI-->>You: Verdict Β· why Β· what you can do Β· Show me
mindmap
root((satlas-core<br/>heuristics))
Common-input ownership
Coins spent together share an owner
Union-find over full history
Names the tx that merged groups
Address reuse
Same address received twice
Always a Fact
Change detection
Round payment amount
Script-type mismatch
Change to a reused address
Unnecessary input
Label mixing
Inputs carry different labels
From previously separate groups
Scanning a real wallet β the details
flowchart LR
A["Derive receive #0β¦n<br/>and change #0β¦n"] --> B{"Address has<br/>history?"}
B -- yes --> C["Fetch all pages<br/>until count matches"] --> D["Extend window:<br/>last used + gap"]
B -- no --> E{"20 unused<br/>in a row?"}
D --> A
E -- no --> A
E -- yes --> F(["Done β
"])
C -. "429 / timeout" .-> G["Back off & retry<br/>or fail over to<br/>next provider"] -.-> C
- Receive and change chains are scanned in parallel, each with its own worker pool
- Unused addresses cost one cheap stats request
- History pagination is checked against the provider's own transaction count, so nothing is silently dropped
Retry-Afteris honoured; each request times out after 15 s so a dead host fails over fast- Results are cached per descriptor, so reopening shows coins instantly while a refresh runs
flowchart LR
subgraph Never["β Satlas never"]
direction TB
N1["sees your seed or private keys"]
N2["signs or broadcasts"]
N3["runs a server or account"]
end
subgraph Does["β οΈ By default"]
direction TB
D1["asks a public block explorer<br/>about your derived addresses"]
end
subgraph Fix["β
To remove that"]
direction TB
F1["Settings β β your own<br/>Esplora / mempool server"]
end
Does --> Fix
The block explorer's operator can see which addresses you asked about β the same trade-off every watch-only wallet with a public backend makes. Point Satlas at your own server and no third party sees them. Labels and cached scans stay in your browser; Forget wipes both. Full details: docs/trust-model.md.
satlas/
βββ crates/
β βββ satlas-core/ π¦ Pure Rust engine β no network, no WASM, reusable as a library
β β βββ src/
β β βββ descriptor.rs xpub / zpub / descriptor parsing, address derivation
β β βββ wallet.rs transactions β UTXOs, balance, tx classification
β β βββ heuristics/ clustering Β· reuse Β· change detection Β· labels
β β βββ simulate.rs coin selection + linkage analysis
β β βββ bip329.rs label import / export
β βββ satlas-wasm/ wasm-bindgen bindings
βββ web/
β βββ src/
β βββ api/ Esplora client + gap-limit scanner
β βββ engine/ typed facade over the WASM engine
β βββ state/ zustand stores (wallet Β· labels Β· settings Β· cache)
β βββ components/ React UI (CoinGraph, Simulator, β¦)
β βββ demo/ synthetic demo wallet
βββ docs/ trust model, screenshots, banner
| Tool | Why | Install |
|---|---|---|
Rust stable + wasm32-unknown-unknown |
the engine | rustup.rs (pinned by rust-toolchain.toml) |
wasm-pack |
Rust β npm package | cargo install wasm-pack |
clang |
compiles secp256k1 for WASM |
Windows: winget install LLVM.LLVM + add C:\Program Files\LLVM\bin to PATH |
| Node 22+ and pnpm | the UI | npm i -g pnpm |
| Command | Where | What |
|---|---|---|
cargo test |
repo root | engine tests |
pnpm wasm |
web/ |
release build of the engine β web/src/wasm/pkg |
pnpm wasm:dev |
web/ |
faster, unoptimised engine build |
pnpm dev |
web/ |
dev server on http://localhost:5173 |
pnpm test |
web/ |
UI + WASM-boundary tests |
pnpm typecheck |
web/ |
TypeScript strict check |
pnpm build |
web/ |
production build β web/dist |
Tip
Re-run pnpm wasm whenever you change Rust code.
Live network test against a real mainnet wallet
SATLAS_NETWORK_TESTS=1 pnpm test
SATLAS_NETWORK_TESTS=1 SATLAS_ESPLORA=https://blockstream.info/api pnpm testRuns import β scan β analyse end to end on the public BIP-84 test-vector wallet.
Troubleshooting
| Error | Fix |
|---|---|
failed to find tool "clang" |
Install LLVM and put C:\Program Files\LLVM\bin on PATH |
Bulk memory operations require bulk memory |
Already handled by the wasm-opt flags in crates/satlas-wasm/Cargo.toml |
| pnpm won't run esbuild's install script | pnpm approve-builds esbuild |
npm.ps1 cannot be loaded (PowerShell) |
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned |
Note
Satlas describes what a typical chain-analysis observer would infer. It is not a privacy guarantee, and CoinJoin / PayJoin transactions can defeat these heuristics.
- Observer-side change detection covers the common one payment + change shape; batched spends aren't analysed
- Fee estimates assume
p2shinputs are wrapped segwit - The Avoid change search considers at most 16 coins
- A first scan of a large wallet over a public API can take tens of seconds
- Descriptor & xpub import, UTXO discovery
- Clustering, reuse and change heuristics with certainty levels
- Pre-spend simulator with six strategies
- BIP-329 export & import
- Relationship map
- Self-hosted endpoint, caching, failover
- Publish
satlas-coreas a standalone crate - Incremental refresh (only re-check addresses with new activity)
- Change analysis for batched spends
- Import a PSBT to check the exact transaction your wallet built
Satlas β because "which coin should I spend?" is really "what will spending this reveal?"
MIT License Β· built with π¦ Rust, βοΈ React and βΏ Bitcoin



