30817:NIP-XMR
Encrypted Monero Payment Information
- signed by
- npub1zwphu...u2s3t5
- published
- 2025-10-06
- event
- b2f62cea26...3c97
NIP-XMR
Encrypted Monero Payment Information
draft optional
This NIP defines a way for users to store their view-only Monero wallet information on Nostr using NIP-44 encryption, enabling portable wallet credentials across multiple services while maintaining privacy.
⚠️ Important: This NIP ONLY handles view-only wallet data (primary address + private view key). Spend keys are NEVER shared. Services cannot spend your funds, only monitor incoming payments.
Motivation
Currently, users must manually enter their Monero wallet details on every service that accepts Monero. This creates friction and reduces interoperability. Additionally, storing wallet information publicly (like Lightning addresses in Kind 0 profiles) compromises privacy for users who want to receive Monero payments.
This NIP enables:
- Portable wallet credentials: Store wallet details once, use across multiple NIP-XMR compatible services
- Privacy-first design: Wallet details are encrypted and only decryptable by the owner
- Service authorization: Users explicitly authorize services to access their wallet data via Nostr extension
- Improved UX: No need to re-enter wallet details on every platform
- Decentralized storage: No central database holds sensitive wallet information
Event Format
A NIP-XMR event is a parameterized replaceable event (kind 38383) with NIP-44 encrypted content:
{
"kind": 38383,
"created_at": <unix timestamp>,
"tags": [
["p", "<user's own pubkey>"],
["d", "monero-wallet"]
],
"content": "<nip44-encrypted wallet data>",
...other fields
}
Tags
p(required): The user's own pubkey. This indicates the content is encrypted to self.d(required): The parameterized replaceable event identifier, always set to"monero-wallet". This allows updating wallet information by publishing a new event with the samedtag.
Content
The content field contains NIP-44 encrypted JSON data with the following structure (after decryption):
{
"xmr_address": "4AdUndX...",
"xmr_view_key": "f359631...",
"updated_at": 1680000000
}
Example (for illustration only, not real credentials):
{
"xmr_address": "4AdUndXHHZ6cfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgYeYTRj5UzqtReoS44qo9mtmXCqY45DJ852K5Jv2684Rge",
"xmr_view_key": "f359631075708155cc3d92a32b75a7d02a5dcf27756707b47a2b31b21c389501",
"updated_at": 1680000000
}
Fields:
xmr_address(required): The user's primary Monero address (starts with4)xmr_view_key(required): The private view key for payment verificationupdated_at(required): Unix timestamp of when wallet info was last updated
Publishing Workflow
- User creates/updates their Monero wallet information in a NIP-XMR compatible service (e.g., Gyges)
- Service requests user's public key via
window.nostr.getPublicKey() - Service creates JSON payload with wallet details
- Service encrypts payload to user's own pubkey using
window.nostr.nip44.encrypt(pubkey, json) - Service creates kind
38383event with encrypted content - User signs event via
window.nostr.signEvent(event) - Event is published to Nostr relays
Fetching Workflow
- User logs into a different NIP-XMR compatible service
- Service queries relays for kind
38383events from user's pubkey withdtag"monero-wallet" - Service requests decryption via
window.nostr.nip44.decrypt(pubkey, encryptedContent) - User's Nostr extension prompts for permission to decrypt
- Upon approval, service receives wallet details and can auto-fill payment information
Privacy Considerations
Encryption to Self
By encrypting to the user's own public key, the wallet data is:
- Private: Only decryptable by the user's private key
- Portable: Accessible from any device where the user has their Nostr private key
- Permissioned: Services must request decryption through the Nostr extension, giving users control
View Key Security
While view keys allow monitoring incoming transactions without spending ability:
- View keys reveal incoming payments: Anyone with the view key can see all payments received
- View keys cannot spend funds: The spend key remains secure
- Subaddress privacy: Services should derive unique subaddresses for each payment to maintain sender privacy
Users should understand that:
- Encrypted data is only as secure as their Nostr private key
- View keys allow payment monitoring but not spending
- Services can derive subaddresses from the primary address and view key
Client Implementation
Publishing Example (JavaScript)
import { nip44 } from 'nostr-tools';
async function publishWallet(xmrAddress, xmrViewKey) {
const pubkey = await window.nostr.getPublicKey();
const walletData = JSON.stringify({
xmr_address: xmrAddress,
xmr_view_key: xmrViewKey,
updated_at: Math.floor(Date.now() / 1000)
});
const encryptedContent = await window.nostr.nip44.encrypt(pubkey, walletData);
const event = {
kind: 38383,
created_at: Math.floor(Date.now() / 1000),
tags: [
['p', pubkey],
['d', 'monero-wallet']
],
content: encryptedContent
};
const signedEvent = await window.nostr.signEvent(event);
// Publish to relays...
}
Fetching Example (JavaScript)
async function fetchWallet(pubkey) {
// Query relays for NIP-XMR event
const events = await pool.querySync(relays, {
kinds: [38383],
authors: [pubkey],
'#d': ['monero-wallet'],
limit: 1
});
if (events.length === 0) return null;
// Request decryption from user's extension
const decrypted = await window.nostr.nip44.decrypt(pubkey, events[0].content);
const walletData = JSON.parse(decrypted);
return walletData; // { xmr_address, xmr_view_key, updated_at }
}
Service Integration
Recommended Implementation Flow
Services implementing NIP-XMR should follow this pattern:
-
Check for existing wallet on login:
- Query relays for user's NIP-XMR event (kind
38383) - If found, offer to "Import wallet from Nostr"
- Query relays for user's NIP-XMR event (kind
-
Request one-time authorization:
- Ask user's Nostr extension to decrypt the event
- User sees permission prompt: "Allow [Service] to access your Monero wallet info?"
- User approves once
-
Store in service database:
- Save decrypted wallet data to your database
- Encrypt at database level (e.g., using column encryption or full-disk encryption)
- This enables invoice generation when user is offline
-
Runtime operations (no user interaction needed):
- Generate subaddresses for invoices
- Monitor payments using view key
- Verify transactions automatically
Important: NIP-XMR is for initial onboarding and portability, not real-time decryption. Services must store wallet credentials to function properly (e.g., generating invoices when creators are offline).
Subaddress Derivation
Services should use subaddress derivation to:
- Generate unique addresses for each payment/invoice
- Maintain payment privacy for users
- Track payments without address reuse
Example using monero-ts:
import { MoneroWalletKeys } from 'monero-ts';
async function generateSubaddress(primaryAddress, viewKey, accountIdx, subaddressIdx) {
const wallet = await MoneroWalletKeys.createWallet({
networkType: 'mainnet',
primaryAddress: primaryAddress,
privateViewKey: viewKey
});
const subaddress = await wallet.getSubaddress(accountIdx, subaddressIdx);
return subaddress.getAddress();
}
Security Considerations
Trust Model
⚠️ Important: NIP-XMR is a wallet credential portability standard, not a runtime decryption system.
When you authorize a service to decrypt your NIP-XMR data, you are trusting that service with your wallet credentials. Services are expected to store wallet data in their own databases after authorization, as real-time decryption is not practical for operations like invoice generation (which happen when you're offline).
What you're sharing:
- ✅ Monero primary address (public, safe to share)
- ✅ Private view key (allows monitoring incoming payments only)
- ❌ NOT your spend key (NIP-XMR never involves spend keys)
What services can do with this data:
- ✅ Generate unique subaddresses for payments
- ✅ Monitor incoming payments to verify transactions
- ❌ CANNOT spend your funds (no spend key is shared)
Risk model:
- A malicious service could leak your view key, exposing your payment history
- A malicious service CANNOT steal your funds (view-only access)
- Only grant access to services you trust
- Consider using separate Monero wallets for different trust levels
User Responsibilities
- Protect Nostr private key: Compromise of the Nostr private key exposes encrypted wallet data
- Use hardware/secure extensions: Store Nostr keys in hardware devices or secure extensions when possible
- Separate wallets for different trust levels: Use a dedicated wallet for public/service payments vs private holdings
- Understand view key privacy: View keys reveal all incoming transactions but cannot spend funds
- Only authorize trusted services: Once decrypted, services can store your wallet data
Service Responsibilities
- Store wallet data securely: Encrypt wallet data at rest in your database (e.g., using database-level encryption)
- Validate addresses: Verify Monero addresses are valid before using them
- Clear permission prompts: Inform users why wallet data access is needed and what it allows
- Secure subaddress generation: Use battle-tested libraries like
monero-tsfor cryptographic operations - Never request spend keys: NIP-XMR only uses view keys; never ask users for spend keys
- Be transparent: Clearly document that you store wallet credentials after authorization
Extension Requirements
NIP-XMR requires Nostr extensions to support:
- NIP-44 encryption:
window.nostr.nip44.encrypt(pubkey, plaintext) - NIP-44 decryption:
window.nostr.nip44.decrypt(pubkey, ciphertext) - Permission prompts: Clear user consent before decrypting sensitive data
Comparison to Lightning Addresses
| Aspect | Lightning (NIP-05/lud16) | Monero (NIP-XMR) |
|---|---|---|
| Privacy | Public in profile | Encrypted, private |
| Portability | Hardcoded per profile | Portable across services |
| Permission | No permission needed | User grants access via extension |
| Transaction Privacy | Network-level analysis possible | Cryptographically private |
Rationale
Why Kind 38383?
Kind 38383 was chosen as a high number to avoid conflicts with existing NIPs while remaining in the parameterized replaceable event range (30000-40000).
Why NIP-44 Instead of NIP-04?
NIP-04 has known security vulnerabilities and can potentially leak private keys. NIP-44 was professionally audited and uses secure encryption primitives (HKDF, ChaCha20).
Why Encrypt to Self?
Encrypting to the user's own pubkey provides:
- Complete privacy (no public wallet exposure)
- User control (explicit permission via extension)
- Portability (accessible from any device with user's key)
- Future compatibility (can be extended for service-specific grants)
Future Extensions
Potential improvements to this NIP:
- Multiple wallets: Support for multiple Monero wallets with different
dtag identifiers - Service-specific grants: Ability to grant permanent access to specific service pubkeys
- Metadata: Additional fields like wallet labels, preferred subaddress indices
- Integration with NIP-57: Hybrid Lightning/Monero payment support
- Recovery hints: Encrypted seed phrase backup mechanisms
Changelog
- 2025-01-05: Initial draft with NIP-44 encryption design
Discussion
Connect a key to comment.