Files
Evan Kaloudis 8b5b5004da docs: add Claude Code skill library under .claude/skills
Adds 16 project skills (plus 3 runnable diagnostic scripts) that encode
  the project's architecture contracts, backend capability matrix, storage
  and migration protocols, build/run/debug runbooks, failure archaeology,
  Lightning domain reference, and research discipline, so contributors and
  AI coding sessions can operate at maintainer standard without rederiving
  project knowledge.

  All facts were verified against master c5fd094fb and adversarially
  reviewed (factual/doctrine/usability passes) before inclusion. Also
  updates .gitignore to share .claude/skills/ while keeping all other
  .claude/ files (e.g. settings.local.json) local.
2026-07-06 12:43:26 -04:00

31 KiB
Raw Permalink Blame History

name, description
name description
zeus-lightning-reference Bitcoin/Lightning/ecash domain theory AS IMPLEMENTED IN ZEUS, for readers who may know React Native but zero Lightning (or vice versa). Load when working on or reading code that touches - BOLT11 invoice decoding (utils/Bolt11Utils.ts), payments (MPP/AMP/keysend, TLV records, routing fee limits), LNURL pay/withdraw/auth/channel or lightning addresses, ZEUS Pay / Zaplocker / nostr attestations, LSP flows (Olympus Flow 2.0, jit_bolt11 wrapping, LSPS0/1/7, 0-conf channel acceptance), Cashu/ecash (CDK FFI, multimint melts, token extraction), Nostr Wallet Connect (NIP-47, service and client roles), Boltz-protocol swaps, on-chain (address types, taproot, PSBT, coin control, CPFP bump retry, mempool.space fees), the universal input router utils/handleAnything.ts and its detector ordering, CLINK/noffer, or watchtowers. Also load when confused by terms like HTLC, preimage, hodl invoice, JIT channel, melt quote, submarine swap, or keysend while reading Zeus code.

Zeus Lightning & Ecash Domain Reference

Protocol knowledge tied to the exact Zeus file that implements it, plus the Zeus-specific twist on each protocol. Every claim below was verified against the code at the commit in "Provenance and maintenance". Paths are repo-relative.

When to use / When NOT to use

Use this skill when you need to understand WHAT a protocol does and HOW Zeus implements it: decoding invoices, payment flows, LNURL, ZEUS Pay, LSPs, Cashu, NWC, swaps, on-chain, or the input router.

Do NOT use this skill for:

Need Use instead
Which of the 7 backends supports feature X; per-backend RPC quirks; adding an RPC zeus-backends-and-capabilities
Settings defaults, config axes, add-a-setting checklist zeus-config-and-flags
Keychain keys, storage blob, migrations (e.g. Cashu per-node namespacing details) zeus-storage-and-migrations
Boot sequence, stores DI, dispatch design rationale zeus-architecture-contract
Symptom-driven debugging, past incident history zeus-debugging-playbook / zeus-failure-archaeology
PR/commit/review rules, what changes are gated zeus-change-control

One rule from change control that this skill must echo because everything here is funds-touching: send/receive/payment-handling code takes minimal diffs only — no drive-by refactoring. See zeus-change-control for the full policy.

1. Lightning in three paragraphs (just enough to read this codebase)

Channels and HTLCs. The Lightning Network is a payment layer on top of Bitcoin. Two nodes lock bitcoin into a shared 2-of-2 on-chain output called a channel; they then pay each other instantly by exchanging signed balance updates off-chain. Multi-hop payments are forwarded through chains of channels using HTLCs (Hashed Time-Locked Contracts): each hop's payment is locked to the SHA-256 hash of a secret (the preimage); revealing the preimage settles every hop atomically. A hodl invoice is one where the receiver deliberately delays revealing the preimage — the payment hangs "in-flight" until released (ZEUS Pay's Zaplocker scheme, section 5, is built on this). 0-conf channels are usable before their funding transaction confirms — safe only when you trust the channel opener, which is why Zeus only auto-accepts 0-conf from its LSP (section 6).

Invoices and routing. A BOLT11 invoice (a lnbc... bech32 string) encodes amount, payment hash, expiry, destination and a signature (section 2). BOLT12 "offers" (lno1...) are reusable payment codes; Zeus supports them on the cln-rest and ldk-node backends. Payment senders find a route themselves and pay routing fees to intermediate hops; wallets set a fee cap per payment (section 3). MPP (multi-path payments) split one payment across several routes; AMP (atomic multi-path) is LND's variant that also works without an invoice. Keysend is a spontaneous payment (no invoice) where the sender generates the preimage and ships it inside the payment's TLV (type-length-value) custom records.

LSPs and the rest. An LSP (Lightning Service Provider) sells inbound channel capacity, including JIT (just-in-time) channels opened at the moment a payment arrives (fee deducted from the received amount). LNURL is a family of HTTP-based UX protocols (pay/withdraw/auth/channel) layered on Lightning; a lightning address (user@domain.com) is LNURL-pay behind an email-like name. Ecash (Cashu) is a custodial-but-blinded token system: a mint holds sats and issues bearer tokens (proofs) it cannot link to you; melting a token converts it back into a Lightning payment. NWC (Nostr Wallet Connect, NIP-47) lets apps drive a wallet remotely over the Nostr relay network. Submarine swaps move funds between on-chain and Lightning through a swap provider (Boltz protocol): submarine = on-chain in, Lightning out; reverse = Lightning in, on-chain out.

2. BOLT11 invoices — utils/Bolt11Utils.ts

Zeus does NOT use a stock bolt11 npm decoder; it has a hand-rolled one (based on light-bolt11-decoder) with two load-bearing performance features:

  1. LRU cache (CACHE_LIMIT = 256), keyed by the lowercased payment request. The Activity list decodes the same invoices repeatedly; the cache collapses that. Cache hits return the same object instance to every caller.
  2. Lazy self-replacing getters for destination, payeeNodeKey, signature, recoveryFlag. Recovering the payee node key from the invoice signature is an ECDSA public-key recovery via @noble/secp256k1 (~520 ms per call on a phone — the comment in the file says so). decode() defers it: those four fields are installed as Object.defineProperty getters that run recovery on first access, then replace themselves with plain values. Callers that only read payment_hash / description / expiry / timestamp pay zero crypto cost. If the invoice carries an explicit payee tag (tag 19), destination/payeeNodeKey are set eagerly from it and the getters are skipped.

Consequences — the rules:

  • Never naively serialize or spread a decoded invoice (JSON.stringify(decoded), {...decoded}). The lazy getters are enumerable, so both operations force the expensive secp256k1 recovery immediately — for every cached invoice you touch — defeating the entire design. Read individual fields instead.
  • Never mutate the returned object. It is shared cache state; your mutation poisons every future decode() of that invoice.
  • decode() throws on non-ln strings — callers gate with AddressUtils.isValidLightningPaymentRequest first (see utils/handleAnything.ts).
  • Amount fields: satoshis (number|null), millisatoshis (string|null), plus Zeus-convention num_satoshis / num_msat (strings, '0' when absent).

The interface is DecodedBolt11 in the same file. Tests: utils/Bolt11Utils.test.ts.

3. Paying: MPP, AMP, keysend, fee limits

Payment dispatch lives in stores/TransactionsStore.ts (sendPaymentsendPaymentInternal); the send UI is views/PaymentRequest.tsx.

MPP vs AMP and their version gates

  • supportsMPP() and supportsAMP() are per-backend capability flags (see zeus-backends-and-capabilities for the doctrine). On the LND family (backends/LND.ts, backends/EmbeddedLND.ts, backends/LightningNodeConnect.ts) they are node-version-gated: MPP requires LND ≥ v0.10.0, AMP requires LND ≥ v0.13.0. backends/LdkNode.ts: MPP true, AMP false. CLNRest, LndHub, NostrWalletConnect: both false.
  • AMP is also forced on when the invoice's feature bit 30 is is_required (views/PaymentRequest.tsx, lockAtomicMultiPathPayment). Enabling AMP sets no_inflight_updates = true. Tor also sets no_inflight_updates = true ("Tor can't handle streaming updates").

The max_parts quirk (verify before relying on defaults)

  • TransactionsStore.sendPaymentInternal: data.max_parts = max_parts ? max_parts : '1' — the store default is '1' (single path) for any caller that doesn't pass it.
  • views/PaymentRequest.tsx triggerPayment: max_parts: enableMultiPathPayment ? maxParts : '16' with state default maxParts: '16' — the main send screen always passes '16', whether or not the MPP toggle is on. So payments from PaymentRequest allow 16 shards; payments from other call sites (e.g. programmatic sends) allow 1.
  • sendPaymentSilently has if (max_parts) { data.max_parts = max_parts || '16'; } — the || '16' branch is unreachable (dead fallback); if the caller omits max_parts the field is simply not set.

Keysend

Constants at the top of stores/TransactionsStore.ts:

TLV record type Content Encoding in Zeus
5482373484 payment preimage (sender-generated) random bytes → base64 (preimage.toString('base64'))
34349334 optional text message UTF-8 → hex → base64 (Base64Utils.hexToBase64(Base64Utils.utf8ToHex(message)))

Plus: data.dest = Base64Utils.hexToBase64(pubkey) and data.last_hop_pubkey = Base64Utils.hexToBase64(...)LND REST wants raw bytes base64-encoded, not hex strings; forgetting the conversion is a classic bug. payment_hash is hexToBase64(sha256(preimage)).

Function selection is one of the few sanctioned implementation branches (see zeus-architecture-contract): cln-rest / embedded-lnd / ldk-node with a pubkey go through BackendUtils.sendKeysend; everything else — including remote LND REST, which has no dedicated keysend endpoint — goes through BackendUtils.payLightningInvoice carrying dest_custom_records.

Routing fee conventions — utils/FeeUtils.ts

calculateDefaultRoutingFee(amount):

  • amount > 1000 sats → fee cap = 5% (DEFAULT_ROUTING_FEE_PERCENT = 0.05), rounded.
  • amount ≤ 1000 sats → fee cap = the amount itself (i.e. 100%; tiny payments routinely need proportionally huge fees).

views/PaymentRequest.tsx passes max_fee_percent (default '5.0') for cln-rest, which takes a percent instead of a sat limit; the store only forwards max_fee_percent when implementation === 'cln-rest'.

4. LNURL family and lightning addresses

Zeus uses the js-lnurl package (getParams, findlnurl, decodelnurl — imported in utils/handleAnything.ts; version pinned in package.json). LNURL flows are routed by the input router (section 11) to one of four screens based on params.tag:

tag Meaning Zeus screen
payRequest (LNURL-pay / LUD-06) server gives min/max, you request an invoice from its callback views/LnurlPay/ (or ChoosePaymentMethod when ecash is enabled)
withdrawRequest (LNURL-withdraw) server offers sats; you submit YOUR invoice to its callback views/Receive.tsx with lnurlParams
channelRequest (LNURL-channel) server opens a channel to you views/LnurlChannel.tsx
login (LNURL-auth) key-based login challenge views/LnurlAuth.tsx, gated by supportsLnurlAuth()

Lightning address (LUD-16) resolution is inline in handleAnything.ts: split user@domain, fetch https://<domain>/.well-known/lnurlp/<user> (plain http:// for .onion). Per LUD-16 the domain is always lowercased and the username normally is too — except for cryptoqr.net addresses, whose usernames are URL-encoded merchant payloads where hex-digit casing matters server-side (isCryptoQR check; covered by utils/handleAnything.test.ts). Before the LNURL-pay lookup, Zeus also tries a BOLT12 DNS lookup (BIP-353 style: TXT record at <user>.user._bitcoin-payment.<domain> via cloudflare-dns.com/dns-query) and offers a payment-method choice if both exist.

Onion gap: direct .onion lightning-address lookups go through doTorRequest when Tor is enabled, but generic LNURL parameter fetches via js-lnurl's getParams do NOT route over Zeus's internal Tor — there is a literal // TODO handle fetching of params with internal Tor in handleAnything.ts; .onion LNURL endpoints currently error out of that path. Open gap, not a bug you introduced.

Nested URI schemes lnurlp:// / lnurlw:// / lnurlc:// / lnurlauth:// are rewritten to https:// (or http:// for .onion) before processing.

5. ZEUS Pay / Zaplocker — stores/LightningAddressStore.ts + stores/LnurlPayStore.ts

ZEUS Pay gives a self-custodial wallet a lightning address (user@zeuspay.com) even though the phone is usually offline. Server: LNURL_HOST = 'https://zeuspay.com'. Three address types exist (address_type sent to the server): 'zaplocker' (hodl-invoice scheme, below), 'cashu' (payments held as Cashu mint quotes, redeemed via /api/lnurl/nuts/redeem), and 'nwc'. Gate: supportsLightningAddress() in utils/BackendUtils.ts is the composite supportsCustomPreimages() || supportsCashuWallet().

Zaplocker lifecycle (receiver side):

  1. Pre-generate preimages: generatePreimages creates 250 preimages per batch (32-byte entropy each), computes hash = sha256(preimage), schnorr-signs each hash with the user's nostr private key, and POSTs {pubkey, hashes, nostrSignatures, ...} to zeuspay.com/api/lnurl/submitHashes. Preimages stay ONLY on the phone (storage key zeuspay-lightning-address-hashes). Auto-replenishes when the server reports fewer than 50 unused hashes.
  2. Someone pays: zeuspay.com issues a hodl invoice against one of your hashes and holds the HTLC. It cannot settle — it doesn't know the preimage.
  3. Redeem (lookupPreimageAndRedeemZaplocker): the app looks up the preimage for the hash, creates a local invoice with that fixed preimage (BackendUtils.createInvoice({ preimage, expiry: '86400', ... })), and POSTs it to /api/lnurl/redeem; the server pays it, which reveals the preimage and settles the held HTLC. This is why the gate is supportsCustomPreimages — the backend must accept caller-chosen preimages.

Anti-fraud attestations (nostr kind 55869):

  • Payer side (LnurlPayStore.broadcastAttestation, triggered from views/PaymentRequest.tsx when isZaplocker): publish a kind-55869 event, signed by an ephemeral key, whose p tag is getPublicKey(paymentHash) — the payment hash itself is used as a nostr secret key so anyone holding the hash can find attestations — and whose content is the bolt11 invoice being paid.
  • Receiver side (LightningAddressStore.lookupAttestations): for each hash, query relays for kind 55869 with #p = getPublicKey(hash), then analyzeAttestation checks the embedded invoice actually commits to that hash and amount. Exactly one valid attestation → success status; more than one attestation → status 'error' (a fraud signal: two different "payers" claiming the same hash means someone is lying).
  • The payer also verifies the receiver's zaplocker setup: LnurlPayStore.load checks schnorr signatures over the payment hash (isPmtHashSigValid) and the relay list (isRelaysSigValid) against the receiver's advertised nostr pubkey.

6. LSP integration — three generations, all in stores/LSPStore.ts

Default endpoints (Olympus, per network) are exported from stores/SettingsStore.ts (DEFAULT_LSP_MAINNET = 'https://0conf.lnolymp.us', LSPS1 REST https://lsps1.lnolymp.us, etc.); resolution via getLspConfig — endpoint/default details belong to zeus-config-and-flags.

Generation 1: Olympus "Flow 2.0" REST (JIT channels on receive)

  • getZeroConfFee POSTs the msat amount to <flowHost>/api/v1/fee; getZeroConfInvoice(bolt11) POSTs to /api/v1/proposal and resolves data.jit_bolt11 — a wrapped invoice routing through the LSP, which opens a 0-conf channel just in time and deducts its fee.
  • The wrapping MUTATES the requested amount (stores/InvoicesStore.ts, createInvoice): when Flow LSP is active and value > zeroConfFee, Zeus creates the local invoice for req.value = requested zeroConfFee, then swaps the displayed payment request for jit_bolt11 (which asks the payer for the full amount). The payer sees the full amount; your node's invoice is smaller by the fee.
  • Silent degradation: if getZeroConfInvoice rejects, Zeus clears the error state and shows the UNWRAPPED invoice — receiving still works if a channel already exists, but no JIT channel will be opened. Don't "fix" the swallowed error without understanding this.
  • LNURL-withdraw interaction: the withdraw callback must receive the wrapped invoice — qs.pr = jit_bolt11 || invoice.getPaymentRequest in InvoicesStore. Passing the unwrapped one would make the withdrawing service pay an invoice the LSP never sees.

Generation 2/3: LSPS0 transport + LSPS1 (buy channel) / LSPS7 (extend lease)

  • LSPS0 = JSON-RPC 2.0 over the Lightning peer-to-peer custom message type 37913 (CUSTOM_MESSAGE_TYPE in LSPStore.ts). Requests are correlated by uuidv4() ids (this.getInfoId = uuidv4(); responses matched by data.id in handleCustomMessages) with a 7-second timeout (CUSTOM_MESSAGE_RESPONSE_TIMEOUT_MS = 7000).
  • Three transports, chosen by backend capability flags: native (embedded-lnd goes straight to lndmobile custom-message APIs), custom message via BackendUtils.sendCustomMessage/subscribeCustomMessages (other LND-family), and REST (<lsps1Rest>/api/v1/get_info, /api/v1/create_order) for backends that can't send peer messages. Methods: lsps1.get_info, lsps1.create_order, lsps1.get_order, and the LSPS7 equivalents for channel-lease extension.
  • 0-conf acceptance policy (handleChannelAcceptorEvent): a channel-acceptor hook rejects any 0-conf channel unless the opener is the LSP pubkey or listed in settings.zeroConfPeers; normal (confirmed) channels are accepted regardless. On embedded-lnd this is wired via the ChannelAcceptor native event stream; other backends go through BackendUtils.initChanAcceptor.

7. Cashu / ecash — stores/CashuStore.ts + utils/CashuUtils.ts

Ecash primer: a Cashu mint custodies sats and issues blinded bearer tokens (proofs). Receiving Lightning through a mint = mint quote (pay invoice → get proofs); paying Lightning from proofs = melt quote. Tokens serialize as cashuA... (v3 JSON) / cashuB... (v4 CBOR) strings.

  • Implementation is the CDK Rust FFI, NOT cashu-ts: import CashuDevKit ... from '../cashu-cdk' — a native module wrapping the Cashu Dev Kit with its own SQLite DB. Wallet ops (initializeWallet(mnemonic, 'sat'), addMint, getBalances, melt, restoreFromSeed) all cross the FFI. Cashu is available only where supportsCashuWallet() is true: embedded-lnd and ldk-node.
  • Seed versions: seedVersion is 'v1' (legacy: key material derived from the LND seed, bytes [32:64]) or 'v2-bip39' (dedicated cashu BIP-39 phrase; stored under <nodeDir>-cashu-seed-phrase with version marker <nodeDir>-cashu-seed-version). v1 wallets keep their original P2PK key via originalSeedVersion special-casing. Per-node storage namespacing (getNodeDir() and the collision it fixed) is owned by zeus-storage-and-migrations.
  • Multimint MPP (NUT-15) bypasses CDK: to pay one invoice from several mints at once, mintSupportsMpp probes each mint's info for NUT-15 (note the quirk: nuts['15'] is an ARRAY of methods, not {methods: [...]}), then queryMeltQuoteMpp POSTs raw mint REST<mint>/v1/melt/quote/bolt11 with options: { mpp: { amount: <msat> } } — because CDK doesn't expose partial-amount melt quotes. Rejections are classified by classifyMppRejection: "internal mpp not allowed" / "self payment" → 'selfSend' (mints refuse MPP toward their own invoices; the planner falls back to a single-mint regular quote via prepareSingleMintRegularQuote, executed with CashuDevKit.melt instead of meltPartial).
  • Token extraction from URL wrappers (CashuUtils.extractTokenString): a greedy regex cashu[AB][0-9A-Za-z+/_=-]+ pulls a token out of any wrapper (zeusln.com/e/, wallet.cashu.me, nutstash, etc.) and stops at URL noise like &memo=; failing that it strips the known prefixes in cashuTokenPrefixes (https://wallet.nutstash.app/#, https://wallet.cashu.me/?token=, web+cashu://, cashu://, cashu:).
  • P2PK DoS guards: P2PK-locked tokens carry attacker-controlled JSON in proof secrets. safeParseP2PKSecret rejects secrets over MAX_P2PK_SECRET_LENGTH = 2048 chars or nested deeper than MAX_P2PK_SECRET_DEPTH = 10 (bracket-scan before JSON.parse). Keep these guards when touching token parsing.

8. NWC (NIP-47) — Zeus plays BOTH roles

  • Wallet service (stores/NostrWalletConnectStore.ts): Zeus exposes its own node to external apps. Each connection has an optional budget (budgetAmount/budgetRenewal with BudgetRenewalType, periodic resets via checkAndResetAllBudgets, capped by wallet balance maxBudgetLimit). Staying reachable in the background is the hard part: on iOS the store drives a background-audio keep-alive (utils/IOSAudioKeepAliveUtils.ts, bundled silent m4a) that auto-starts whenever at least one connection exists; Android uses a foreground service. This store is a known hotspot (~3200 lines, zero tests) — see zeus-debugging-playbook before touching it.
  • Client backend (backends/NostrWalletConnect.ts): Zeus can itself be a thin client driving a remote NWC wallet, via @getalby/sdk's NostrWebLNProvider constructed from the node's nostr+walletconnect:// URL. Capability surface is minimal (no keysend, no on-chain, no channels — see the supports* block at the bottom of the file). Quirk: NWC makeInvoice returns only paymentRequest, so createInvoice back-fills payment_hash by decoding the bolt11 with Bolt11Utils.

9. Swaps — Boltz protocol v2, stores/SwapStore.ts

Zeus speaks the Boltz v2 REST API; default provider is ZEUS's own instance (DEFAULT_SWAP_HOST_MAINNET = 'https://swaps.zeuslsp.com/api/v2' in stores/SettingsStore.ts), with Boltz (https://api.boltz.exchange/v2) selectable. Endpoints used: /swap/submarine, /swap/reverse, /swap/restore.

Deterministic rescue key — the design goal is that a single mnemonic can recover any in-flight swap:

  • The rescue key is a BIP-39 mnemonic stored under swaps-rescue-key (SWAPS_RESCUE_KEY in utils/SwapUtils.ts). On ldk-node it reuses the node's own mnemonic instead of generating a new one (generateRescueKey).
  • Per-swap keys derive at m/44/0/0/0/<index> (DERIVATION_PATH = 'm/44/0/0/0', getPath(index) appends the index; last used index persisted under swaps-last-used-key).
  • Reverse-swap preimage = sha256(child privkey) (derivePreimageFromRescueKey) — so preimages never need separate backup; the mnemonic regenerates them.
  • Restore: getRescuableSwaps derives an xpub from the mnemonic and POSTs it to <host>/swap/restore; the server returns swaps whose keys belong to that xpub.

10. On-chain: address types, PSBT/coin control, CPFP, fees

  • Address types (views/Receive.tsx, values are LND AddressType enum strings): '0' = p2wkh (native SegWit, bc1q..., the default), '1' = np2wkh (nested SegWit, 3..., shown only if supportsNestedSegWit()), '4' = p2tr (taproot, bc1p..., shown only if supportsTaproot() — LND-family gates this at node ≥ v0.15.0).
  • Coin control / PSBT: a UTXO is an unspent output; coin control means picking which UTXOs fund a transaction; a PSBT (Partially Signed Bitcoin Transaction) is the interchange format for building/signing across devices. stores/UTXOsStore.ts lists UTXOs and on-chain accounts; BackendUtils.fundPsbt/finalizePsbt dispatch to capable backends (supportsCoinControl(): LND-family ≥ v0.12.0, CLNRest, LdkNode, and LNC — gated on the session's permNewAddress permission). Scanned/pasted PSBTs and raw tx hex route to the PSBT / TxHex screens via handleAnything.ts.
  • CPFP bump with the output-index flip (stores/FeeStore.ts, bumpFeeOpeningChannel): CPFP (child-pays-for-parent) accelerates a stuck funding tx by spending its change output at a high fee. Zeus parses outpoint as txid:output_index, tries the bump, and if LND answers "the passed output does not belong to the wallet" it retries once with the index flipped (0↔1) — the channel-open change output is at whichever index the funding output isn't. Comment in code: only works for single-party-funded channels. Force-close bumps go through bumpForceCloseFee (no flip retry).
  • Fee estimation: FeeStore.getOnchainFeesviaMempool fetches https://mempool.space/<testnet/>api/v1/fees/recommended (fastest/halfHour/hour/ economy); backend-native estimators exist per backend but the fee picker UI is mempool.space-driven.

11. utils/handleAnything.ts — the universal input router

Every scanned QR, pasted string, deep link, and NFC payload goes through handleAnything(data, setAmount?, isClipboardValue?). It returns a [screenName, params] navigation tuple — or, in clipboard mode (isClipboardValue = true, used to decide whether to show the "paste detected" prompt), plain booleans (true = recognized, false = not) from most detectors. Caveat: several late-chain detectors (npub, zeuscontact:, PSBT, TxHex, account imports, bare Cashu tokens, BOLT12 withdrawal requests) have no isClipboardValue check and return the navigation tuple even in clipboard mode — callers must treat any truthy return as "recognized". New detectors should add the boolean guard. Don't mix up the two return shapes.

Detector ORDER is load-bearing. It is one giant if/else chain; earlier detectors shadow later ones. Current order (verified):

  1. BIP-21 URI unpack (AddressUtils.processBIP21Uri) + multiple-payment-method choice
  2. CLINK noffer → ClinkPay; BOLT12 offer → Send
  3. On-chain address (+ optional embedded lightning param) → Send/Accounts
  4. Lightning pubkey → keysend Send; BOLT11 → PaymentRequest (or ChoosePaymentMethod when ecash enabled); BOLT12 offer string → Send
  5. Node-connection strings (clnrest://, nostr+walletconnect://, LNC pairing, lndconnect, LNDHub) → WalletConfiguration
  6. Node URI (pubkey@host) → OpenChannel; lightning address (LUD-16, with BOLT12 DNS probe and the cryptoqr.net casing exception, section 4)
  7. BTCPay pairing config
  8. Web-wrapped Cashu tokens BEFORE the LNURL detector — an https URL containing a cashu[AB]... base64 body. The comment in the file is explicit: findlnurl can match bech32-like substrings INSIDE cashu tokens and misroute them; the base64 shape gate avoids swallowing URLs that merely contain the literal "cashuA" (e.g. ?ref=cashuAfrica).
  9. LNURL (findlnurl / decoded lnurl / /lnurl|lnurlp|... path regex) → tag dispatch (section 4)
  10. npub / zeuscontact: → contacts; PSBT; TxHex; account imports (xpub, descriptors, keystore JSON) gated by supportsAccounts()
  11. Bare Cashu token (isValidCashuTokenAsync) → CashuToken
  12. BOLT12 withdrawal requests (gated supportsWithdrawalRequests())
  13. Merchant QR near-LAST (isMerchantQR): South-African merchant payloads (Pick n Pay, Ecentric, SnapScan, Zapper...) are converted to ...@cryptoqr.net lightning addresses and recursively re-fed into handleAnything. Guard: MERCHANT_QR_MAX_LEN = 500 — the alternation-heavy regexes catastrophically backtrack and overflow Hermes' regex stack on long inputs (Hermes = React Native's JS engine), so anything longer is skipped. Keep this near the end: its regexes are greedy enough to steal inputs that earlier detectors parse precisely.
  14. Fallback: WIF private-key sweep → WIFSweeper, else "not valid" error.

When adding a detector: place it as late as correctness allows, add clipboard-mode boolean returns, cap regex input length if patterns use nested alternation, and add cases to utils/handleAnything.test.ts.

12. One-liners

  • CLINK / noffer (utils/ClinkUtils.ts): noffer1... bech32-TLV payment codes resolved over nostr — kind-21001 (CLINK_KIND) NIP-44-encrypted request/response with a 30 s default timeout (DEFAULT_TIMEOUT_MS = 30_000); .onion relays are explicitly refused (ONION_NOT_SUPPORTED) rather than silently leaking clearnet traffic.
  • Watchtowers (LND-family only): a watchtower watches the chain for old-state channel breaches while you're offline. BackendUtils dispatches listWatchtowers/addWatchtower/etc.; supportsWatchtowerClient() is true on LND, EmbeddedLND, and LNC, false/absent elsewhere (absent methods dispatch to false — see zeus-backends-and-capabilities).

Provenance and maintenance

Facts verified 2026-07-06 against master c5fd094fb (v13.1.3-alpha) by reading the implementing files directly. No commands below mutate anything.

Volatile fact Re-verify with
Bolt11 LRU size / lazy getters grep -n "CACHE_LIMIT|defineLazyRecoveryFields" utils/Bolt11Utils.ts
Keysend TLV constants grep -n "5482373484|34349334" stores/TransactionsStore.ts
max_parts defaults '1' vs '16' grep -n "max_parts" stores/TransactionsStore.ts views/PaymentRequest.tsx
Routing fee 5% / 1000-sat threshold grep -n "DEFAULT_ROUTING_FEE_PERCENT|1000" utils/FeeUtils.ts
MPP/AMP version gates grep -rn "supportsMPP|supportsAMP" backends/
cryptoqr.net casing exception, merchant QR 500-char cap grep -n "isCryptoQR|MERCHANT_QR_MAX_LEN" utils/handleAnything.ts
js-lnurl version grep js-lnurl package.json
Zaplocker: 250 preimages, kind 55869, zeuspay.com grep -n "250|55869|zeuspay" stores/LightningAddressStore.ts stores/LnurlPayStore.ts
LSPS0 message type 37913 / 7 s timeout grep -n "CUSTOM_MESSAGE_TYPE|TIMEOUT_MS" stores/LSPStore.ts
Flow amount mutation + jit_bolt11 swap grep -n "zeroConfFee|jit_bolt11" stores/InvoicesStore.ts
Flow/LSPS1 default hosts grep -n "DEFAULT_LSP" stores/SettingsStore.ts
Cashu raw MPP melt endpoint grep -n "melt/quote/bolt11" stores/CashuStore.ts
Cashu token prefixes / P2PK limits grep -n "cashuTokenPrefixes|MAX_P2PK" utils/CashuUtils.ts
Cashu seed versions grep -n "v2-bip39|originalSeedVersion" stores/CashuStore.ts
NWC client SDK head -20 backends/NostrWalletConnect.ts
Swap hosts / path / preimage derivation grep -n "DEFAULT_SWAP_HOST" stores/SettingsStore.ts; grep -n "DERIVATION_PATH|derivePreimageFromRescueKey|swap/restore" stores/SwapStore.ts
CPFP output-index flip grep -n "does not belong" stores/FeeStore.ts
Address type values 0/1/4 grep -n "value: '0'|value: '1'|value: '4'" views/Receive.tsx
CLINK kind / timeout grep -n "CLINK_KIND|DEFAULT_TIMEOUT_MS" utils/ClinkUtils.ts
Watchtower support matrix grep -rn "supportsWatchtowerClient" backends/
Detector chain order read the if/else chain in utils/handleAnything.ts top to bottom

Labeled uncertainties: none of the facts above are speculative; server-side behavior of zeuspay.com / swaps.zeuslsp.com / lnolymp.us is described only as far as the client code shows (request/response shapes) — API contracts are not versioned in this repo.