Skip to main content

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.

AspectLightning ZapsOn-Chain Zaps
Recipient setupLNURL, Lightning address, wallet configNone - npub is enough
Sender setupFunded channels, liquidity managementAny Bitcoin wallet
AvailabilityRecipient online, channels openFunds wait on-chain
Amount limitsChannel capacity constraintsUnlimited
PermanenceRelay-dependent receiptsBlockchain-secured
PrivacyRoute-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:

  1. Derive P2TR from recipient's npub
  2. Generate a Bitcoin URI or QR
  3. User pays with any Bitcoin wallet

Privacy Considerations

Public Ledger

On-chain zaps create a permanent, public link between your npub and Bitcoin transactions. Consider the tradeoffs.

The Privacy Tradeoff

AspectImplication
Address reuseAll zaps to same npub go to same address
Public historyAnyone can see total received
Sender correlationYour funding source is visible
Dust attacksAnyone can send tiny amounts

Choosing Your Privacy Level

Use CaseRecommended Approach
Public tips (attribution wanted)Raw npub → P2TR
Moderate privacyTweaked keys
High privacySilent 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

ApproachPrivacyComplexityRecipient Setup
Raw npub → P2TRNoneTrivialNone
Tweaked keyModerateLowKnow the tweak
Silent PaymentsHighModerateWallet support
Fresh key per txMaximumHighPer-tx coordination

How Tweaks Help

  1. Address unlinkability: bc1p-tweaked can't be trivially mapped back to npub1...
  2. Still spendable: Owner knows their nsec + tweak, can derive the spending key
  3. Deterministic: Same sender + recipient + tweak = same address (good for recurring)
  4. 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

  1. Add P2TR derivation - Convert npub to bc1p address
  2. Generate payment URI - bitcoin:bc1p...?amount=0.001
  3. Show QR code - User scans with any Bitcoin wallet
  4. Optional: Track payments - Monitor address for incoming txs

For Wallet Developers

  1. Recognize npub inputs - Auto-convert to P2TR
  2. Support Silent Payments - BIP-352 + NIP-17 notifications
  3. 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

FeatureLightning (NIP-57)On-Chain
SpeedInstant10-60 min confirmation
Fees~1 sat (but channel costs)Market rate (often under $1)
PrivacyRoute-basedConfigurable (tweaks/silent)
SetupLNURL + wallet + channelsNone (npub = address)
Max amountChannel limitedUnlimited
Offline receiveNo (must be online)Yes
ProofRelay-dependent receiptBlockchain-permanent
ComplexityHigh (liquidity mgmt)Low (just Bitcoin)
Native to NostrNo (separate system)Yes (same keys)

Testing on Testnet

On-chain zaps are perfect for testnet experimentation - same cryptography, zero risk.

Why Use Testnet

BenefitDescription
Free coinsFaucets provide test sats
Same code pathsIdentical derivation logic
Safe iterationMistakes cost nothing
Client testingVerify 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.

See awesome-testnet4 for more resources.

Testnet3 (Legacy)

Still works, but testnet4 is preferred:

Which Network?

NetworkStatusBest For
Testnet4CurrentAll testing (recommended)
Testnet3LegacyExisting integrations
RegtestLocalDevelopment

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 CaseHow It Helps
Provable donations"I donated X sats to @developer on date Y"
Grant accountabilityPublic record of fund distribution
Patronage historyVerifiable support timeline
Contract paymentsTimestamped proof of payment
Dispute resolutionImmutable 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

AspectLightning ZapOn-Chain Zap
Public proofZap receipt (kind 9735)Blockchain tx
ImmutabilityRelay-dependentBitcoin-secured
TimestampEvent created_atBlock timestamp
Verifiable byNostr usersAnyone with internet
PermanenceRelay retentionForever

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:

  1. On-chain zap → Permanent blockchain proof
  2. 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:

  1. File → New Wallet
  2. Select "Airgapped Hardware Wallet"
  3. Import your x-only pubkey (from npub)
  4. 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

DeviceP2TR SupportAir-Gap MethodNostr Signing
ColdcardYesMicroSD, QRNo
SeedSignerYesQR onlyNo
LedgerYesUSBVia app
TrezorYesUSBNo
JadeYesQR, USBNo

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:

  1. Generate seed on Coldcard (or import your nsec-derived seed)
  2. Export xpub via MicroSD
  3. Import xpub into Sparrow as watch-only
  4. Verify address matches your npub-derived P2TR

Spending:

  1. Sparrow: Create transaction, select UTXOs, set destination and fee
  2. Sparrow: Save PSBT to MicroSD (or display as QR)
  3. Coldcard: Load PSBT, verify outputs, sign
  4. Coldcard: Save signed PSBT to MicroSD
  5. 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:

MethodStatusHow
NIP-46WorkingRemote signer, hardware as backend
Nostr Signing DeviceExperimentalDedicated firmware
USB HIDProposedSign 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?

AmountRecommendation
Under 100k satsHot wallet fine
100k - 1M satsConsider hardware
Over 1M satsHardware required
Over 10M satsHardware + 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
MethodPurposeBitcoin Use?
signEventNostr eventsNo
signSchnorrRaw messageTheoretically yes
PSBT signingNot in specNo

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:

  1. No context - Extension shows hex blob, not "Send 0.5 BTC to bc1p..."
  2. No validation - User can't verify what they're signing
  3. Phishing magnet - Malicious site shows "Sign this message" but it's a tx
Never Sign Blind

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

ExtensionLightningOn-ChainPSBT
AlbyYes (NWC)ExperimentalNo
nos2xNoNoNo
NostoreNoNoNo
FlamingoNoNoNo

Alby is exploring WebBTC integration, but it's early. For now, extensions are Lightning-focused.

Extension vs Hardware vs NWC

MethodSecurityUXBest For
Extension (signSchnorr)LowEasyNever for Bitcoin
Extension (future PSBT)MediumEasySmall amounts, when available
Hardware walletHighMore stepsSerious amounts
NWC (Lightning)MediumEasyZaps, 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 signSchnorr from 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

  1. Check for Lightning first - If recipient has lud16, use Lightning
  2. Confirm large amounts - On-chain is permanent
  3. Consider fees - Batch if sending to multiple recipients
  4. Use Silent Payments - When privacy matters

For Recipients

  1. Monitor your address - Funds may arrive unexpectedly
  2. Consider privacy - Your on-chain history becomes public
  3. Set up Lightning too - Offer both options
  4. Use separate keys - Consider derived keys for large holdings

Further Reading


The Native Path

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.