diff --git a/app/backend/app.py b/app/backend/app.py
index 82fe59a..78c294e 100644
--- a/app/backend/app.py
+++ b/app/backend/app.py
@@ -195,6 +195,19 @@ def get_public_lnurl_address():
return (cfg.get("public_lnurl_address") or "").strip() or PUBLIC_LNURL_ADDRESS
+def get_payment_mode():
+ cfg = load_config()
+ mode = str(cfg.get("payment_mode", "compatibility")).strip().lower()
+
+ if mode not in ("compatibility", "privacy"):
+ return "compatibility"
+
+ return mode
+
+
+def privacy_mode_enabled():
+ return get_payment_mode() == "privacy"
+
def _mask_secret(value: str) -> str:
value = (value or "").strip()
@@ -1925,13 +1938,20 @@ def lnurl_for_address(address: str) -> LnurlInfoResponse:
@app.get("/.well-known/lnurlp/{username}", response_model=LnurlPayMetadataResponse)
def lnurl_pay_metadata(username: str) -> LnurlPayMetadataResponse:
- alias = _resolve_lnurl_alias(username)
+ if privacy_mode_enabled():
+ raise HTTPException(
+ status_code=404,
+ detail="LNURL disabled by privacy mode",
+ )
+
+ alias = _resolve_lnurl_alias(username)
min_sendable = LNURL_MIN_SENDABLE_MSAT
max_sendable = LNURL_MAX_SENDABLE_MSAT
if alias["fixed_amount_sat"] is not None:
fixed_msat = int(alias["fixed_amount_sat"]) * 1000
+
min_sendable = fixed_msat
max_sendable = fixed_msat
@@ -1954,6 +1974,13 @@ async def lnurl_callback(
nostr: Optional[str] = Query(default=None),
lnurl: Optional[str] = Query(default=None),
) -> dict[str, Any]:
+
+ if privacy_mode_enabled():
+ raise HTTPException(
+ status_code=404,
+ detail="LNURL disabled by privacy mode",
+ )
+
alias = _resolve_lnurl_alias(username)
min_sendable = LNURL_MIN_SENDABLE_MSAT
@@ -2096,6 +2123,15 @@ def set_setup_config(payload: dict, request: StarletteRequest):
cfg["lnurl_base_domain"] = str(safe_payload.get("lnurl_base_domain", "")).strip().lower()
cfg["lnurl_base_url"] = str(safe_payload.get("lnurl_base_url", "")).strip().rstrip("/")
+ payment_mode = str(
+ safe_payload.get("payment_mode", "compatibility")
+ ).strip().lower()
+
+ if payment_mode not in ("compatibility", "privacy"):
+ payment_mode = "compatibility"
+
+ cfg["payment_mode"] = payment_mode
+
dns_mode = str(safe_payload.get("dns_mode", "none")).strip().lower()
cfg["dns_mode"] = dns_mode
@@ -2586,25 +2622,28 @@ async def public_index_page():
aliases = cfg.get("aliases", {}) or {}
items_html = ""
+
for alias_name, alias in aliases.items():
description = alias.get("description") or "Lightning payment"
amount_sat = alias.get("amount_sat")
amount_label = f"{amount_sat} sats" if amount_sat else "variable amount"
+ bolt12_domain = get_public_bolt12_address().split("@", 1)[1]
+ bip353_address = f"{alias_name}@{bolt12_domain}"
+
items_html += f"""
-
{alias_name}@{get_lnurl_base_domain()}
+
{bip353_address}
{description}
Amount: {amount_label}
-
+
"""
-
if not items_html:
items_html = """
@@ -3528,7 +3567,10 @@ async def public_alias_page(alias_name: str):
alias = aliases[alias_name]
+ privacy_mode = privacy_mode_enabled()
+
bip353_domain = get_bip353_base_domain()
+
lnurl_domain = get_lnurl_base_domain()
bip353_address = f"{alias_name}@{bip353_domain}"
@@ -3559,6 +3601,11 @@ async def public_alias_page(alias_name: str):
except Exception:
bolt11_invoice = None
+ subline = (
+ "BOLT12 only"
+ if privacy_mode
+ else "BOLT12 first · LNURL and BOLT11 as fallback"
+ )
offer_section = ""
if last_offer:
offer_section = f"""
@@ -3577,9 +3624,8 @@ async def public_alias_page(alias_name: str):
"""
-
bolt11_section = ""
- if bolt11_invoice:
+ if bolt11_invoice and not privacy_mode:
bolt11_section = f"""
BOLT11 Compatibility Fallback
@@ -3598,7 +3644,7 @@ async def public_alias_page(alias_name: str):
"""
lnurl_section = ""
- if lnurl_fallback:
+ if lnurl_fallback and not privacy_mode:
lnurl_section = f"""
LNURL Fallback
@@ -3837,7 +3883,7 @@ async def public_alias_page(alias_name: str):
{description}
Amount: {amount_label}
-BOLT12 first · LNURL and BOLT11 as fallback
+{subline}
{offer_section}
diff --git a/app/backend/config.py b/app/backend/config.py
index cde16cb..4214f4c 100644
--- a/app/backend/config.py
+++ b/app/backend/config.py
@@ -10,6 +10,7 @@
"public_lnurl_address": "lnurl@pay.local",
"lnurl_base_domain": "",
"lnurl_base_url": "",
+ "payment_mode": "compatibility", # compatibility | privacy
"dns_mode": "none", # none | manual | cloudflare
"ui_password_hash": "",
"cloudflare": {
diff --git a/app/frontend/public/index.html b/app/frontend/public/index.html
index dd28b27..d0baaa8 100644
--- a/app/frontend/public/index.html
+++ b/app/frontend/public/index.html
@@ -316,6 +316,58 @@
margin-top: 16px;
}
+ .paymentModeLabel {
+ margin-top: 14px;
+ margin-bottom: 6px;
+ font-size: 1.05rem;
+ font-weight: 700;
+ }
+ .paymentModeCards {
+ display: grid;
+ gap: 12px;
+ margin-top: 12px;
+ }
+
+ .paymentModeCard {
+ position: relative;
+ display: block;
+ padding: 20px 16px 20px 52px;
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ cursor: pointer;
+ background: rgba(255,255,255,0.02);
+ }
+
+ .paymentModeCard:hover {
+ border-color: var(--accent);
+ }
+
+ .paymentModeCard input[type="radio"] {
+ position: absolute;
+ left: 18px;
+ top: 22px;
+ width: auto;
+ min-width: 0;
+ height: auto;
+ margin: 0;
+ padding: 0;
+ transform: scale(1.15);
+ }
+ .paymentModeTitle {
+ font-weight: 700;
+ font-size: 1rem;
+ }
+
+ .paymentModeDesc {
+ margin-top: 4px;
+ color: var(--text);
+ }
+
+ .paymentModeHint {
+ margin-top: 6px;
+ color: var(--muted);
+ font-size: 0.9rem;
+ }
.aliasItem {
border: 1px solid var(--border);
background: #0c1322;
@@ -603,14 +655,78 @@
⚙️ BOLT12 Pay Setup
-
-
+
+ Choose Payment Mode
+
-
-
+
+ Select between maximum wallet compatibility or maximum privacy.
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
@@ -877,6 +993,7 @@
⚡ BOLT12 Offer & Alias Console
const saveSetupBtn = document.getElementById('saveSetupBtn');
const manualDnsHint = document.getElementById('manualDnsHint');
const manualDnsExampleName = document.getElementById('manualDnsExampleName');
+ const lnurlSettingsBlock = document.getElementById('lnurlSettingsBlock');
const cloudflareFields = document.getElementById('cloudflareFields');
const cfZoneName = document.getElementById('cfZoneName');
@@ -884,6 +1001,7 @@ ⚡ BOLT12 Offer & Alias Console
const cfApiToken = document.getElementById('cfApiToken');
const setupPassword = document.getElementById('setupPassword');
const setupPasswordConfirm = document.getElementById('setupPasswordConfirm');
+ const paymentModeInputs = document.querySelectorAll('input[name="paymentMode"]');
const aliasNameInput = document.getElementById('aliasNameInput');
const aliasDescriptionInput = document.getElementById('aliasDescriptionInput');
@@ -1099,6 +1217,15 @@ ⚡ BOLT12 Offer & Alias Console
cloudflareFields.hidden = setupDnsMode.value !== 'cloudflare';
}
+ function updatePaymentModeUi() {
+ const privacy =
+ document.querySelector(
+ 'input[name="paymentMode"]:checked'
+ )?.value === 'privacy';
+
+ lnurlSettingsBlock.hidden = privacy;
+ }
+
// ------------------------------------------------------------------
// Data loading helpers
// ------------------------------------------------------------------
@@ -1140,6 +1267,14 @@ ⚡ BOLT12 Offer & Alias Console
console.log('Setup config unavailable', e);
}
setupBolt12Address.value = configData.public_bolt12_address || '';
+
+ const paymentMode =
+ configData.payment_mode || 'compatibility';
+
+ paymentModeInputs.forEach(el => {
+ el.checked = el.value === paymentMode;
+ });
+ updatePaymentModeUi();
setupLnurlAddress.value = configData.public_lnurl_address || '';
setupLnurlBaseDomain.value = configData.lnurl_base_domain || '';
setupLnurlBaseUrl.value = configData.lnurl_base_url || '';
@@ -1175,6 +1310,10 @@ ⚡ BOLT12 Offer & Alias Console
const payload = {
public_bolt12_address: (setupBolt12Address.value || '').trim(),
+ payment_mode:
+ document.querySelector(
+ 'input[name="paymentMode"]:checked'
+ )?.value || 'compatibility',
public_lnurl_address: (setupLnurlAddress.value || '').trim(),
lnurl_base_domain: (setupLnurlBaseDomain.value || '').trim().toLowerCase(),
lnurl_base_url: (setupLnurlBaseUrl.value || '').trim(),
@@ -1953,6 +2092,9 @@ ⚡ BOLT12 Offer & Alias Console
});
saveSetupBtn.addEventListener('click', saveSetup);
+ paymentModeInputs.forEach(el => {
+ el.addEventListener('change', updatePaymentModeUi);
+ });
setupDnsMode.addEventListener('change', updateDnsModeUi);
setupLnurlBaseDomain.addEventListener('input', updateDnsModeUi);
setupBolt12Address.addEventListener('input', updateDnsModeUi);
@@ -2170,6 +2312,17 @@ ⚡ BOLT12 Offer & Alias Console
"setup.onionWarningUmbrelFiles": "Öffne Umbrel → Files und navigiere zu:",
"setup.onionWarningStartOS": "Keine manuelle Konfiguration erforderlich. Falls Onion Messages nicht aktiviert sind, fordert BOLT12 Pay automatisch die benötigte LND Auto-Configure-Aufgabe an.",
"setup.copyUmbrelConfig": "Umbrel-Konfiguration kopieren",
+ "setup.paymentModeTitle": "Kompatibilität oder Privatsphäre?",
+ "setup.paymentModeHint": "Wähle zwischen maximaler Wallet-Kompatibilität und maximaler Privatsphäre.",
+
+ "setup.paymentMode.compatibility.title": "Kompatibilität",
+ "setup.paymentMode.compatibility.desc": "BOLT12 + LNURL + BOLT11 Fallback",
+ "setup.paymentMode.compatibility.hint": "Beste Kompatibilität mit heutigen Wallets.",
+
+ "setup.paymentMode.privacy.title": "Privatsphäre",
+ "setup.paymentMode.privacy.desc": "Nur BOLT12 + BIP353",
+ "setup.paymentMode.privacy.hint": "Keine LNURL-Endpunkte werden veröffentlicht.",
+
"dns.none": "Keine",
"dns.manual": "Manuell",
"dns.cloudflare": "Cloudflare",
@@ -2269,6 +2422,16 @@ ⚡ BOLT12 Offer & Alias Console
"setup.onionWarningStartOS": "No manual configuration required. If Onion Messages are not enabled, BOLT12 Pay will automatically request the required LND Auto-Configure task.",
"setup.onionWarningUmbrelFiles": "Open Umbrel → Files and navigate to:",
"setup.copyUmbrelConfig": "Copy Umbrel config",
+ "setup.paymentModeTitle": "Compatibility or Privacy?",
+ "setup.paymentModeHint": "Choose between maximum wallet compatibility and maximum privacy.",
+
+ "setup.paymentMode.compatibility.title": "Compatibility",
+ "setup.paymentMode.compatibility.desc": "BOLT12 + LNURL + BOLT11 fallback",
+ "setup.paymentMode.compatibility.hint": "Best compatibility with today's wallets.",
+
+ "setup.paymentMode.privacy.title": "Privacy",
+ "setup.paymentMode.privacy.desc": "BOLT12 + BIP353 only",
+ "setup.paymentMode.privacy.hint": "No LNURL endpoints are exposed.",
"dns.none": "None",
"dns.manual": "Manual",
"dns.cloudflare": "Cloudflare",