On-Chain Zaps
On-chain zaps enable direct Bitcoin transfers to any Nostr user without Lightning Network intermediaries. By leveraging the shared cryptography between Nostr and Bitcoin Taproot, you can send Bitcoin to anyone's npub - whether they're ready to receive or not.
Why On-Chain Zaps Matter
On-chain zaps embrace what makes Nostr special: your npub is already a Bitcoin address. No intermediaries, no setup, no permission needed.
| Aspect | Lightning Zaps | On-Chain Zaps |
|---|---|---|
| Recipient setup | LNURL, Lightning address, wallet config | None - npub is enough |
| Sender setup | Funded channels, liquidity management | Any Bitcoin wallet |
| Availability | Recipient online, channels open | Funds wait on-chain |
| Amount limits | Channel capacity constraints | Unlimited |
| Permanence | Relay-dependent receipts | Blockchain-secured |
| Privacy | Route-based (good) | Configurable (raw → tweaked → silent) |
Lightning has its place for high-frequency micropayments, but on-chain is the native path - same cryptography, no layers between.
How It Works
The Cryptographic Bridge
Nostr and Bitcoin Taproot use identical cryptography:
Both are x-only public keys on secp256k1. The encoding differs, but the underlying key is the same.
Deriving a P2TR Address
import { nip19 } from 'nostr-tools';
import * as bitcoin from 'bitcoinjs-lib';
function npubToP2TR(npub) {
// Decode npub to get raw pubkey
const { data: pubkey } = nip19.decode(npub);
// Create P2TR address (x-only pubkey, same as Nostr)
const { address } = bitcoin.payments.p2tr({
internalPubkey: Buffer.from(pubkey, 'hex'),
network: bitcoin.networks.bitcoin
});
return address; // bc1p...
}
// Example
const npub = 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m';
const btcAddress = npubToP2TR(npub);
// bc1p... derived directly from npub
Simple On-Chain Zap Flow
Client Support
Ditto
Ditto 2.12 shipped on-chain zaps first:
- One-click on-chain tips
- Automatic address derivation
- Integrated with Soapbox UI
Amethyst
Android's leading Nostr client added support within 24 hours:
- On-chain zap option alongside Lightning
- Wallet integration
- Transaction tracking
More Coming
The simplicity of the approach means any client can add support:
- Derive P2TR from recipient's npub
- Generate a Bitcoin URI or QR
- User pays with any Bitcoin wallet
Privacy Considerations
On-chain zaps create a permanent, public link between your npub and Bitcoin transactions. Consider the tradeoffs.
The Privacy Tradeoff
| Aspect | Implication |
|---|---|
| Address reuse | All zaps to same npub go to same address |
| Public history | Anyone can see total received |
| Sender correlation | Your funding source is visible |
| Dust attacks | Anyone can send tiny amounts |
Choosing Your Privacy Level
| Use Case | Recommended Approach |
|---|---|
| Public tips (attribution wanted) | Raw npub → P2TR |
| Moderate privacy | Tweaked keys |
| High privacy | Silent Payments + NIP-17 |
When On-Chain Shines
- Any amount - No channel capacity limits
- No recipient setup - Works for any npub, even if they've never configured payments
- Permanent proof - Blockchain receipt, not relay-dependent
- True self-custody - No intermediary nodes
- Offline receiving - Funds arrive whether recipient is online or not
When Lightning May Fit
- Sub-second settlement needed
- Very high frequency (dozens per day)
- Recipient has reliable Lightning infrastructure
Simple Tweaks: A Privacy Middle Ground
Between raw npub derivation (fully public) and Silent Payments (complex), there's a practical middle ground: tweaked keys.
The Problem with Raw Derivation
npub1abc... → bc1pabc...
Anyone can compute this mapping. Your npub becomes a transparent window into your Bitcoin holdings.
Tweaked Key Approach
Instead of deriving P2TR directly from the raw npub, apply a tweak:
import { sha256 } from '@noble/hashes/sha256';
import { secp256k1 } from '@noble/curves/secp256k1';
function tweakedP2TR(npubHex, tweakData = 'nostr-zap') {
// Compute tweak: H(pubkey || domain)
const tweak = sha256(
Buffer.concat([
Buffer.from(npubHex, 'hex'),
Buffer.from(tweakData)
])
);
// Tweaked pubkey: P' = P + tweak*G
const pubPoint = secp256k1.ProjectivePoint.fromHex(npubHex);
const tweakPoint = secp256k1.ProjectivePoint.BASE.multiply(
BigInt('0x' + Buffer.from(tweak).toString('hex'))
);
const tweakedPub = pubPoint.add(tweakPoint);
// Derive P2TR from tweaked key
return deriveP2TR(tweakedPub.toHex());
}
Privacy Tradeoff Spectrum
| Approach | Privacy | Complexity | Recipient Setup |
|---|---|---|---|
| Raw npub → P2TR | None | Trivial | None |
| Tweaked key | Moderate | Low | Know the tweak |
| Silent Payments | High | Moderate | Wallet support |
| Fresh key per tx | Maximum | High | Per-tx coordination |
How Tweaks Help
- Address unlinkability:
bc1p-tweakedcan't be trivially mapped back tonpub1... - Still spendable: Owner knows their nsec + tweak, can derive the spending key
- Deterministic: Same sender + recipient + tweak = same address (good for recurring)
- No scanning: Unlike Silent Payments, no blockchain scanning needed
Sender-Specific Tweaks
For better privacy, use sender-specific tweaks:
// Sender includes their pubkey in the tweak
const tweak = sha256(senderPubkey + recipientPubkey + 'zap');
// Now each sender→recipient pair has a unique address
The recipient can try known sender pubkeys to find payments, or senders notify via NIP-17 DM.
Silent Payments: Privacy-Enhanced On-Chain
For maximum privacy, Silent Payments (BIP-352) combined with Nostr notifications solve address reuse:
How Silent Payments Work
Nostr as Notification Layer
Instead of on-chain notifications (OP_RETURN), use NIP-17 encrypted DMs:
// Sender creates notification for recipient
const notification = {
kind: 14, // NIP-17 chat message (sealed + gift-wrapped)
content: JSON.stringify({
type: 'silent_payment',
sender_pubkey: alicePubkey,
counter: i,
txid: transactionId
}),
tags: [['p', bobPubkey]]
};
// Wrapped per NIP-17 for privacy
// Bob receives everything needed to find and spend the UTXO
Benefits:
- No address reuse (each payment gets unique address)
- No on-chain scanning required
- Private notification via encrypted DMs
- Works with existing Nostr infrastructure
Integration with Wallets
Sparrow Wallet
Sparrow has a "color-address" plugin demonstrating:
- Stealth address generation from npub
- NIP-17 notification broadcasting
- Automatic UTXO detection
Future: NWC for On-Chain
Nostr Wallet Connect (NIP-47) could extend to on-chain:
// Hypothetical NWC on-chain method
{
"method": "pay_onchain",
"params": {
"address": "bc1p...",
"amount": 100000, // sats
"fee_rate": 10 // sat/vB
}
}
Implementation Guide
For Client Developers
- Add P2TR derivation - Convert npub to bc1p address
- Generate payment URI -
bitcoin:bc1p...?amount=0.001 - Show QR code - User scans with any Bitcoin wallet
- Optional: Track payments - Monitor address for incoming txs
For Wallet Developers
- Recognize npub inputs - Auto-convert to P2TR
- Support Silent Payments - BIP-352 + NIP-17 notifications
- Integrate NWC - Remote signing for on-chain
Example: Minimal On-Chain Zap Button
import { nip19 } from 'nostr-tools';
import { payments, networks } from 'bitcoinjs-lib';
function OnChainZapButton({ recipientNpub, amountSats }) {
const handleZap = () => {
const { data: pubkey } = nip19.decode(recipientNpub);
const { address } = payments.p2tr({
internalPubkey: Buffer.from(pubkey, 'hex'),
network: networks.bitcoin
});
const btcAmount = amountSats / 100_000_000;
const uri = `bitcoin:${address}?amount=${btcAmount}`;
window.open(uri); // Opens user's Bitcoin wallet
};
return <button onClick={handleZap}>Zap On-Chain</button>;
}
Comparison: Lightning vs On-Chain Zaps
| Feature | Lightning (NIP-57) | On-Chain |
|---|---|---|
| Speed | Instant | 10-60 min confirmation |
| Fees | ~1 sat (but channel costs) | Market rate (often under $1) |
| Privacy | Route-based | Configurable (tweaks/silent) |
| Setup | LNURL + wallet + channels | None (npub = address) |
| Max amount | Channel limited | Unlimited |
| Offline receive | No (must be online) | Yes |
| Proof | Relay-dependent receipt | Blockchain-permanent |
| Complexity | High (liquidity mgmt) | Low (just Bitcoin) |
| Native to Nostr | No (separate system) | Yes (same keys) |
Testing on Testnet
On-chain zaps are perfect for testnet experimentation - same cryptography, zero risk.
Why Use Testnet
| Benefit | Description |
|---|---|
| Free coins | Faucets provide test sats |
| Same code paths | Identical derivation logic |
| Safe iteration | Mistakes cost nothing |
| Client testing | Verify UX before mainnet |
Deriving Testnet Addresses
import { payments, networks } from 'bitcoinjs-lib';
function npubToTestnetP2TR(npubHex) {
const { address } = payments.p2tr({
internalPubkey: Buffer.from(npubHex, 'hex'),
network: networks.testnet // tb1p... address
});
return address;
}
Testnet4 Faucets
Testnet4 is the current recommended testnet - more stable than testnet3, actively maintained.
- mempool.space/testnet4/faucet - Mempool faucet
- coinfaucet.eu - Coinfaucet
- faucet.testnet4.dev - Testnet4.dev
- testnet4.info - Testnet4.info
See awesome-testnet4 for more resources.
Testnet3 (Legacy)
Still works, but testnet4 is preferred:
- coinfaucet.eu/btc-testnet - Testnet3
Which Network?
| Network | Status | Best For |
|---|---|---|
| Testnet4 | Current | All testing (recommended) |
| Testnet3 | Legacy | Existing integrations |
| Regtest | Local | Development |
Proof of Publication
An underappreciated property: on-chain zaps create immutable, timestamped proof that a payment was made to a specific Nostr identity.
What You Get
The blockchain permanently records:
- When: Block timestamp (unforgeable)
- How much: Exact satoshi amount
- To whom: P2TR address → npub mapping is deterministic
Use Cases for Proof of Publication
| Use Case | How It Helps |
|---|---|
| Provable donations | "I donated X sats to @developer on date Y" |
| Grant accountability | Public record of fund distribution |
| Patronage history | Verifiable support timeline |
| Contract payments | Timestamped proof of payment |
| Dispute resolution | Immutable payment evidence |
Verification
Anyone can verify a claimed on-chain zap:
function verifyOnChainZap(txid, npub, expectedAmount) {
// 1. Fetch transaction from any block explorer
const tx = await fetchTransaction(txid);
// 2. Derive expected address from npub
const expectedAddress = npubToP2TR(npub);
// 3. Check outputs
const matchingOutput = tx.outputs.find(
o => o.address === expectedAddress && o.value >= expectedAmount
);
return {
verified: !!matchingOutput,
blockHeight: tx.blockHeight,
timestamp: tx.blockTime,
amount: matchingOutput?.value
};
}
Contrast with Lightning
| Aspect | Lightning Zap | On-Chain Zap |
|---|---|---|
| Public proof | Zap receipt (kind 9735) | Blockchain tx |
| Immutability | Relay-dependent | Bitcoin-secured |
| Timestamp | Event created_at | Block timestamp |
| Verifiable by | Nostr users | Anyone with internet |
| Permanence | Relay retention | Forever |
Lightning zap receipts are Nostr events - they can be lost if relays purge data. On-chain transactions are permanent.
Combining Both
For important payments, do both:
- On-chain zap → Permanent blockchain proof
- Nostr announcement → Social visibility
{
"kind": 1,
"content": "Just supported @developer with an on-chain zap! 🔗",
"tags": [
["p", "developer_npub"],
["r", "https://mempool.space/tx/abc123..."]
]
}
Spending Your On-Chain Zaps
You've received on-chain zaps to your npub-derived P2TR address. Now how do you spend them securely?
The Security Model
Your nsec controls both your Nostr identity AND your Bitcoin. Exposing it to spend Bitcoin risks your entire identity. Solution: hardware wallet + PSBT.
Watch-Only Wallet Setup
Import your npub-derived key as watch-only (no private key on the computer):
Sparrow Wallet:
- File → New Wallet
- Select "Airgapped Hardware Wallet"
- Import your x-only pubkey (from npub)
- Sparrow watches
bc1p...for incoming funds
// Your watch address
const watchAddress = npubToP2TR(yourNpub);
// Sparrow monitors this - sees all incoming on-chain zaps
PSBT Workflow
PSBT (Partially Signed Bitcoin Transaction) lets you build transactions on an online machine and sign on an offline device.
Hardware Wallet Options
| Device | P2TR Support | Air-Gap Method | Nostr Signing |
|---|---|---|---|
| Coldcard | Yes | MicroSD, QR | No |
| SeedSigner | Yes | QR only | No |
| Ledger | Yes | USB | Via app |
| Trezor | Yes | USB | No |
| Jade | Yes | QR, USB | No |
For maximum security: Use QR-based air-gap (Coldcard Q, SeedSigner, Jade). Device never connects to computer.
Step-by-Step: Coldcard + Sparrow
One-time setup:
- Generate seed on Coldcard (or import your nsec-derived seed)
- Export xpub via MicroSD
- Import xpub into Sparrow as watch-only
- Verify address matches your npub-derived P2TR
Spending:
- Sparrow: Create transaction, select UTXOs, set destination and fee
- Sparrow: Save PSBT to MicroSD (or display as QR)
- Coldcard: Load PSBT, verify outputs, sign
- Coldcard: Save signed PSBT to MicroSD
- Sparrow: Load signed PSBT, broadcast
QR-Based Air-Gap Flow
For devices with cameras (Coldcard Q, SeedSigner, Jade):
┌─────────────┐ ┌─────────────┐
│ Sparrow │ QR -> │ Hardware │
│ (online) │ │ (offline) │
│ │ <- QR │ │
└─────────────┘ └─────────────┘
No cables, no MicroSD, no USB - just cameras and screens. The air-gap is physically visible.
Deriving Hardware Wallet from nsec
Your nsec is 32 bytes of entropy - same as a BIP-39 seed. You can:
Option A: Import nsec as seed
// Convert nsec to 24-word mnemonic
const mnemonic = entropyToMnemonic(nsecBytes, wordlist);
// Import this mnemonic into hardware wallet
Option B: Use same derivation path
m/86'/0'/0'/0/0 → P2TR address
Ensure the derived P2TR matches your npub-derived address.
Option C: Keep separate
Use hardware wallet's own seed for Bitcoin, keep nsec for Nostr only. Less elegant but simpler backup story.
Signing Nostr Events with Hardware
Currently limited, but emerging:
| Method | Status | How |
|---|---|---|
| NIP-46 | Working | Remote signer, hardware as backend |
| Nostr Signing Device | Experimental | Dedicated firmware |
| USB HID | Proposed | Sign events via hardware |
For now, most users keep nsec hot for Nostr events (low value) and use hardware only for Bitcoin spending (high value).
Security Checklist
- Watch-only wallet on daily computer (no private keys)
- Hardware wallet stores seed offline
- Verify addresses on hardware screen before signing
- Use air-gap (QR/SD) over USB when possible
- Test with small amount first
- Backup seed phrase on metal (fire/flood proof)
When Hardware Overkill?
| Amount | Recommendation |
|---|---|
| Under 100k sats | Hot wallet fine |
| 100k - 1M sats | Consider hardware |
| Over 1M sats | Hardware required |
| Over 10M sats | Hardware + multisig |
For small on-chain zaps, spending directly with nsec (via a good wallet) is acceptable. Hardware becomes essential as amounts grow.
Browser Extension Signing
Browser extensions (Alby, nos2x, Nostore) manage your nsec and sign Nostr events. Could they sign Bitcoin transactions too?
NIP-07: What Extensions Offer Today
// Standard NIP-07 methods
window.nostr.getPublicKey() // Get npub
window.nostr.signEvent(event) // Sign Nostr event
window.nostr.nip04.encrypt(pubkey, plaintext)
window.nostr.nip04.decrypt(pubkey, ciphertext)
// Less common
window.nostr.signSchnorr(message) // Raw Schnorr signature
| Method | Purpose | Bitcoin Use? |
|---|---|---|
signEvent | Nostr events | No |
signSchnorr | Raw message | Theoretically yes |
| PSBT signing | Not in spec | No |
The signSchnorr Danger
signSchnorr(message) signs arbitrary bytes. In theory, you could:
// DON'T DO THIS - example only
const txHash = bitcoin.Transaction.hashForSignature(tx, 0, prevout);
const sig = await window.nostr.signSchnorr(txHash);
Why this is dangerous:
- No context - Extension shows hex blob, not "Send 0.5 BTC to bc1p..."
- No validation - User can't verify what they're signing
- Phishing magnet - Malicious site shows "Sign this message" but it's a tx
If a site asks you to signSchnorr a hex string for "verification" or "proof of ownership," it could be a Bitcoin transaction draining your funds. Legitimate apps don't need raw Schnorr signatures.
What Safe Extension Signing Would Look Like
A proper Bitcoin signing flow needs:
// Hypothetical future NIP-07 extension
window.nostr.bitcoin.signPSBT(psbtBase64, {
// Extension parses PSBT and shows:
// - Inputs (your UTXOs being spent)
// - Outputs (destinations and amounts)
// - Fee
// - Change address
})
The extension would display a human-readable summary before signing:
┌─────────────────────────────────────┐
│ Sign Bitcoin Transaction? │
├─────────────────────────────────────┤
│ Sending: 0.001 BTC │
│ To: bc1pxyz... │
│ Fee: 450 sats │
│ Change: bc1pabc... (yours) │
├─────────────────────────────────────┤
│ [Reject] [Sign] │
└─────────────────────────────────────┘
Current State of Extension Bitcoin Support
| Extension | Lightning | On-Chain | PSBT |
|---|---|---|---|
| Alby | Yes (NWC) | Experimental | No |
| nos2x | No | No | No |
| Nostore | No | No | No |
| Flamingo | No | No | No |
Alby is exploring WebBTC integration, but it's early. For now, extensions are Lightning-focused.
Extension vs Hardware vs NWC
| Method | Security | UX | Best For |
|---|---|---|---|
| Extension (signSchnorr) | Low | Easy | Never for Bitcoin |
| Extension (future PSBT) | Medium | Easy | Small amounts, when available |
| Hardware wallet | High | More steps | Serious amounts |
| NWC (Lightning) | Medium | Easy | Zaps, micropayments |
Recommendations
For Lightning zaps: Use extensions with NWC - this is mature and safe.
For on-chain spending:
- Small amounts → Desktop wallet (Sparrow) with your nsec
- Larger amounts → Hardware wallet + PSBT
- Extensions → Wait for proper PSBT support
Never:
- Sign raw hex via
signSchnorrfrom websites - Trust "verification" requests that need signatures
- Use experimental Bitcoin features for real funds
Future: NIP for PSBT Signing
A future NIP could standardize:
// Proposed extension method
window.nostr.bitcoin = {
// Get P2TR address derived from npub
getAddress(derivationPath),
// Sign PSBT with proper UI
signPSBT(psbtBase64, options),
// Get xpub for watch-only setup
getXPub(derivationPath)
}
This would bridge Nostr extensions to on-chain Bitcoin safely. Until then, use dedicated Bitcoin wallets for on-chain.
Best Practices
For Senders
- Check for Lightning first - If recipient has lud16, use Lightning
- Confirm large amounts - On-chain is permanent
- Consider fees - Batch if sending to multiple recipients
- Use Silent Payments - When privacy matters
For Recipients
- Monitor your address - Funds may arrive unexpectedly
- Consider privacy - Your on-chain history becomes public
- Set up Lightning too - Offer both options
- Use separate keys - Consider derived keys for large holdings
Further Reading
- On-Chain Zaps in Ditto - The original Soapbox article that started it all
- On-Chain Infrastructure - PSBT, multisig, escrow
- Taproot Wallets - Understanding P2TR
- Key Tweaks - Privacy via derived keys
- P2P Trading - Fiat on/off ramps
- NIP-17 - Private DMs for notifications
On-chain zaps are the purest expression of "Nostr is Taproot Native" - your identity IS your wallet. No channels to manage, no liquidity to worry about, no LNURL to configure. Just Bitcoin, the way it was designed.