The mathematical construction is flawed: it conflates identity key with payment key, destroys the security boundary between scanning and spending, and trains users to treat root-equivalent material as "scan-only".
## TL;DR
A proposal is circulating to derive Silent Payment addresses directly from your Nostr `nsec`.
This is dangerous.
It makes your scan key root-equivalent to your identity key.
Anyone who gets your scan capability gets your `nsec` and your funds.
BAO Markets implements BIP-352 the correct way: hardened BIP-32 derivation with cryptographically separated scan/spend keys, and local-only scanning that never exposes private keys to any server.
---
## The Bad Idea
The proposal looks elegant at first:
```
t_scan = hash("nostr-sp/scan", npub)
t_spend = hash("nostr-sp/spend", npub)
scan_priv = nsec + t_scan
spend_priv = nsec + t_spend
```
Every Nostr identity gets a deterministic Silent Payment address. Reusable. Verifiable. Simple.
**Here's the fatal flaw:**
`t_scan` is publicly computable from your `npub`. That means:
```
nsec = scan_priv - t_scan
```
Anyone who learns your `scan_privkey` can recover your raw nsec and then derive your `spend_privkey`.
Your identity.
Your funds.
Everything.
## What BIP-352 Actually Requires
The Bitcoin specification is explicit:
- **Scan key** and **spend key** must be derived from **separate hardened BIP-32 paths**
- `m/352'/coin_type'/account'/1'/0` for scan
- `m/352'/coin_type'/account'/0'/0` for spend
These keys are **cryptographically independent**. Leaking the scan key reveals which outputs belong to you. It does **not** reveal the spend key. It does **not** reveal your seed. Your funds survive scan-key compromise.
This separation is the entire point of BIP-352's security model. Without it, you don't have Silent Payments. You have a fancy address format wrapped around a single-key wallet with extra steps.
## How BAO Markets Implements It
BAO's Silent Payments stack was built from scratch against the official BIP-352 v1.0.2 specification. All 57 official test vectors pass. Byte-for-byte compatible with Bitcoin Core, Sparrow, and every other compliant wallet.
### Key Derivation
```
scan = m/352'/coin_type'/account'/1'/0
spend = m/352'/coin_type'/account'/0'/0
```
Proper hardened derivation from a BIP-39 mnemonic. Scan and spend are independent. Your Nostr `nsec` is never used for Bitcoin key material.
### Local-Only Scanning (Protocol B1)
The BAO indexer stores only **public data** — tweak keys and output prefixes derived from transaction inputs. The client downloads batches and performs **all ECDH math in the browser**:
```
Client: ecdh = tweak_key * scan_privkey ← local, key never leaves device
Client: derives P0, P1... checks prefixes
Client: on match, fetches full tx, verifies scriptPubKey == P_xonly
```
**The server never sees `scan_privkey`. It never sees `spend_privkey`. It never sees ECDH results. It stores only public blockchain data that anyone could compute.**
### What the Server Knows
- Which height ranges you downloaded (coarse metadata)
- Your `scan_pubkey` (used for rate limiting, not for scanning)
That's it. Under a nation-state threat model with full VPS access, the attacker learns **which blocks you requested**. They do not learn which outputs are yours. They do not learn your keys. They cannot spend your funds.
### Browser Security
- Spending private keys are **never persisted** to `localStorage`
- Only public keys, address, and output metadata are stored
- Spending keys are re-derived in-memory from `spend_privkey + tweak` on every load
- If a malicious extension reads `localStorage`, it learns your output history. It does **not** learn your private keys.
---
## The "On-Chain Zaps" Discussion
There's growing talk about making Nostr zaps land on-chain as Silent Payments. The motivation is correct — reusable `npub` → private, non-reusable on-chain outputs. The privacy goal is correct.
But **deriving the Silent Payment keys from the `nsec` is the wrong path.**
If you build on-chain zaps this way:
- Every zap receiver who uses a remote scanner exposes their `nsec`
- Every zap sender who derives the address locally does so correctly, but the receiver's operational security is destroyed the moment they detect the payment
- The privacy harm is **durable and asymmetric** — the receiver can rotate, but the sender's payment history remains exposed if the scan key leaks
The correct architecture for on-chain zaps:
1. Nostr identity remains separate from Bitcoin wallet identity
2. Wallet derives SP keys from its own seed via BIP-352 paths
3. User publishes their `sp1q...` address on their Nostr profile
4. Senders derive outputs from the published SP address (not from `npub`)
5. Receivers scan locally or download tweak batches — scan key never transmitted
This preserves the privacy of both sender and receiver. It preserves the security of the Nostr identity. It is compatible with every BIP-352 wallet in existence.
---
## What BAO Ships Today
- ✅ Spec-compliant BIP-352 crypto (57/57 test vectors pass)
- ✅ Hardened BIP-32 key derivation with scan/spend separation
- ✅ Browser-based batch tweak-key download (Protocol B1)
- ✅ Real-time ZMQ indexer with `rawblock` + `sequence` ingestion
- ✅ WebSocket push notifications for new tweaks
- ✅ P2TR key-path sweep with `@scure/btc-signer`
- ✅ Full backend settlement service for sending to `sp1q...` addresses
- ✅ 250+ tests across crypto, scanner, hook, and API routes
Demo running now on signet at **bao.markets/demo**. Claim signet sats, generate a `tsp1q...` address, receive, scan, sweep. When mainnet launches, the same code runs against real Bitcoin.
---
## To Developers Building Zap Infrastructure
Please. Read BIP-352. Run the official test vectors. Do not derive Bitcoin private keys from Nostr private keys. Do not tell users to upload scan keys to servers. Do not train users to treat private key exposure as operationally acceptable.
The math is not forgiving. A single `nsec`-derived scan key uploaded to a Frigate-style indexer is not a privacy leak. It is total identity and fund compromise for every user who trusted your architecture.
