A mobile Bitcoin Lightning wallet for smart savers and their families, built with Expo/React Native, connecting via Nostr Wallet Connect (NWC) with Nostr social features.
Wallets
- Connect any Lightning wallet via NWC (Nostr Wallet Connect)
- On-chain Bitcoin wallets (watch-only via xpub, or hot wallets via mnemonic)
- Wallets tab: swipe between wallet cards, and each wallet's transactions appear instantly while fresh data loads in the background. Add, reorder, hide or remove wallets under Account → Manage wallets
- Transfer funds between wallets (LN-to-LN, LN-to-on-chain, on-chain-to-LN, on-chain-to-on-chain)
- Send payments by scanning QR codes or pasting invoices
- Send to lightning addresses (user@domain) via LNURL-pay
- Send to on-chain addresses (BIP-21 URI support with amount)
- NIP-57 zaps to Nostr contacts
- Receive payments with QR code generation (optional amount for BIP-21)
- Real-time balance display with fiat conversion
Nostr identity and messaging
- Nostr identity login: local
nsec, Amber (NIP-55, Android), or a remote signer over NIP-46 / Nostr Connect (Clave on iOS, Aegis, nsec.app). Several accounts on one phone, each with its own language, currency and safety settings - Signing out of a local-key account warns first and offers a key backup
- Private messaging: NIP-17 1:1 and group chats (photos, voice notes, polls, invoices, zaps, live location), plus Marmot (MLS over Nostr, Alpha) end-to-end encrypted 1:1 and group chats:
- edit and delete your own messages, react with emoji in 1:1s, and send photos and voice notes as encrypted media (MIP-04);
- group admins: see and manage them; only admins can rename a group or change its members;
- group invites select fresh compatible devices (up to 10 per person); a new 1:1 uses the newest selected White Noise package when present, and you can manage your own invitation keys under Account → Messages;
- if someone can't receive Marmot yet, the app asks before falling back to NIP-17;
- see each person's devices: a Secure chat · N devices by-line in 1:1s, per-person device lists (with the app each runs) in the members sheet, and "Sent from" in message info.
- Live location: on Android, sharing continues while Lightning Piggy is minimised, with an ongoing notification and a Stop sharing action. iOS background sharing needs a newer iOS build.
- In Marmot chats, start, updates and end go over Marmot only. Updates aren't saved to chat history; they are sent after a move of at least 25 m (at most once every 30 s), or every 5 minutes when still.
- NIP-17 shares use a throwaway key per share and NIP-44 encryption for updates. Neither transport needs signer prompts for updates.
- If Android kills the app mid-share, a Marmot share stops with a Live location stopped notice; its chat end marker is sent when the app next opens.
- Other apps such as White Noise see "📍 Sharing my live location…" with a map link.
- @mentions in group chats (NIP-17 and Marmot): type
@to pick a member (sent as anostr:npubreference, plus aptag in Marmot); mentions of you show as tappable@Namechips (White Noise's@npubform too), with an @ badge on the chat row and an @ button that jumps to the oldest unread mention - Remembered read position: 1:1 and group chats reopen at an "Unread messages" divider, for every protocol
- Push notifications (opt-in, per account): Marmot messages via MIP-05, plus separate switches for messages, zaps, mentions and payments through Lightning Piggy's notification server. Alerts are generic ("New message", "Zap received") and never show content, and a Notifications screen (the bell on Wallets) lists the last 30 days
- Profiles: Posts, Media and Shop tabs, a Posts | Posts & replies switch that shows what each reply answers, and NIP-27 references rendered as tappable
@namechips and embedded notes, articles and products - NIP-22 note replies: kind-1111 comments on kind-1 notes (e.g. from Ditto) appear under a profile's Posts & replies with "Replying to @Name", never under Posts. Market and Piglet comments are unaffected.
- Friends tab with Nostr contacts and phone contacts
- Follow/unfollow Nostr contacts (kind 3 event publishing)
- Add friends by pasting npub or scanning QR code
- Contact profile cards with deep linking to Nostr apps
- QR code sharing for npub and Lightning address
- Secure credential storage (expo-secure-store)
- Link safety: chat link previews, images and plain voice notes auto-load only for your own messages or people you follow, after URL address checks; other senders need a tap. Encrypted voice notes load on play. These checks reject private, local and reserved address forms, but cannot prevent DNS rebinding; preview-page redirects are checked at each hop, while media redirects are followed natively.
- Relay health: unreachable relays are parked and skipped automatically, so one dead relay can't slow the app down. A message, listing or invite is only shown as sent once a relay has accepted it
Market
- Buy and sell with sats (Explore → Market): browse Lightning Piggy's preferred sellers or your friends' listings, filter by country, currency or merchant, read and write reviews and comments (and edit or delete your own), and check out in-app.
- My shop: set up a shop with shipping options (name, cost and the countries each ships to), then list items with photos, price, stock, categories, per-item shipping costs and a Markdown description. Edit, preview or delete each listing. Listings are standard Gamma Markets events (NIP-99), so other Nostr shops can read them, and product pages render any seller's Markdown and shipping prices.
Explore
- Explore tab: discover Bitcoin merchants nearby (BTC Map / OpenStreetMap),
opt-in geofence alerts when you walk past one (#467), hunt for hidden
LNURL-withdraw "Piggies" stashed on NFC tags or QR codes (#468), and find
Bitcoin meetups, conferences, and workshops via NIP-52 calendar events
(kind 31923 — read-only, with native Add-to-Calendar via
expo-calendar). Hunt Piggies adopt treasures.to's NIP-GC (kind 37516listings,kind 7516found-logs,kind 1111NIP-22 comments) and tag the listing with a NIP-32com.lightningpiggy.app / payout-lnurl-wlabel so Lightning Piggy renders the 🐷 claim UX on top while generic geocaching clients render the cache normally. The LNURL bearer token never goes on the public relay event — it lives only on the physical NFC tag / QR (the access control) and in the hider's local SecureStore. Hider can update location / hint photo / cooldown / expiry by re-publishing with the samedtag (PRE semantics). See docs/PROTOCOLS.adoc → "Hunt: Lightning extension to NIP-GC" for the full tag schema, and docs/SOLUTION_DESIGN.adoc → "Explore tab" for the BTC Map + Nostr cache architecture, bbox-driven list filtering, and offline cold-start guarantees.
Lightning Piggy speaks 9 languages: English, Deutsch (German), Español (Spanish), Magyar (Hungarian), Polski (Polish), Українська (Ukrainian), हिन्दी (Hindi), मराठी (Marathi) and 中文(简体)(Chinese, Simplified). Pick one per account under Account → Display & language, or follow the phone's language. Plurals follow each language's CLDR rules, and numbers and dates use the app language rather than the phone's.
Translators and contributors: English (src/i18n/locales/en.json) is the source of truth, and every new or changed string is translated into all locales in the same PR. npm run i18n:sync lists what's missing or out of date and can draft translations; docs/i18n/GLOSSARY.md has the terminology and tone rules. Dev and preview builds also offer a Pseudo (testing) language (en-XA) for spotting clipped or untranslated text.
Lightning Piggy Mobile implements the following open standards. See docs/STANDARDS.adoc for usage details and source references.
| Standard | Name |
|---|---|
| BIP-21 | URI Scheme (bitcoin: with ?amount=) |
| BIP-32 | Hierarchical Deterministic Wallets |
| BIP-39 | Mnemonic Seed Phrases |
| BIP-84 | Native SegWit HD Wallets (bc1q...) |
| BIP-327 | MuSig2 Schnorr Key Aggregation |
| BIP-340 | Schnorr Signatures |
| BIP-341 | Taproot (SegWit v1) |
| Standard | Name |
|---|---|
| NIP-01 | Basic Protocol (relays, events, subscriptions) |
| NIP-04 | Encrypted Direct Messages (legacy — inbound read only; outbound migrated to NIP-17 in #334) |
| NIP-05 | DNS Identity Verification |
| NIP-09 | Event Deletion (kind 5) — deleting your own Market listings, shipping options, comments and reviews, retracting DM reactions; honours sellers' deletions when reading |
| NIP-10 | Replies (e tag markers) — tells posts from replies on the profile Posts tab |
| NIP-14 | Subject Tag (used as group-chat name) |
| NIP-15 | Nostr Marketplace — legacy kind-30018 products are still read by the Market |
| NIP-17 | Private Direct Messages — canonical 1:1 and multi-recipient group chat (sealed via NIP-44 + gift-wrapped per recipient) |
| NIP-19 | Bech32-encoded Entities (npub, nsec, nprofile, note, nevent, naddr) |
| NIP-20 | Command Results (OK messages) |
| NIP-21 | nostr: URI Scheme |
| NIP-22 | Comments (kind 1111) — read note replies in profiles; Market product comments, and NIP-GC DNF / maintenance / archived cache statuses |
| NIP-23 | Long-form Content (kind 30023) — read only, shown as an article card when a post references one |
| NIP-25 | Reactions (kind 7) — emoji reactions in chats |
| NIP-27 | Text Note References — nostr: mentions render as tappable @name chips, and referenced notes, articles and products as embedded cards (profiles) |
| NIP-32 | Labels (L namespace + l value tags — used to mark Hunt caches as Lightning-payout via the com.lightningpiggy.app / payout-lnurl-w label) |
| NIP-40 | Expiration Timestamp (auto-retire Hunt cache listings) |
| NIP-44 | Encrypted Payloads v2 (used by NIP-17 sealing) |
| NIP-46 | Nostr Connect — remote signer over relays (Clave, Aegis, nsec.app); see docs/nip46-clave.adoc |
| NIP-47 | Nostr Wallet Connect (NWC) |
| NIP-52 | Calendar Events (kind 31923 time-based — read-only consumer for the Events sub-screen) |
| NIP-55 | Android Signer (Amber) |
| NIP-57 | Lightning Zaps |
| NIP-59 | Gift Wrap (used by NIP-17 to mask sender + metadata) |
| NIP-65 | Relay List Metadata |
| NIP-68 / NIP-71 | Picture (kind 20) and video (kinds 21 / 22) events — read for the profile Media tab |
| NIP-88 | Polls (kind 1068 polls, kind 1018 votes) in chats |
| NIP-89 | client tag (Lightning Piggy) on the public profile, follow-list, zap-request and Piglet events it publishes; never inside encrypted messages |
| NIP-92 | Media Attachments (imeta) — profile Media tab, and MIP-04 encrypted photos |
| NIP-94 | File Metadata |
| NIP-99 | Classified Listings, via the Gamma Markets spec: Market reader and My shop seller — products (30402), shop collection (30405), shipping options (30406), reviews (31555) and orders (16 / 17) |
| NIP-GC (draft, on Nostr) | Geocaching Events (kinds 37516 / 7516 / 1111 / 7517 — used by the Hunt feature, with an com.lightningpiggy.app NIP-32 label marker for Lightning-payout caches) |
Group state for client-side group chat is propagated via a parameterised-replaceable
kind:30200 event (custom to this client; receivers reconcile by groupId + created_at).
Not a NIP — see src/services/nostrService.ts (GROUP_STATE_KIND) for the schema.
Media uploads go to Blossom servers (BUD-01 / BUD-02), with backup servers and a BUD-03 server list (kind 10063) you can publish under Account → Nostr → Image servers.
1:1 and group chats via the vendored @internet-privacy/marmot-ts (vendor/). The vendored package describes itself as tracking Marmot v2 and wire-compatible with darkmatter; this is a library claim, not an app interoperability test. The table is a historical MIP cross-reference, not a conformance claim against the adopted protocol. White Noise interoperability depends on the client version and protocol profile; no tested version/profile is specified here.
GIFs and polls are app-level payloads: Lightning Piggy sends GIFs as a GIPHY URL plus via GIPHY, structured NIP-88 polls in 1:1 Marmot chats, and legacy text polls in Marmot groups. These formats do not establish White Noise poll support.
| MIP | Name | Status in Lightning Piggy |
|---|---|---|
| MIP-00 | Credentials & Key Packages | Implemented. One kind-30443 key package per install, refreshed weekly and retired on sign-out; see and manage them under Account → Messages → Invitation keys |
| MIP-01 | Group Construction & Group Data | Implemented: group name, admins (shown and managed; only admins rename or change members). Commits realign the extension order for MDK; cross-client behavior depends on the client version and profile |
| MIP-02 | Welcome Events | Implemented. Kind-444 Welcomes, gift-wrapped; invites from people you follow are accepted automatically |
| MIP-03 | Group Messages | Implemented. Kind-445 group events: text, reactions, edits, "delete for everyone" and member removal; quoted replies from White Noise are shown |
| MIP-04 | Encrypted Media | Implemented. Photos and voice notes as encrypted-media-v2 on Blossom (v1 still read) |
| MIP-05 | Push Notifications (draft) | Implemented. Sends push triggers to other members' notification servers (Lightning Piggy's own are reached via their kind-10050 relay lists, not a fixed relay hint); receiving is opt-in per account |
| MIP-06 | Multi-device (being redesigned upstream) | Partial: groups invite selected eligible devices, capped at 10 per person; a new 1:1 uses the newest selected White Noise package when present. No device linking or pairing of your own installs yet |
If a contact can't receive Marmot yet, the app asks before falling back to NIP-17. Known interop gaps are tracked in GitHub issues. See docs/PROTOCOLS.adoc → "Marmot".
| Standard | Name |
|---|---|
| LUD-03 | LNURL-withdraw — used by the Hunt feature to claim Piggies |
| LUD-16 | Lightning Address (user@domain → LNURL-pay endpoint) |
| LUD-17 | URI scheme prefixes (lnurlw://, lnurlp://, …) — accepted as input alongside bech32 LNURL1… |
| Standard | Name |
|---|---|
| BOLT-11 | Lightning Invoice Encoding |
| LNURL / LUD-16 | Lightning Address Protocol (user@domain) |
On top of these, Lightning Piggy Mobile uses the Boltz v2 submarine swap API for trustless Lightning ↔ on-chain transfers.
Lightning Piggy is an Expo / React Native app (SDK 55, RN 0.83) that runs from a custom dev-client — it ships custom native modules, so Expo Go is not used. Lightning payments ride on Nostr Wallet Connect (NWC); the social layer is Nostr (encrypted DMs, zaps, contacts, geo-caches). A fuller write-up lives in docs/ARCHITECTURE.adoc.
┌─────────────────────────────────────────────────────────────┐
│ Screens & Components (src/screens, src/components) │
│ Wallets · Messages · Explore · Friends + Account drawer │
├─────────────────────────────────────────────────────────────┤
│ Navigation (src/navigation/AppNavigator.tsx) │
│ 4 tabs → Explore stack · account drawer · root stack │
├─────────────────────────────────────────────────────────────┤
│ Contexts (src/contexts) Hooks (src/hooks) │
│ WalletContext · NostrContext · UI-facing per-feature │
│ Groups · TrustGraph · LiveLoc logic │
├─────────────────────────────────────────────────────────────┤
│ Services (src/services) — by domain │
│ Nostr (nostrService, dm*, places) · Wallet (nwcService, │
│ onchainService/BDK, lnurl, boltz) · location/geofence · │
│ media upload · NFC · notifications · Market · relay health │
├─────────────────────────────────────────────────────────────┤
│ Persistence │
│ SQLCipher DM store (op-sqlite) · expo-secure-store (keys) · │
│ AsyncStorage caches (per-account namespaced) │
├─────────────────────────────────────────────────────────────┤
│ Native (modules/, plugins/) │
│ amber-signer · background-dm-service · nostr-native · │
│ bdk-rn (BDK, UniFFI) · Expo config plugins │
└─────────────────────────────────────────────────────────────┘
│ │ │
Nostr relays NWC wallet svc Electrum / Boltz
(DMs, zaps, (pay_invoice, (on-chain sync,
contacts, caches) get_balance) LN↔on-chain swaps)
Layers. Presentation (screens/components) sits over React Navigation; long-lived state lives in contexts (WalletContext for wallets/balances/transactions, NostrContext for identity/signer/relays/DMs), which compose per-responsibility hooks and a large, mostly-stateless service layer grouped by domain. Persistence is split three ways by sensitivity: an encrypted SQLCipher DM store, expo-secure-store for keys/secrets, and per-account-namespaced AsyncStorage caches. Native code is limited to four local Expo modules in modules/, plus BDK and a set of config plugins:
amber-signer— Android-only NIP-55 (Amber) intent bridge for signing.background-dm-service— Android-only persistent foreground service that hosts the opt-in background message watch (and NWC incoming-payment checks).nostr-native— Kotlin / Swift wrapper over rust-nostr. Its NIP-44 and Schnorr primitives are an experimental, off-by-default setting and are synchronous, so they still run on the JS thread; only the async relay-engine functions move work off it. Falls back to pure JS where it is not linked.secure-clipboard— copies a secret (the key on the Back up your key screen) with the platform's protections: marked sensitive on Android, local-only with an expiry on iOS.bdk-rn— Bitcoin Dev Kit (Rust, via UniFFI) for on-chain wallets, a fork pinned inpackage.json.plugins/— Expo config plugins:withAdjustResize,withAmberQueries,withFcmAutoInitDisabled,withForegroundService,withLargeHeap,withNfc,withTransparentSplashIcon.
Background delivery has an always-on part and opt-in parts. Periodic detect-and-ping via expo-background-task (WorkManager / BGTaskScheduler) is registered on every launch, subject to the OS's scheduling limits. The Android foreground service (near-realtime message and NWC payment checks) and Marmot instant alerts (MIP-05 push) are both opt-in, under Settings → Notifications.
Key data flows.
- Sending a DM — build a NIP-17 chat rumor (kind 14) → seal (kind 13) → gift-wrap (kind 1059) with NIP-44 → publish to the peer's and own relays. Inbound wraps are unwrapped once and stored plaintext in the encrypted SQLite DB. When the app is closed on Android, a periodic
expo-background-task(WorkManager, ~15-min floor) does detect-and-ping, and the opt-inbackground-dm-serviceforeground service keeps a relay watch alive (also checking NWC incoming payments, see docs/BACKGROUND_PAYMENTS.adoc). No FCM is required by default. See docs/architecture/notifications.adoc. In a Marmot thread the same message is sent inside an MLS group (kind 445) instead — see docs/SOLUTION_DESIGN.adoc → "Marmot (MLS) messaging". - Paying over NWC — a scanned invoice (or an LNURL-pay / Lightning address resolved to a BOLT-11) is sent to the wallet service as an encrypted NIP-47
pay_invoicerequest over the NWC relay; the encrypted response updates balance and history. Zaps (kind 9734 request → 9735 receipt) ride the same rail. - Publishing a geo-cache ("Piglet") — a NIP-GC listing (kind 37516) is published with a NIP-32 payout label and a stable
dtag; the LNURL-withdraw bearer stays on the physical NFC tag / QR and in the hider's secure store, never on the public event. Claims record a found-log (kind 7516).
See docs/ARCHITECTURE.adoc for the module map, the full Nostr event-kind reference, signer options (nsec / Amber / NIP-46), and persistence detail.
- Node.js 22.13 or newer (see
.nvmrcandenginesinpackage.json) - An Android device or emulator for local development. iOS builds go through EAS cloud / TestFlight (
npx expo run:iosalso works on macOS, see docs/INSTALLATION.adoc) - A Lightning wallet that supports NWC (e.g. Alby, LNbits)
git clone https://github.com/BenGWeeks/lightning-piggy-mobile
cd lightning-piggy-mobile
npm installThis project uses custom native modules, so it requires a dev client build rather than Expo Go.
# First time: build and install the dev client
npx expo run:android
# Subsequent runs: start Metro (connects to the dev client)
npm start
# Press 'a' to open on a connected Android deviceNote: Always use
npm start(notnpx expo start) — the start script includes--dev-clientwhich is required for custom native modules. Usingnpx expo startdirectly will launch in Expo Go mode and show "Something went wrong".
Optional keys are read from .env at the repo root (gitignored) and inlined into the bundle at build time via app.config.ts / Metro's EXPO_PUBLIC_* handling.
| Variable | Used by | Notes |
|---|---|---|
EXPO_PUBLIC_GIPHY_API_KEY |
In-conversation GIF picker | Free key from developers.giphy.com/dashboard. Restrict it to the Android package / iOS bundle ID in the GIPHY dashboard — keys are public by design, not a secret. When unset, the "Send GIF" row is hidden from the attach menu and nothing else breaks. |
After changing .env, restart Metro (npm start) so the new value gets inlined. No native rebuild is required for EXPO_PUBLIC_* changes.
Releases go through the GitHub Release workflow, not manual builds: bump with npm version <patch|minor|major>, run git push origin main --follow-tags, then publish a GitHub Release for the tag. .github/workflows/release.yml runs EAS cloud builds for both platforms, submits iOS to TestFlight and attaches the Android APK to the Release. eas build --local is only an offline fallback. See docs/DEPLOYMENT.adoc → "Cutting a release". For a tester APK, eas build --platform android --profile preview.
src/
components/ # Reusable UI components (SendSheet, ContactProfileSheet, etc.)
contexts/ # React contexts (WalletContext, NostrContext, ...)
hooks/ # Per-feature hooks
i18n/ # Translations: 9 locale catalogues, CLDR plural rules
navigation/ # React Navigation: Wallets · Messages · Explore · Friends tabs + account drawer
screens/ # Tab and feature screens (Home, Messages, Explore/Hunt/Map, Friends, account/)
services/ # Business logic (NWC, LNURL, Nostr, Marmot, Market, Boltz, BDK)
styles/ # Theme and per-component style files
types/ # TypeScript type definitions
utils/ # Pure helpers
assets/ # Images and icons
modules/ # Local native Expo modules (amber-signer, background-dm-service, nostr-native, secure-clipboard)
plugins/ # Expo config plugins
vendor/ # Vendored packages (marmot-ts tarball)
.maestro/ # Maestro E2E flows, by feature area
scripts/ # CI gates and dev tooling
docs/ # Architecture, protocols, deployment, troubleshooting
- Unit tests — Jest (
jest-expo), co-located assrc/**/*.test.ts; coverage coverssrc/services,src/utilsandsrc/contexts. Runnpm test. A CI workflow fails PRs that drop line coverage by more than 0.5pp. - Translations — a Jest guard (
src/i18n/locales/catalogues.test.ts) fails CI if any locale is missing keys, has stale keys, empty strings, mismatched placeholders or missing plural forms, or has too many strings left in English.npm run i18n:sync -- --checkdoes the same staleness check locally, including reworded English. - E2E — Maestro flows in
.maestro/, grouped by feature area:maestro test .maestro/<area>/flow-NNN-<name>.yaml. See .maestro/README.adoc.
The Explore tab pulls in a small set of Expo modules beyond the wallet + Nostr core. All are listed in docs/PACKAGES.adoc; the Explore-specific additions are:
| Package | Used by | Why |
|---|---|---|
react-native-webview |
ExploreMiniMap, MapScreen |
Leaflet WebView for OSM tile rendering — no Google Maps API key, no billing setup. |
expo-asset + expo-file-system + expo-sharing |
HuntCreateScreen "Save STL" |
Ships the 188 KB Piggy Bag Charm STL inside the APK, copies it out on tap, and shares via the system sheet. |
expo-calendar |
EventDetailScreen "Add to Calendar" |
Native calendar insert with lazy permission prompt. |
expo-notifications |
"Alerts near Bitcoin shops" geofences (#467) | Foreground pings when the user walks past a Bitcoin merchant. |
More Explore screens live under docs/screenshots/explore/:
01-tab-bar-home.png— bottom tab bar with the new Explore tab02-explore-hub.png/02b-explore-hub-scrolled.png— Lessons / Map / Hunt / Events rails03-lessons.png04-map-merchants-and-caches.png— full Leaflet view05-hunt-hub.png,06-hunt-create.png,06b-hunt-nfc-write.png,07-hunt-discover.png08-events.png09-hunt-piggy-detail.png/09b-hunt-piggy-detail-scrolled.png10-places-list.png,11-place-detail.png
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. See CONTRIBUTING.md, the Code of Conduct and the security policy.









