Non-custodial, peer-to-peer Bitcoin ↔ fiat exchange on Lightning Network — powered by the Mostro protocol over Nostr.
- What is Mostro App?
- Progress Overview
- What is Mostro?
- The Mostro Protocol
- Architecture & Fundamentals
- Supported Platforms
- Getting Started (Users)
- Getting Started (Developers)
- Project Structure
- Contributing
- Internationalization
- Security
- License
Mostro App is the official cross-platform client for the Mostro peer-to-peer exchange network. It lets anyone buy or sell Bitcoin for local fiat currency using the Lightning Network — without accounts, without KYC, and without trusting a third party.
Key features:
- Non-custodial — Your keys stay on your device. No exchange holds your funds.
- Privacy-first — End-to-end encrypted over Nostr: NIP-44 direct messages with the Mostro daemon, NIP-59 Gift Wrap for peer-to-peer chat. Optional privacy mode hides reputation data.
- Lightning-native — All BTC settlements happen via Lightning invoices (BOLT 11). Supports Nostr Wallet Connect (NWC) for automated invoice generation.
- Censorship-resistant — Built on the Nostr network; no central server or domain to block.
- Multi-platform — Single codebase targets Android, iOS, Web (PWA), macOS, Windows, and Linux.
- Open source — MIT licensed. Fully auditable, no proprietary components.
Status of the client's features across the Rust core and the Flutter UI.
Legend: [x] done · [~] partial · [ ] planned
- Order flow (create / take / fiat-sent / release)
- Real-time order book
- Order book filtering
- My trades / own orders
- Lightning invoice handling (add / pay hold invoice)
- Rate counterpart
- User ↔ user chat
- Mnemonic identity & key management (BIP-39 / BIP-32, NIP-06)
- Secure storage (OS keystore)
- NWC (Nostr Wallet Connect)
- Relay management (manual add / toggle)
- Proof-of-Work (NIP-13)
- About / Mostro info
- [~] Mostro node switching — single-node switch works; multi-node management pending
- [~] Range orders — created / taken, but shown as a single amount in the order book
- [~] Cooperative cancellation — works; context-aware UX pending
- [~] Disputes — opening works; inbound list / admin chat not wired
- [~] In-app notifications — screen works; trade/message events not yet wired to feed it
- [~] Lightning address — not synced to the Rust store, so invoice auto-fill does not trigger
- [~] Push notifications — client wired (FCM); push server not yet deployed
- [~] Multi-language — missing translations / inconsistent switching
- [~] Log report — UI present but capture not working
- [~] Reputation / privacy mode — toggle wired, effect not confirmed
- User ↔ user file attachments
- User ↔ admin chat
- User ↔ admin file attachments
- Configurable session retention
- Session restore from mnemonic
- Relay auto-discovery (kind 10002)
- Anti-abuse bond
- Deep link
mostro:URI - Tor / anonymous relays
Mostro is a decentralized peer-to-peer Bitcoin exchange system. It acts as a non-custodial escrow daemon that facilitates trades between parties who want to exchange Bitcoin (via Lightning) for fiat currency.
Unlike centralized exchanges (e.g., Binance, Coinbase), Mostro:
- Does not hold user funds or require registration
- Does not have access to trade secrets or personal data
- Cannot censor or block orders
- Relies on cryptographic proofs, not corporate trust
- Is run by independent operators — anyone can run a Mostro node
The Mostro daemon (server component) is an open-source project written in Rust: MostroP2P/mostro. This repository is the mobile/desktop/web client that talks to any Mostro daemon.
The Mostro protocol is a message-passing specification built on top of Nostr. All communication between clients and the Mostro daemon travels as encrypted Nostr events.
| Concept | Description |
|---|---|
| Order Book | Mostro daemons publish pending orders as Nostr events of Kind 38383 — parameterized replaceable events. |
| Trade Messages | All trade actions (take order, add invoice, confirm fiat sent, release funds) are sent as NIP-44-encrypted direct messages — signed Kind 14 events authored by the per-trade key and directed to the Mostro node pubkey (Mostro protocol v2). |
| P2P Chat | Direct messages between buyer and seller (and dispute–admin chat) use NIP-59 Gift Wrap (Kind 1059) directed to the peer's pubkey. |
| Hold Invoices | The seller pays a Lightning hold invoice when taking a buy order. Funds are locked until the buyer confirms fiat receipt, then the daemon releases the HTLC. |
| Reputation | Each trade can result in a mutual star rating, published as a Nostr event by the daemon. |
| Disputes | Either party can open a dispute, escalating to a human Mostro operator for resolution. |
Maker creates order → Mostro daemon publishes Kind 38383 to relays
Taker sees order → sends "take-order" to daemon (NIP-44 direct, Kind 14)
Daemon responds → notifies both parties via NIP-44 direct DM (Kind 14)
Seller pays Lightning hold invoice (funds locked in HTLC)
Buyer submits Lightning invoice to receive sats
Fiat transfer happens off-chain (bank transfer, cash, etc.)
Buyer confirms "fiat-sent" (NIP-44 direct, Kind 14)
Seller confirms receipt → daemon settles hold invoice
Sats released to buyer → trade complete
Both parties rate each other (optional)
| Kind | Description |
|---|---|
38383 |
Public order book (published by Mostro daemon) |
14 |
NIP-44 direct DM — encrypted trade messages to/from the daemon, signed by the trade key (Mostro protocol v2) |
1059 |
NIP-59 Gift Wrap — P2P chat between peers and dispute–admin chat |
- Full spec: mostro.network/protocol
- NIP-44 Encrypted Payloads: github.com/nostr-protocol/nips/blob/master/44.md
- NIP-59 Gift Wrap: github.com/nostr-protocol/nips/blob/master/59.md
- Order Kind 38383: mostro.network/protocol/list_orders.html
Mostro App uses a split-architecture model: all cryptography, protocol logic, and network I/O live in a Rust core; the UI shell is written in Flutter/Dart.
┌─────────────────────────────────────────────┐
│ Flutter / Dart UI │
│ Riverpod state · GoRouter · Material 3 │
│ Sembast (UI-layer persistence, all plats.) │
└──────────────────┬──────────────────────────┘
│ flutter_rust_bridge 2.x (FFI / WASM)
┌──────────────────▼──────────────────────────┐
│ Rust Core │
│ │
│ nostr-sdk 0.44 → relay pool, NIP-44/59 │
│ mostro-core 0.13.1 → protocol FSM, types, │
│ transport │
│ bip32 / bip39 → HD key derivation │
│ k256 → secp256k1 ECDH │
│ chacha20poly1305 → file encryption │
│ │
│ SQLite (native) · IndexedDB (WASM/web) │
└─────────────────────────────────────────────┘
- Offline-first — Orders and trade state persist locally; outbound messages are queued and retried when relay connectivity is restored.
- Identity via BIP-32/39 — The master identity key is derived from a BIP-39 mnemonic. Per-trade ephemeral keys are derived via BIP-32 path
m/44'/1237'/38383'/0/Nto prevent cross-trade correlation. - Zero server dependency — The app can function with any compliant Mostro node. The default node pubkey is configurable in settings.
- Type-safe bridge — The Rust API surface is defined through
flutter_rust_bridgeannotations; all Dart–Rust interactions are generated, never hand-written. - Platform parity — Web builds use
wasm-packto compile the Rust core to WASM; storage and async runtime are feature-gated per target.
| Layer | Technology | Version |
|---|---|---|
| UI Framework | Flutter | 3.x |
| UI Language | Dart | 3.x |
| Core Language | Rust | 1.94+ stable |
| Rust–Dart Bridge | flutter_rust_bridge | 2.11.1 |
| State Management | Riverpod | 2.6.1 |
| Routing | GoRouter | 14.8.1 |
| Nostr Protocol | nostr-sdk | 0.44 |
| Mostro Types / FSM / Transport | mostro-core | 0.13.1 |
| UI-layer Persistence | Sembast | 3.8.2 |
| Protocol Persistence (native) | SQLite via sqlx | 0.8 |
| Protocol Persistence (web) | IndexedDB | 0.4 |
| HD Key Derivation | bip32 / bip39 | 0.5 / 2 |
| ECDH (P2P chat keys) | k256 | 0.13 |
| File Encryption | chacha20poly1305 | 0.10 |
| Async Runtime (native) | tokio | 1 |
| Async Runtime (web) | wasm-bindgen-futures | 0.4 |
| Wallet Integration | Nostr Wallet Connect | NWC spec |
| Platform | Status |
|---|---|
| Android 5.0+ | Supported |
| iOS 13+ | Supported |
| Web (PWA) | Supported (WASM) |
| macOS 10.15+ | Supported |
| Windows 10+ | Supported |
| Linux (GTK) | Supported |
Android
Download the latest APK from the Releases page and install it, or build from source (see the Developer section below).
iOS
Available via TestFlight (link in Releases) or build from source with Xcode.
Web
Open https://mostro.network/app/ in a modern browser — it always serves the latest
build of main. No native app is installed and nothing is uploaded: the identity is generated
in your browser and stays there, and the app talks to Nostr relays directly.
(https://mostrop2p.github.io/app/ redirects there.)
The very first visit reloads itself once while it installs the service worker that supplies the cross-origin isolation headers the Rust core needs; that is expected. Two requirements follow from it: the page must be loaded over HTTPS (a service worker needs a secure context), and a browser with service workers disabled cannot run the web client.
Desktop (macOS / Windows / Linux)
Download the binary for your platform from the Releases page.
- On first launch, the app generates a BIP-39 mnemonic seed phrase.
- Back up your seed phrase immediately — it is the only way to recover your identity and trade history.
- Configure your preferred Mostro node pubkey and Nostr relays in Settings → Relays.
- (Optional) Connect a Lightning wallet via Settings → Connect Wallet (Nostr Wallet Connect) for automated invoice handling.
Make sure the following tools are installed on your system:
| Tool | Version | Install |
|---|---|---|
| Flutter SDK | 3.x stable | flutter.dev/docs/get-started |
| Rust toolchain | 1.94+ stable | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| WASM target | — | rustup target add wasm32-unknown-unknown |
| wasm-pack | latest | cargo install wasm-pack |
Rust nightly + rust-src (web only) |
— | rustup toolchain install nightly && rustup component add rust-src --toolchain nightly |
| flutter_rust_bridge CLI | 2.11.1 | cargo install flutter_rust_bridge_codegen --version 2.11.1 --locked |
| Xcode (macOS / iOS only) | 15+ | Mac App Store |
| Android Studio / NDK (Android only) | latest | developer.android.com |
git clone https://github.com/MostroP2P/app.git
cd app
# Install Dart/Flutter dependencies
flutter pub get
# Install the flutter_rust_bridge codegen CLI at the pinned version.
# It MUST match `flutter_rust_bridge` in pubspec.yaml and rust/Cargo.toml (2.11.1):
# a mismatched CLI generates bindings that fail to compile, with an error that
# never mentions versions. Re-running this is cheap if it is already installed.
cargo install flutter_rust_bridge_codegen --version 2.11.1 --locked
# Verify the Rust core compiles
cd rust && cargo build && cd ..
# Regenerate flutter_rust_bridge bindings (needed after any Rust API change).
# The wrapper checks your CLI against the pin before generating.
./scripts/frb-generate.sh# Auto-detect connected device
flutter run
# Target a specific platform
flutter run -d android
flutter run -d ios
flutter run -d chrome # Web — needs extra steps, see "Running on Web" below
flutter run -d macos
flutter run -d windows
flutter run -d linuxWeb needs two steps that the other platforms don't. On native, the Flutter build compiles the Rust core for you; on web it does not — you compile the Rust core to WebAssembly yourself, and the page must be served with cross-origin isolation headers. Skip either step and the app builds but shows a blank page.
One-time prerequisites — in addition to the table above. The wasm build compiles
the Rust core with -Z build-std, which requires a nightly toolchain with rust-src
(and the wasm target on that same nightly toolchain):
rustup toolchain install nightly
rustup component add rust-src --toolchain nightly
rustup target add wasm32-unknown-unknown --toolchain nightly
cargo install wasm-pack1. Compile the Rust core to WASM. This produces web/pkg/ (rust.js +
rust_bg.wasm). Re-run it after any change under rust/src/:
./scripts/build-web.sh # add --release for an optimized buildDo not run flutter_rust_bridge_codegen build-web directly: on current nightly it
silently produces WASM without shared memory, which crashes at runtime with
DataCloneError: ... #<Memory> could not be cloned when FRB spawns its worker pool.
The script adds the required linker flags and verifies the output (see issue #212).
If you skip this step entirely, the console shows Refused to execute script from '.../pkg/rust.js' because its MIME type ('text/html') is not executable — the WASM was
never built.
2. Run in Chrome with cross-origin isolation headers. The Rust core uses
SharedArrayBuffer, which browsers enable only under COOP/COEP:
flutter run -d chrome \
--web-header "Cross-Origin-Opener-Policy=same-origin" \
--web-header "Cross-Origin-Embedder-Policy=require-corp"Without the headers the console shows Buffers cannot be shared due to missing cross-origin headers and the page stays blank.
On first launch the app generates an identity and connects to relays; you should see the order book populate from the daemon's Kind 38383 events without running anything locally.
main is published automatically to https://mostro.network/app/ by
.github/workflows/deploy-pages.yml on every merge.
(The organization serves its Pages sites from that domain, so this project page lives under
mostro.network/app/ rather than mostrop2p.github.io/app/, which 301s to it. The sub-path
is the same either way.)
The build itself lives in the reusable
.github/workflows/web-build.yml, which the deploy calls
and CI also runs on every pull request — so a change that breaks the web target fails
before it lands, and what CI validates cannot drift from what ships. It runs the same steps as
above (scripts/frb-generate.sh, then scripts/build-web.sh --release) and finishes with:
flutter build web --release --base-href "/app/" --pwa-strategy=noneIt then verifies the bundle and smoke-tests it in headless Chrome
(test/web/smoke/smoke.mjs): the release bundle is served
cross-origin isolated under /app/, and the test asserts the page is isolated, the Flutter
view mounted, a Rust bridge call returned, and nothing errored. Static checks alone cannot
catch that — every blank-page cause below greps perfectly clean.
Three things make it work on a static host that cannot set HTTP headers:
web/coi-serviceworker.min.js(vendored, MIT — seeweb/coi-serviceworker.LICENSE) is loaded as the first script inweb/index.html. It installs a service worker that re-serves every request with the COOP/COEP headers injected, which is what makesSharedArrayBuffer— and therefore the Rust core's wasm threads — available. The cost is one automatic reload on a visitor's first load. It stays out of the way during local development, where the headers already come from--web-header.--base-href "/app/"— project pages are served from a sub-path; without it every asset 404s and the page is blank.--pwa-strategy=none— Flutter's own (deprecated) service worker would register over the same scope and evict the isolation shim, silently un-isolating the page. Offline caching is the trade-off.
The shim registers a service worker, which browsers only allow in a secure context — the site has to be reachable over HTTPS (Pages: Settings → Pages → Enforce HTTPS), or the page loads un-isolated and stays blank.
Deploying elsewhere works too, but the bundle is not portable across paths: --base-href
is baked in at build time, so rebuild with the path the site is actually served from
(/ at a domain root, /whatever/ under a sub-path). What carries over unchanged is the
isolation choice — either set Cross-Origin-Opener-Policy: same-origin and
Cross-Origin-Embedder-Policy: require-corp at the server/CDN and drop the shim, or keep
the shim and let it supply them.
# Android APK
flutter build apk --release
# Android App Bundle (Play Store)
flutter build appbundle --release
# iOS
flutter build ios --release
# Web — compile the Rust core first (see "Running on Web"), then:
./scripts/build-web.sh --release
flutter build web --release --base-href "/app/" --pwa-strategy=none
# macOS
flutter build macos --release
# Windows
flutter build windows --release
# Linux
flutter build linux --release# Rust unit tests
cargo test
# Rust linter (must pass with zero warnings)
cargo clippy -- -D warnings
# Flutter static analysis
flutter analyze
# Flutter tests
flutter testAll CI checks must pass before merging. Run both suites locally before opening a PR.
If you modify any #[frb]-annotated Rust function in rust/src/api/:
./scripts/frb-generate.shThis wraps flutter_rust_bridge_codegen generate and refuses to run when your local codegen CLI does not match the version pinned in pubspec.yaml — generating with a mismatched CLI produces bindings that fail to compile. Pass --check to verify without generating.
Do not hand-edit files under
lib/src/rust/— they are auto-generated and will be overwritten on the next codegen run. They are gitignored and regenerated on the fly, both locally and in CI, so there is nothing to commit.
app/
├── lib/ # Flutter/Dart UI shell
│ ├── core/ # App root, routing (GoRouter), theme, design tokens
│ ├── features/ # Feature modules (one per user flow)
│ │ ├── walkthrough/ # Onboarding flow
│ │ ├── home/ # Order book + filters
│ │ ├── order/ # Create / take order, invoices
│ │ ├── trades/ # Active trades, trade detail
│ │ ├── chat/ # P2P encrypted chat
│ │ ├── disputes/ # Dispute management
│ │ ├── rate/ # Post-trade rating
│ │ ├── notifications/ # Notification center
│ │ ├── drawer/ # Navigation drawer / side menu
│ │ ├── account/ # Identity, key backup
│ │ ├── about/ # Mostro node info
│ │ └── settings/ # Relays, wallet, preferences
│ ├── shared/ # Cross-feature providers, widgets, utils
│ ├── l10n/ # Localization strings (EN, ES, IT, FR, DE)
│ └── src/rust/ # Auto-generated Rust bridge (DO NOT EDIT)
│
├── rust/ # Rust core
│ └── src/
│ ├── api/ # Public bridge API surface (9 modules)
│ ├── crypto/ # BIP-32/39 derivation, ECDH, file encryption
│ ├── db/ # SQLite (native) / IndexedDB (WASM)
│ ├── mostro/ # Protocol FSM & state machine
│ ├── nostr/ # Relay pool, gift wrap, event parsers
│ ├── nwc/ # Nostr Wallet Connect client
│ └── queue/ # Offline outbound message queue
│
├── rust_builder/ # iOS/macOS Cargokit build integration
├── test/ # Flutter widget tests
├── specs/ # Design specs, planning docs, API contracts
├── assets/ # Walkthrough images, fiat currency data
├── pubspec.yaml # Dart/Flutter manifest
└── rust/Cargo.toml # Rust manifest
Contributions are welcome. Please read this section before opening an issue or pull request.
- Search existing issues first to avoid duplicates.
- Open a new issue using the Bug Report template.
- Include: platform, Flutter/Rust versions, reproduction steps, and relevant logs (
flutter run --verboseoradb logcat).
- Open a Feature Request issue.
- Describe the use case — what problem does it solve and who benefits?
- Features that align with the Mostro protocol roadmap are prioritized.
-
Fork the repository and create a branch from
main:git checkout -b feat/your-feature-name
-
Follow the code style:
- Dart: standard Flutter conventions, enforced by
analysis_options.yaml - Rust:
cargo fmt+cargo clippy -- -D warnings(zero warnings required) - Commits: use Conventional Commits —
feat:,fix:,refactor:,docs:,test:
- Dart: standard Flutter conventions, enforced by
-
Write tests for any new Rust API functions. Flutter widget tests are encouraged for new screens.
-
Keep PRs focused — one concern per PR. Large refactors should be discussed in an issue first.
-
Ensure all checks pass locally before pushing:
cargo test && cargo clippy -- -D warnings flutter analyze && flutter test
-
Open the PR against
main, fill in the PR template, and link the related issue. -
Maintainers will review within a few days. Address feedback with new commits — do not force-push a branch under review.
| Branch pattern | Purpose |
|---|---|
main |
Protected — requires passing CI and one approving review |
feat/<name> |
New features |
fix/<name> |
Bug fixes |
docs/<name> |
Documentation only |
refactor/<name> |
Code restructuring without behavior change |
- Bridge changes: Any modification to
rust/src/api/requires re-running./scripts/frb-generate.sh. The generated files are gitignored — there is nothing to commit. - Serde conventions:
mostro-coreuses#[serde(rename_all = "kebab-case")]— all protocol status strings on the wire are kebab-case (e.g.,"waiting-buyer-invoice","fiat-sent","in-progress"). pubvspub(crate): Only types that must be exposed to the Dart bridge should bepub. Internal helpers and types wrappingnostr-sdkstructs should bepub(crate)to prevent broken FRB stub generation.- Key derivation: Per-trade keys follow BIP-32 path
m/44'/1237'/38383'/0/N. Never reuse the master identity key for trade-level messages. - Offline-first: New trade actions must go through the outbound message queue (
rust/src/queue/) so they are retried on reconnect. Do not call the relay client directly from action handlers.
This project follows the Contributor Covenant Code of Conduct. Be respectful, constructive, and inclusive. Violations can be reported to the maintainers via the contact listed in the GitHub organization profile.
The app is localized in:
| Language | Code | File |
|---|---|---|
| English | en |
lib/l10n/app_en.arb |
| Spanish | es |
lib/l10n/app_es.arb |
| Italian | it |
lib/l10n/app_it.arb |
| French | fr |
lib/l10n/app_fr.arb |
| German | de |
lib/l10n/app_de.arb |
To add a new language:
- Copy
lib/l10n/app_en.arbtolib/l10n/app_<code>.arb - Translate all string values (keep the
"@@locale"key correct) - Run
flutter gen-l10nto regenerate the Dart localizations — it auto-detects the new.arbfile (nol10n.yamlchange needed) - Open a PR — translation contributions are always welcome
If you discover a security vulnerability, please do not open a public issue. Instead:
- Open a GitHub Security Advisory (preferred), or
- Email the maintainers directly via the contact in the GitHub organization profile.
We aim to acknowledge reports within 72 hours and provide a fix within 30 days for critical issues.
- Keys never leave the device — the BIP-39 seed is stored in platform secure storage (
flutter_secure_storage, backed by Android Keystore / iOS Keychain / Linux SecretService). - End-to-end encrypted trade messages — all communication between the app and the Mostro daemon uses NIP-44 encrypted direct messages (secp256k1 ECDH + ChaCha20 + HMAC-SHA256). Peer-to-peer and dispute chat use NIP-59 Gift Wrap.
- Per-trade ephemeral keys — a new BIP-32 child key is derived for each trade, preventing cross-trade correlation even if a single trade key is compromised.
- The Mostro daemon never sees plaintext — all messages are encrypted to the daemon's public key; only the holder of the corresponding private key can decrypt them.
- No telemetry — the app does not collect analytics, crash reports, or any usage data.
MIT License
See LICENSE for the full text.
Mostro App is an independent open-source project. It is not affiliated with any centralized exchange or financial institution.