# Development Fee Technical Specification
## Overview
The development fee mechanism provides sustainable funding for Mostro development by automatically sending a configurable percentage of the Mostro fee to a lightning address set on `DEV_FEE_LIGHTNING_ADDRESS` on each successful order.
**Key Design Principles:**
- Transparent and configurable
- Non-blocking (failures don't prevent order completion)
- Full audit trail for accountability
- Node operator contribution model (mostrod pays from its earnings, not users)
## Implementation Status
### Phase 1: Infrastructure ✅ COMPLETE
**What's Implemented:**
- Configuration constants (MIN/MAX percentages, Lightning address) in `src/config/constants.rs`
- Settings validation on daemon startup in `src/config/util.rs`
- Database schema with 3 columns: `dev_fee`, `dev_fee_paid`, `dev_fee_payment_hash`
- Database fields initialized in order creation (currently hardcoded to 0)
- Default dev_fee_percentage: 0.30 (30%)
**Status:** Ready for Phase 2 implementation
### Phase 2: Fee Calculation ✅ COMPLETE
**Implemented Components:**
- `calculate_dev_fee()` pure function in `src/util.rs`
- `get_dev_fee()` wrapper function in `src/util.rs`
- Dev fee calculation (total amount paid by mostrod from its earnings)
- Integration in message amount calculations across 4 critical locations
- Unit tests for fee calculation logic (4 tests passing)
**Implementation Details:**
- Two-function approach: Pure calculation function + Settings wrapper
- Simple rounding for whole satoshi amounts
- Dev fee tracked for payment by mostrod
- User payments simplified (no dev_fee in buyer/seller amounts)
**Status:** ✅ Complete - Fee calculations implemented and tested across all order flows
### Phase 3: Payment Execution ✅ COMPLETE
**Implemented Components:**
- `send_dev_fee_payment()` function in `src/app/release.rs`
- Scheduler job `process_dev_fee_payment()` in `src/scheduler.rs`
- Database query function `find_unpaid_dev_fees()` in `src/db.rs`
- **Database field updates:**
- `dev_fee`: Always set to 0 at order creation, calculated when order is taken (unified for all order types)
- `dev_fee_paid`: Changed from 0 to 1 after successful payment in scheduler
- `dev_fee_payment_hash`: Set to Lightning payment hash on successful payment
- Error handling and retry logic with automatic retries every 60 seconds
- Payment timeout handling (LNURL: 15s, send_payment: 5s, result: 25s, total: 50s per attempt)
- Dev fee amount validation (rejects payments with dev_fee <= 0)
- Enhanced logging with BEFORE/AFTER state tracking and database verification
- Race condition handling (query checks both 'settled-hold-invoice' AND 'success' statuses)
- Unified dev_fee calculation at order take time for both fixed and market price orders
- Invoice validation fix ensuring dev_fee is calculated before validating buyer invoices
**Status:** ✅ Complete - Automated dev fee payment system fully operational with unified calculation logic
**Implementation Commits:**
- f508669: feat: implement automated development fee payment system
- 102cfed: fix: resolve dev fee payment failures with two critical fixes
- eaf3319: Validate dev_fee amount before attempting payment
- 42253f1: Fix edge case on dev fund payment
- 2655943: fix: improve dev fee payment reliability and coverage
- 2349669: refactor: unify dev_fee calculation at order take time
- 7e9b0a0: fix: calculate dev_fee before invoice validation in take_sell
### Phase 4: Audit Events via Nostr ✅ COMPLETE
**What Was Implemented:**
- Custom Nostr event kind (8383) for dev fee payment audits
- Event publishing in scheduler after successful payment
- Public relay distribution for third-party verification and total tracking
- Complete payment details: order_id, dev_fee, payment_hash, timestamp
- Non-blocking event publication (payment succeeds even if event fails)
**Status:** ✅ Complete - Dev fee payments are auditable via Nostr events (see Phase 4 section for full specification)
**Key Design Decisions:**
1. **Scheduler-Based vs Inline Payment:**
- Chose scheduler-based to avoid blocking order completion
- 60-second interval balances responsiveness with resource usage
- Automatic retry mechanism handles transient failures
2. **Dual Status Query:**
- Critical for handling race conditions
- Ensures dev fee is collected even if buyer payment completes during dev fee payment
- Prevents revenue loss from timing edge cases
3. **Enhanced Logging:**
- BEFORE/AFTER/VERIFY pattern provides complete audit trail
- Essential for debugging database persistence issues
- Helps identify race conditions and timing problems
4. **Validation Before Payment:**
- Fail-fast approach for invalid amounts
- Reduces unnecessary network calls
- Clearer error messages for debugging
## Architecture
### Fee Flow Diagram
```
Order Creation → Fee Calculation → Hold Invoice → Seller Release → Buyer Payment → Dev Payment
↓ ↓ ↓ ↓ ↓ ↓
amount mostro_fee seller pays settle hold buyer paid mostrod pays
↓ amount + fee ↓ ↓ ↓
dev_fee mostrod collects status=success send dev_fee
(tracking) mostro fee from earnings
↓
update db fields
```
### System Components
1. **Configuration Layer** (`src/config/`)
- `constants.rs`: Hardcoded constraints (10-100%, Lightning Address)
- `types.rs`: MostroSettings with dev_fee_percentage
- `util.rs`: Startup validation
2. **Fee Calculation** (`src/util.rs`)
- `get_dev_fee()`: Computes percentage of Mostro fee
- `prepare_new_order()`: Calculates fees during order creation
- `show_hold_invoice()`: Includes dev fee in seller's hold invoice
3. **Payment Execution** (`src/app/release.rs`)
- `send_dev_fee_payment()`: LNURL resolution and payment
- `payment_success()`: Integration point after buyer payment
4. **Database Schema** (`migrations/20251126120000_dev_fee.sql`)
- `dev_fee`: Amount in satoshis
- `dev_fee_paid`: Boolean (0/1)
- `dev_fee_payment_hash`: Payment hash for reconciliation
## Configuration
### Constants (`src/config/constants.rs`)
```rust
pub const MIN_DEV_FEE_PERCENTAGE: f64 = 0.10; // 10% minimum
pub const MAX_DEV_FEE_PERCENTAGE: f64 = 1.0; // 100% maximum
pub const DEV_FEE_LIGHTNING_ADDRESS: &str = "<dev@lightning.address>";
```
### Settings (`~/.mostro/settings.toml`)
```toml
[mostro]
# Development sustainability fee
# Percentage of Mostro fee sent to development fund (minimum 10%)
dev_fee_percentage = 0.30
```
**Validation Rules:**
- Must be between 0.10 (10%) and 1.0 (100%)
- Validated on daemon startup
- Invalid values cause startup failure with error message
### Default Values
- `dev_fee_percentage`: 0.30 (30%)
- Configured in `src/config/types.rs::MostroSettings::default()`
## Technical Implementation
### Fee Calculation
**Current State:** ✅ IMPLEMENTED - Two functions provide fee calculation with proper satoshi handling.
**Implementation:**
Two-function approach in `src/util.rs`:
1. **Pure calculation function**:
```rust
pub fn calculate_dev_fee(total_mostro_fee: i64, percentage: f64) -> i64 {
let dev_fee = (total_mostro_fee as f64) * percentage;
dev_fee.round() as i64
}
```
2. **Settings wrapper**:
```rust
pub fn get_dev_fee(total_mostro_fee: i64) -> i64 {
let mostro_settings = Settings::get_mostro();
calculate_dev_fee(total_mostro_fee, mostro_settings.dev_fee_percentage)
}
```
**Benefits of Two-Function Approach:**
- `calculate_dev_fee()` is pure, testable without global config
- `get_dev_fee()` provides convenient access using Settings
- Tests can verify behavior with different percentages
- Production code uses Settings seamlessly
**Formula Specification:**
```
total_dev_fee = round(total_mostro_fee × dev_fee_percentage)
```
**Why This Formula:**
- Simple percentage calculation of total Mostro fee
- Mostrod pays this amount from its earnings to the development fund
- No need for buyer/seller split since users don't pay dev_fee
- Rounding ensures whole satoshi amounts
**Examples:**
- Total Mostro fee: 1,000 sats, Percentage: 30% → Dev fee: 300 sats (paid by mostrod) ✓
- Total Mostro fee: 1,003 sats, Percentage: 30% → Dev fee: 301 sats (paid by mostrod) ✓
- Total Mostro fee: 333 sats, Percentage: 30% → Dev fee: 100 sats (paid by mostrod) ✓
- Mostro fee: 0 sats → Dev fee: 0 sats ✓
### Order Creation
**Current State:** ✅ IMPLEMENTED - ALL orders are created with `dev_fee = 0`. The `dev_fee` is calculated when the order is taken, unifying the behavior for both fixed price and market price orders. Database fields `dev_fee_paid` and `dev_fee_payment_hash` are initialized to `false` and `None` respectively.
**Implementation:** `src/util.rs::prepare_new_order()`
When creating a new order:
1. Calculate Mostro fee: `fee = get_fee(amount)` (for fixed price orders where amount > 0)
2. Set `dev_fee = 0` for ALL orders (both fixed price and market price)
3. Store in Order struct with `dev_fee_paid = false` and `dev_fee_payment_hash = None`
**Dev Fee Calculation at Take Time:**
- ALL orders (both fixed price and market price) have `dev_fee = 0` at creation
- Dev fee is calculated when order is taken (see "Dev Fee Calculation at Take Time" section)
- Implemented in `take_buy.rs` and `take_sell.rs`
- Formula: `dev_fee = get_dev_fee(fee * 2)` where fee is calculated based on the order amount
### Market Price Orders and Dev Fee Reset
**Critical Behavior for Market Price Orders:**
When a user takes a market price order, the following sequence occurs:
1. **Order Taken** (status changes from `pending` to `waiting-buyer-invoice` or `waiting-payment`):
- Dev fee is calculated based on current market price
- `order.dev_fee` field is updated with the calculated amount
- Order waits for taker to provide invoice (sell order) or make payment (buy order)
2. **Taker Abandons Order** (doesn't provide invoice or make payment):
- Order status is reset back to `pending`
- **CRITICAL:** `order.dev_fee` field **MUST** be reset to `0`
- This prevents incorrect dev fee charges if order is re-taken at different price
3. **Why Reset is Necessary:**
- Market prices fluctuate continuously
- Next taker may take order at different fiat amount
- Mostro fee and dev fee must be recalculated for new amount
- Leaving old dev_fee value would cause incorrect accounting
**Example Scenario:**
```
Initial Order: 100,000 sats at $50,000/BTC (market price)
- Mostro fee: 1,000 sats
- Dev fee (30%): 300 sats
- Order status: pending, dev_fee: 0
Taker 1 takes order at $50,000/BTC:
- Order status: waiting-buyer-invoice
- Dev fee calculated: 300 sats
- Order dev_fee: 300
Taker 1 abandons (doesn't provide invoice):
- Order status: pending
- Dev fee RESET: 0 ← CRITICAL RESET
- Order dev_fee: 0
Taker 2 takes order at $52,000/BTC (price increased):
- Order status: waiting-buyer-invoice
- Dev fee recalculated: 310 sats (new amount)
- Order dev_fee: 310
Taker 2 completes order:
- Order status: success
- Dev fee paid: 310 sats (correct amount for actual trade)
```
**Implementation Requirements:**
When order status transitions back to `pending` from any intermediate state (`waiting-buyer-invoice`, `waiting-payment`, etc.):
```rust
// Reset order state for market price orders
if order.is_market_price() {
order.dev_fee = 0; // Reset to zero
order.status = Status::Pending;
order.update(pool).await?;
}
```
**Database Consistency:**
Orders in `pending` status should always have `dev_fee = 0` for market price orders. You can verify this:
```sql
-- Should return 0 rows (all pending market orders should have dev_fee = 0)
SELECT id, premium, dev_fee
FROM orders
WHERE status = 'pending'
AND premium IS NULL -- market price indicator
AND dev_fee != 0;
```
### Dev Fee Calculation at Take Time
**Implementation Status:** ✅ IMPLEMENTED - Unified calculation for both fixed price and market price orders
**Critical Implementation Detail:**
When ANY order is taken (both fixed price and market price), the `dev_fee` is calculated. This ensures consistent behavior across all order types.
**Locations:**
- `/home/negrunch/dev/mostro/src/app/take_buy.rs`
- `/home/negrunch/dev/mostro/src/app/take_sell.rs`
**Actual Implementation (take_buy.rs and take_sell.rs):**
```rust
// For market price orders: calculate amount, fee, and dev_fee
if order.has_no_amount() {
match get_market_amount_and_fee(order.fiat_amount, &order.fiat_code, order.premium).await {
Ok(amount_fees) => {
order.amount = amount_fees.0;
order.fee = amount_fees.1;
let total_mostro_fee = order.fee * 2;
order.dev_fee = get_dev_fee(total_mostro_fee);
}
Err(_) => return Err(MostroInternalErr(ServiceError::WrongAmountError)),
};
} else {
// For fixed price orders: calculate dev_fee only (amount and fee already set at creation)
let total_mostro_fee = order.fee * 2;
order.dev_fee = get_dev_fee(total_mostro_fee);
}
```
**Why This Is Critical:**
1. **Unified Behavior:** Both fixed price and market price orders calculate `dev_fee` at the same time (when taken)
2. **Consistency:** All pending orders have `dev_fee = 0`, all taken orders have `dev_fee > 0`
3. **Simplicity:** Single point of calculation makes the code easier to understand and maintain
4. **Correctness:** Ensures all order types have correct dev_fee for payment collection
**Example Scenarios:**
**Fixed Price Order:**
```
Order Created:
- amount: 100,000 sats (known at creation)
- fee: 1,000 sats (calculated at creation)
- dev_fee: 0 (NOT calculated at creation anymore)
- Status: pending
Order Taken:
- amount: 100,000 sats (unchanged)
- fee: 1,000 sats (unchanged)
- total_mostro_fee: 2,000 sats (both parties)
- dev_fee: 600 sats (30%) ← Calculated when taken
- Status: waiting-buyer-invoice
Order Completes:
- Dev fee payment triggered: 600 sats sent to dev fund ✓
```
**Market Price Order:**
```
Order Created:
- Fiat: $100 USD
- amount: 0 (unknown until taken)
- fee: 0
- dev_fee: 0
- Status: pending
Order Taken:
- Market Price Lookup: $100 @ $50,000 = 200,000 sats
- amount: 200,000 sats ← Calculated
- fee: 2,000 sats (1%) ← Calculated
- total_mostro_fee: 4,000 sats (both parties)
- dev_fee: 1,200 sats (30%) ← Calculated
- Status: waiting-buyer-invoice
Order Completes:
- Dev fee payment triggered: 1,200 sats sent to dev fund ✓
```
### Taker Abandonment and Order Reset
**Implementation Status:** ✅ IMPLEMENTED
**Critical Behavior:** When a taker abandons an order (doesn't proceed and order times out), all price-dependent fields must be reset to ensure correct recalculation when the order is re-taken.
**Two Reset Paths:**
1. **Explicit Cancellation** (`src/app/cancel.rs::reset_api_quotes()`):
- Taker explicitly calls cancel action
- Resets: `amount = 0`, `fee = 0`, `dev_fee = 0`
- Status: ✅ Implemented
2. **Automatic Timeout** (`src/scheduler.rs::job_cancel_orders()`):
- Scheduler detects taker hasn't proceeded within `expiration_seconds`
- Resets: `amount = 0`, `fee = 0`, `dev_fee = 0`
- Status: ✅ Implemented
**Why All Three Must Reset:**
- Market price can change between takes
- Fee is calculated from amount
- Dev fee is calculated from fee
- Leaving stale `dev_fee` value causes incorrect charges on re-take
**Example Flow:**
```
Order Created (market price):
- amount: 0, fee: 0, dev_fee: 0, status: pending
Order Taken at BTC=$50,000:
- amount: 200,000 sats, fee: 2,000 sats, dev_fee: 600 sats
- status: waiting-buyer-invoice
Taker Abandons (timeout after expiration_seconds):
- Scheduler detects timeout (taken_at > expiration_seconds)
- Resets: amount: 0, fee: 0, dev_fee: 0
- status: pending (ready for new taker)
Order Re-taken at BTC=$52,000 (price increased):
- amount: 192,308 sats, fee: 1,923 sats, dev_fee: 577 sats
- Correct dev_fee for new market price ✓
```
**Implementation Details:**
- `src/app/cancel.rs`: `reset_api_quotes()` function
- `src/scheduler.rs`: Automatic timeout handler in `job_cancel_orders()`
- Both paths use same logic:
```rust
if order.price_from_api {
order.amount = 0;
order.fee = 0;
order.dev_fee = 0;
}
```
- Database function: `update_order_to_initial_state()` persists the reset values
**Database Persistence Fix:**
Prior to commit `c803471`, the `update_order_to_initial_state()` function in `src/db.rs`
did not include `dev_fee` in its SQL UPDATE statement, causing stale dev_fee values to
remain in the database even though the in-memory Order struct had `dev_fee = 0`.
**Before Fix:**
- Memory: `order.dev_fee = 0` ✓
- Database: `dev_fee = 300` (stale) ✗
- After `edit_pubkeys_order()` fetches from DB: `order.dev_fee = 300` (wrong!) ✗
**After Fix:**
- Memory: `order.dev_fee = 0` ✓
- Database: `dev_fee = 0` ✓
- After `edit_pubkeys_order()` fetches from DB: `order.dev_fee = 0` ✓
The fix added `dev_fee` as a parameter to `update_order_to_initial_state()` and
included it in the SQL UPDATE statement, ensuring the value is properly persisted
to the database.
**Configuration:**
- Timeout duration: Configured via `expiration_seconds` in settings
- Default: Orders return to pending after taker hasn't proceeded for configured time
### Hold Invoice Generation
**Current State:** ✅ IMPLEMENTED - Hold invoices include only the Mostro fee (no dev fee).
**Implementation:** `src/util.rs::show_hold_invoice()`
Seller's hold invoice includes only the order amount and Mostro fee:
```rust
// Seller pays the order amount plus their Mostro fee
// Dev fee is NOT charged to seller - it's paid by mostrod from its earnings
let new_amount = order.amount + order.fee;
// Now we generate the hold invoice that seller should pay
let (invoice_response, preimage, hash) = ln_client...
```
**Key Points:**
- `order.dev_fee` stores the total dev fee for tracking purposes only
- Seller pays only `order.amount + order.fee` (no dev_fee added)
- Buyer receives only `order.amount - order.fee` (no dev_fee subtracted)
- Hold invoice amount = `order.amount + order.fee` (transparent, as advertised)
- Mostrod pays `dev_fee` from its earnings after collecting the Mostro fee
### Message Amount Calculations
**Implementation Status:** ✅ COMPLETE
When creating messages for buyers and sellers during order flow, the amounts include only the Mostro fee (not dev_fee). The implementation ensures correct amounts are communicated to both parties.
**Seller Messages:**
```rust
seller_order.amount = order.amount.saturating_add(order.fee);
```
**Buyer Messages:**
```rust
buyer_order.amount = order.amount.saturating_sub(order.fee);
```
**Critical Implementation Locations:**
1. **`src/flow.rs::hold_invoice_paid()`**
- Purpose: Status updates after seller payment
- Seller amount: `order.amount + order.fee` (no dev_fee)
- Buyer amount: `order.amount - order.fee` (no dev_fee)
- Impact: Initial payment confirmation messages
2. **`src/app/add_invoice.rs::add_invoice_action()`**
- Purpose: Invoice acceptance flow
- Seller amount: `order.amount + order.fee` (no dev_fee)
- Buyer amount: `order.amount - order.fee` (no dev_fee)
- Impact: Order acceptance notifications
3. **`src/app/release.rs::check_failure_retries()`**
- Purpose: Payment failure handling
- Buyer amount: `order.amount - order.fee` (no dev_fee)
- Impact: Failure notification amounts
4. **`src/app/release.rs::do_payment()`** ⚠️ **CRITICAL**
- Purpose: Actual Lightning payment calculation
- Payment amount: `order.amount - order.fee` (no dev_fee)
- Impact: **Real sats transferred** to buyer via Lightning
- Why critical: Determines actual payment amount, not just messages
**Implementation Pattern:**
All locations follow the same simplified pattern:
```rust
// No dev_fee split needed - users only pay Mostro fee
seller_amount = order.amount + order.fee;
buyer_amount = order.amount - order.fee;
```
### Example Calculation
**Order Amount**: 100,000 sats
**Mostro Fee (1%)**: 1,000 sats (split: 500 buyer + 500 seller)
**Dev Fee Percentage**: 30%
**Total Dev Fee**: 1,000 × 0.30 = 300 sats
**Seller Pays**: 100,000 + 500 = **100,500 sats**
**Buyer Receives**: 100,000 - 500 = **99,500 sats**
**Mostrod pays**: 300 sats (to development fund)
**Fee Distribution:**
- Buyer pays: 500 (Mostro fee) = **500 sats total**
- Seller pays: 500 (Mostro fee) = **500 sats total**
- Mostrod receives: 1,000 - 300 = **700 sats** (keeps after donating to dev fund)
- Dev fund receives: **300 sats** (paid by mostrod from its earnings)
### Edge Cases
**Rounding**:
- Total: 333 sats Mostro fee × 30% = 99.9 → **100 sats dev fee** (paid by mostrod)
- Total: 3 sats Mostro fee × 30% = 0.9 → **1 sat dev fee** (paid by mostrod)
- **Odd or even numbers**: No longer matters, mostrod pays the total rounded amount
- Formula: `dev_fee = round(total_mostro_fee × dev_fee_percentage)`
- Implementation: `src/util.rs`
**Zero Fee Orders**:
- If `mostro_fee = 0`, then `dev_fee = 0`
- No dev payment attempted
**Tiny Amounts**:
- Smallest: 1 sat Mostro fee × 10% = 0.1 → **0 sats** dev fee (rounds to zero)
- No dev payment attempted for 0 sat dev fees
### Payment Execution
**Current State:** ✅ IMPLEMENTED - Automated dev fee payment system fully operational with scheduler-based processing.
**Implementation:**
**Scheduler-Based Payment Trigger:**
The dev fee payment is executed by mostrod **from its earnings**, not from users. The payment happens asynchronously:
1. **Order Release** (`src/app/release.rs::release_action()`):
- Seller's hold invoice is settled
- Order is marked as `status = 'settled-hold-invoice'`
- Mostrod collects the Mostro fee (1,000 sats in our example)
- Order is **enqueued for scheduler processing** by marking `dev_fee_paid = false`
- Mostro then proceeds to pay buyer's invoice
- **Key Point:** Dev fee payment happens asynchronously AFTER mostrod collects the Mostro fee
2. **Scheduler Processing** (`src/scheduler.rs::job_process_dev_fee_payment()`):
- Runs every 60 seconds
- Uses `find_unpaid_dev_fees()` to query database for orders where: `(status = 'settled-hold-invoice' OR status = 'success') AND dev_fee > 0 AND dev_fee_paid = 0`
- **Important:** Query checks BOTH statuses to handle race conditions where buyer payment succeeds during dev fee payment
- Processes each unpaid dev fee asynchronously with 50-second timeout per payment attempt
- Mostrod pays the dev_fee amount (e.g., 300 sats) from its earnings to the development fund
- Enhanced logging: Logs BEFORE/AFTER state, database update results, and verification queries
**Why This Timing?**
- **Fee Earned:** Seller has released funds, so mostrod has earned the Mostro fee
- **Contribution from earnings:** Mostrod donates a percentage of what it earned to the dev fund
- **Risk Mitigation:** Dev fee payment independent of buyer payment status
- **Non-blocking:** Order flow continues regardless of dev fee payment status
- **Retry mechanism:** Failed payments are automatically retried every 60 seconds
**Why Scheduler-Based?**
- **Non-blocking order completion:** Seller release and buyer payment happen immediately, dev fee payment happens asynchronously
- **Retry mechanism:** Failed payments are automatically retried on the next cycle (60 seconds)
- **Fault tolerance:** Order completes successfully even if dev fee payment fails temporarily
- **Better user experience:** Users don't wait for dev fee payment during order release
**Payment Flow Specification (4 Steps with Timeouts):**
Implementation in `src/app/release.rs::send_dev_fee_payment()`:
```rust
// [Step 0] Validation - Reject invalid amounts
if order.dev_fee <= 0 {
return Err(MostroInternalErr(ServiceError::WrongAmountError));
}
// [Step 1/4] LNURL resolution (15 second timeout)
let payment_request = tokio::time::timeout(
std::time::Duration::from_secs(15),
resolv_ln_address(DEV_FEE_LIGHTNING_ADDRESS, dev_fee_amount)
).await?;
// [Step 2/4] Create LND connector
let ln_client = LndConnector::new().await?;
// [Step 3/4] Send payment (5 second timeout for send_payment call + 25 second timeout for payment result)
let send_result = tokio::time::timeout(
std::time::Duration::from_secs(5),
ln_client.send_payment(&payment_request, dev_fee_amount, tx)
).await?;
// Wait for payment result (25 second timeout)
let payment_result = tokio::time::timeout(
std::time::Duration::from_secs(25),
rx.recv()
).await?;
```
**Total Time Budget:**
- Validation: Instant (< 1ms)
- LNURL resolution: 15s max
- send_payment call: 5s max (prevents hanging on self-payments or network issues)
- Payment result wait: 25s max
- **Total: ~45s max** (under 50s scheduler timeout)
**Validation Note:** The function validates `dev_fee > 0` before attempting payment. This was added in commit eaf3319 to prevent unnecessary payment attempts for orders with zero dev fees.
**Success Response:**
```rust
Ok(hash) => {
order.dev_fee_paid = true;
order.dev_fee_payment_hash = Some(hash);
// Database updated, won't be retried
}
```
**Failure Response:**
```rust
Err(e) => {
order.dev_fee_paid = false;
// Logged for audit
// Will be retried on next scheduler cycle (60 seconds)
}
```
### Database Field Updates
This section details exactly when and how the database fields (`dev_fee`, `dev_fee_paid`, `dev_fee_payment_hash`) are modified throughout the order lifecycle.
#### `dev_fee` Field Lifecycle
**When Initialized:** During order creation in `src/util.rs::prepare_new_order()`
**Initialization:**
```rust
let mut fee = 0;
let dev_fee = 0;
if new_order.amount > 0 {
fee = get_fee(new_order.amount);
// dev_fee is NOT calculated here — always initialized to 0
// It is calculated later when the order is taken
}
```
**Initial Value:** Always 0 (zero) — dev fee is calculated later when the order is taken (in `take_buy.rs` / `take_sell.rs`), not at creation time.
**Purpose:** Tracking amount that mostrod will pay to the development fund from its earnings (not charged to users)
**Special Case - Market Price Orders:** When a market price order returns to `pending` status (taker abandons), the `dev_fee` field **MUST** be reset to `0` to allow recalculation at the new market price when re-taken. This is documented in detail in the "Market Price Orders and Dev Fee Reset" section.
**Database State:** Persists throughout order lifecycle unless order returns to pending status (market price orders only).
#### `dev_fee_paid` Field Updates
**Initial Value:** `0` (false) - Set during order creation in `src/util.rs::prepare_new_order()`
**When Changed to `1` (true):** After successful dev fee payment in the scheduler job `process_dev_fee_payment()` in `src/scheduler.rs`
**Trigger Sequence:**
1. Seller releases order (`status = 'settled-hold-invoice'`)
2. Scheduler runs every 60 seconds
3. Query identifies unpaid orders: `SELECT * FROM orders WHERE (status = 'settled-hold-invoice' OR status = 'success') AND dev_fee > 0 AND dev_fee_paid = 0`
4. Scheduler calls `send_dev_fee_payment()` for each unpaid order
5. On payment success, scheduler updates: `order.dev_fee_paid = true` (stored as `1` in database)
6. Important: Query includes BOTH statuses to handle race conditions where buyer payment completes during dev fee payment failure
**Database Update:**
```sql
UPDATE orders
SET dev_fee_paid = 1, dev_fee_payment_hash = ?
WHERE id = ?
```
**Timing:** Asynchronously after order completes, typically within 60 seconds (next scheduler cycle)
**Remains `0` When:**
- Payment hasn't been attempted yet (order just completed)
- Payment failed (LNURL resolution error, routing failure, timeout)
- Will retry on next scheduler cycle (60 seconds)
#### `dev_fee_payment_hash` Field Updates
**Initial Value:** `NULL` - Set during order creation in `src/util.rs::prepare_new_order()`
**When Set:** Simultaneously with `dev_fee_paid = 1` after successful payment
**Value Source:** Lightning payment hash returned from the Lightning Network payment result. The hash comes from the `ln_client.send_payment()` call's result channel in `src/app/release.rs::send_dev_fee_payment()`.
**Payment Hash Retrieval Flow:**
```rust
// Step 1: LNURL resolution (15s timeout)
let payment_request = resolv_ln_address(DEV_FEE_LIGHTNING_ADDRESS, dev_fee_amount).await?;
// Step 2: Send payment (5s timeout for send, 25s for result)
let payment_result = ln_client.send_payment(&payment_request, dev_fee_amount, tx).await?;
// Step 3: Extract hash from successful payment result
let payment_hash = rx.recv().await?; // ← THIS is what goes into dev_fee_payment_hash
```
**Format:** 64-character hexadecimal string (standard Lightning payment hash)
**Purpose:**
- Audit trail for reconciliation
- Proof of payment for accountability
- Debugging and payment verification
**Remains `NULL` When:**
- Payment hasn't been attempted yet
- Payment failed (no hash generated for failed payments)
#### Payment Flow Timeline
Complete timeline showing database field states at each stage:
```
Order Creation (t=0):
└─> dev_fee = 0 (always zero at creation)
└─> dev_fee_paid = 0
└─> dev_fee_payment_hash = NULL
└─> Database: INSERT INTO orders (dev_fee, dev_fee_paid, dev_fee_payment_hash) VALUES (0, 0, NULL)
Order Taken (t=take):
└─> dev_fee = calculated_value (e.g., 300 sats)
└─> Calculated in take_buy.rs / take_sell.rs based on total_mostro_fee × dev_fee_percentage
└─> Database: UPDATE orders SET dev_fee = 300 WHERE id = ?
Order Processing (t=minutes):
└─> Buyer and seller complete trade
└─> Dev fee fields remain unchanged
└─> Database: No updates to dev_fee fields
Order Release (t=seller_release):
└─> Seller's hold invoice settled
└─> status = 'settled-hold-invoice'
└─> Dev fee fields unchanged (dev_fee_paid = 0)
└─> Order enters scheduler queue
└─> Buyer payment initiated (asynchronous)
Scheduler Cycle (t=next_60s_cycle, typically within 60s of seller release):
└─> Scheduler wakes up every 60 seconds
└─> Query: SELECT * FROM orders WHERE (status = 'settled-hold-invoice' OR status = 'success') AND dev_fee > 0 AND dev_fee_paid = 0
└─> Order found in unpaid queue
└─> Call: send_dev_fee_payment(order)
Payment Attempt (t=payment_start):
└─> Step 1/3: LNURL resolution (timeout: 15s)
└─> Step 2/3: LND send_payment call (timeout: 5s)
└─> Step 3/3: Wait for payment result (timeout: 25s)
Dev Fee Payment Success (t=payment_complete, ~3-8 seconds typical):
└─> Payment hash received: "a1b2c3d4e5f6..."
└─> dev_fee_paid = 1
└─> dev_fee_payment_hash = "a1b2c3d4e5f6..."
└─> Database: UPDATE orders SET dev_fee_paid = 1, dev_fee_payment_hash = 'a1b2c3d4e5f6...' WHERE id = ?
└─> Order removed from retry queue
└─> DONE ✓
Order Success (t=order_complete, after dev fee payment):
└─> status = 'success'
└─> Buyer receives satoshis
└─> Dev fee already paid (dev_fee_paid = 1)
└─> Database: UPDATE orders SET status = 'success' WHERE id = ?
Dev Fee Payment Failure (t=payment_timeout, could be 15s, 25s, or 50s timeout):
└─> Error logged (LNURL failure, routing failure, timeout, etc.)
└─> dev_fee_paid = 0 (unchanged)
└─> dev_fee_payment_hash = NULL (unchanged)
└─> Database: No update (fields remain unchanged)
└─> Order remains in retry queue
└─> Retry on next scheduler cycle (60 seconds later)
└─> Will retry indefinitely until payment succeeds
Edge Case - Buyer Payment Fails:
└─> Order remains in status = 'settled-hold-invoice'
└─> failed_payment = true
└─> Retry scheduler (job_retry_failed_payments) attempts buyer payment again
└─> Dev fee already paid regardless (dev_fee_paid = 1)
└─> This is correct: seller released, fee was earned
Edge Case - Dev Fee Payment Fails, Buyer Payment Succeeds (Race Condition):
└─> Seller releases → status = 'settled-hold-invoice'
└─> Scheduler attempts dev fee payment → FAILS (dev_fee_paid = 0)
└─> Simultaneously, buyer payment → SUCCEEDS → status = 'success'
└─> Order now in 'success' status with dev_fee_paid = 0
└─> Query includes both statuses, so order is still picked up on next cycle
└─> Dev fee payment eventually succeeds and updates dev_fee_paid = 1
```
#### Actual Implementation
The actual implementation in `src/scheduler.rs::job_process_dev_fee_payment()` includes enhanced logging:
```rust
/// Process unpaid development fees for successful orders
/// Called every 60 seconds by scheduler
async fn job_process_dev_fee_payment(ctx: AppContext) {
let interval = 60u64; // Every 60 seconds
tokio::spawn(async move {
let pool = ctx.pool(); // Get pool from AppContext
loop {
info!("Checking for unpaid development fees");
// Query unpaid orders using find_unpaid_dev_fees()
// Query: WHERE (status = 'settled-hold-invoice' OR status = 'success')
// AND dev_fee > 0 AND dev_fee_paid = 0
if let Ok(unpaid_orders) = find_unpaid_dev_fees(&pool).await {
info!("Found {} orders with unpaid dev fees", unpaid_orders.len());
for mut order in unpaid_orders {
// Attempt payment with 50-second timeout (under 60s cycle)
match tokio::time::timeout(
std::time::Duration::from_secs(50),
send_dev_fee_payment(&order),
)
.await
{
Ok(Ok(payment_hash)) => {
// SUCCESS: Update both fields atomically
let order_id = order.id;
let dev_fee_amount = order.dev_fee;
// Enhanced logging - BEFORE state
info!(
"BEFORE UPDATE: order_id={}, dev_fee_paid={}, dev_fee_payment_hash={:?}",
order_id, order.dev_fee_paid, order.dev_fee_payment_hash
);
order.dev_fee_paid = true;
order.dev_fee_payment_hash = Some(payment_hash.clone());
// Enhanced logging - AFTER modification
info!(
"AFTER MODIFY: order_id={}, dev_fee_paid={}, dev_fee_payment_hash={:?}",
order_id, order.dev_fee_paid, order.dev_fee_payment_hash
);
match order.update(&pool).await {
Err(e) => {
error!(
"❌ DATABASE UPDATE FAILED for order {}: {:?}",
order_id, e
);
}
Ok(_) => {
info!("✅ DATABASE UPDATE SUCCEEDED for order {}", order_id);
// Verification query - confirm database persistence
if let Ok(verified_order) = sqlx::query_as::<_, Order>(
"SELECT * FROM orders WHERE id = ?",
)
.bind(order_id)
.fetch_one(&*pool)
.await
{
info!(
"VERIFICATION: order_id={}, dev_fee_paid={}, dev_fee_payment_hash={:?}",
verified_order.id,
verified_order.dev_fee_paid,
verified_order.dev_fee_payment_hash
);
}
info!(
"Dev fee payment succeeded for order {} - amount: {} sats, hash: {}",
order_id, dev_fee_amount, payment_hash
);
}
}
}
Ok(Err(e)) => {
// FAILURE: Leave fields unchanged for retry
error!(
"Dev fee payment failed for order {} ({} sats) - error: {:?}, will retry",
order.id, order.dev_fee, e
);
}
Err(_) => {
// TIMEOUT: Leave fields unchanged for retry
error!(
"Dev fee payment timeout (50s) for order {} ({} sats) - will retry",
order.id, order.dev_fee
);
}
}
}
}
tokio::time::sleep(tokio::time::Duration::from_secs(interval)).await;
}
});
}
```
**Key Implementation Points:**
1. **Atomic Updates:** Always update `dev_fee_paid` and `dev_fee_payment_hash` together in a single database transaction
2. **Only Update on Success:** Never update these fields on payment failure - leave them unchanged for retry
3. **Dual Status Query:** Query checks BOTH `'settled-hold-invoice'` AND `'success'` statuses to handle race conditions
4. **Enhanced Logging:** BEFORE/AFTER/VERIFY pattern provides complete diagnostic trail
5. **Database Verification:** After update, re-query database to confirm fields were persisted correctly
6. **Automatic Retry:** Failed payments remain with `dev_fee_paid = 0`, causing them to be retried on the next cycle (60s)
7. **Non-Blocking:** Order completion is never blocked by dev fee payment attempts
8. **Error Categorization:** Separate handling for payment failures vs timeouts with specific error messages
### Error Handling
**Payment Failures:**
- LNURL resolution failure (timeout: 15 seconds)
- LND send_payment hanging (timeout: 5 seconds)
- LND connection error
- Payment routing failure
- Payment result timeout (25 seconds)
- Scheduler timeout (50 seconds total)
**Response:** All errors logged but order completes successfully. Failed payments are automatically retried on next scheduler cycle (60 seconds).
**Common Error Scenarios:**
1. **Self-Payment Attempts:** When dev fee destination uses same Lightning node as Mostro, LND may hang trying to route payment. The 5-second timeout on `send_payment()` prevents indefinite blocking.
2. **LNURL Resolution Failures:** Network issues or DNS problems resolving `DEV_FEE_LIGHTNING_ADDRESS`. 15-second timeout ensures fast failure.
3. **Routing Failures:** No route found to destination or insufficient liquidity. Payment fails after attempting routing for up to 25 seconds.
## Database Schema
### Migration: `migrations/20251126120000_dev_fee.sql`
```sql
ALTER TABLE orders ADD COLUMN dev_fee INTEGER DEFAULT 0;
ALTER TABLE orders ADD COLUMN dev_fee_paid INTEGER NOT NULL DEFAULT 0;
ALTER TABLE orders ADD COLUMN dev_fee_payment_hash CHAR(64);
```
### Field Descriptions
| `dev_fee` | INTEGER | 0 | Development fee amount in satoshis |
| `dev_fee_paid` | INTEGER | 0 | Boolean: 0 = failed/not paid, 1 = paid |
| `dev_fee_payment_hash` | CHAR(64) | NULL | Lightning payment hash for reconciliation |
### Backward Compatibility
- Existing orders: dev_fee = 0, dev_fee_paid = 0
- No migration required for existing data
- Daemon handles NULL/zero values gracefully
## Implementation Roadmap
This section provides a checklist for implementing the remaining phases of the development fee feature.
### Phase 2: Fee Calculation ✅ COMPLETE
**Prerequisites:** Phase 1 complete ✅
**Implementation Tasks:**
- [x] Implement `calculate_dev_fee()` pure function in `src/util.rs`
- Input: `total_mostro_fee: i64, percentage: f64`
- Output: `i64` (rounded dev fee amount)
- Logic: `(total_mostro_fee as f64) * percentage`, rounded
- [x] Implement `get_dev_fee()` wrapper function in `src/util.rs`
- Input: `total_mostro_fee: i64`
- Output: `i64` (calls calculate_dev_fee with Settings percentage)
- [x] Implement dev_fee calculation (for tracking mostrod's donation amount)
- Formula: `dev_fee = round(total_mostro_fee × dev_fee_percentage)`
- Simple rounding for whole satoshi amounts
- [x] Update message creation in `src/flow.rs::hold_invoice_paid()`
- Seller amount: `order.amount + order.fee`
- Buyer amount: `order.amount - order.fee`
- [x] Update message creation in `src/app/add_invoice.rs::add_invoice_action()`
- No dev_fee in user-facing amounts
- [x] Update payment calculation in `src/app/release.rs::check_failure_retries()`
- [x] Update Lightning payment in `src/app/release.rs::do_payment()` ⚠️ CRITICAL
- Dev fee paid separately by mostrod
- [x] Add unit tests for `calculate_dev_fee()` in `src/util.rs::tests`
- Test `test_get_dev_fee_basic`: Standard calculation (1000 @ 30% = 300)
- Test `test_get_dev_fee_rounding`: Rounding (333 @ 30% = 100)
- Test `test_get_dev_fee_zero`: Zero fee (0 → 0)
- Test `test_get_dev_fee_tiny_amounts`: Tiny amounts (1 @ 30% = 0)
- All tests passing ✓
- [x] Integration testing with various order amounts
- Verified correct amounts in all message flows
- Verified correct Lightning payment amounts
- Verified users only pay Mostro fee (not dev_fee)
**Deliverables:** ✅ All fee calculations implemented, tested, and integrated across entire order flow
### Phase 3: Payment Execution ✅ COMPLETE
**Prerequisites:** Phase 2 complete ✅
**Completed Implementation (Commits: f508669, 102cfed, eaf3319, 42253f1, 2655943):**
- [x] Implement `send_dev_fee_payment()` in `src/app/release.rs`
- Step 0: Dev fee amount validation (`dev_fee > 0` check)
- Step 1: LNURL resolution with 15-second timeout
- Call: `resolv_ln_address(DEV_FEE_LIGHTNING_ADDRESS, amount)`
- Error handling: Log and return error on timeout/failure
- Step 2: Create LND connector
- Call: `LndConnector::new().await`
- Step 3: Send payment with 5-second timeout
- Call: `ln_client.send_payment(&payment_request, amount, tx)`
- Error handling: Timeout prevents hanging on self-payments
- Step 4: Wait for payment result with 25-second timeout
- Call: Loop receiving messages until terminal status
- Success: Return payment hash
- Failure: Return error with details
- [x] Create scheduler job `job_process_dev_fee_payment()` in `src/scheduler.rs`
- Uses `find_unpaid_dev_fees()` database function (`src/db.rs`)
- Query: `SELECT * FROM orders WHERE (status = 'settled-hold-invoice' OR status = 'success') AND dev_fee > 0 AND dev_fee_paid = 0`
- **Key improvement:** Query checks BOTH statuses to handle race conditions (commit 102cfed)
- For each unpaid order:
- Call `send_dev_fee_payment()` with 50-second timeout
- On success: Update `dev_fee_paid = 1`, `dev_fee_payment_hash = hash`
- On failure: Log error, leave `dev_fee_paid = 0` for retry
- Schedule: Run every 60 seconds
- [x] Enhanced logging (commit 2655943)
- Logs BEFORE/AFTER state for dev_fee_paid and dev_fee_payment_hash
- Database update success/failure logging
- Verification queries to confirm database persistence
- Info: Payment initiation, success with order_id, amount, hash
- Error: Resolution failures, payment failures, timeouts with details
- [x] Error handling and retry logic
- Dev fee validation before payment attempt (commit eaf3319)
- Self-payment detection (5s timeout prevents hanging)
- LNURL resolution failures (15s timeout)
- Routing failures (25s timeout)
- Scheduler timeout (50s total)
- All errors: Log and allow automatic retry on next cycle (60s)
- [x] Market price order dev_fee calculation (commit 2655943)
- Implemented in `take_buy.rs` and `take_sell.rs`
- Calculates `dev_fee = get_dev_fee(fee * 2)` when order amount is determined
- Fixes bug where market price orders had `dev_fee = 0` permanently
- [x] Integration testing
- Tested successful dev fee payment flow
- Tested LNURL resolution failure handling
- Tested payment timeout scenarios
- Tested scheduler retry mechanism
- Verified order completes regardless of dev fee payment status
- Fixed edge cases through commits 42253f1, 102cfed, 2655943
**Deliverables:** ✅ Automated dev fee payment system fully operational with scheduler-based processing, enhanced logging, race condition handling, and automatic retry mechanism
### Phase 4: Audit Events via Nostr ✅ COMPLETE
**Purpose:** Provide transparent, verifiable audit trail of all dev fee payments through Nostr relays.
**What Was Implemented:**
- Custom Nostr event kind (8383) for dev fee payment audits
- Event publishing in scheduler after successful payment
- Complete payment details: amount, hash, order reference, timestamp
- Public relay distribution for third-party verification
- Queryable tags for analytics and reporting
**Event Specification:**
| Event Kind | 8383 (Regular Event) |
| Replaceability | No - complete audit trail |
| Published After | Successful dev fee payment & DB update |
| Content Format | JSON with structured payment data |
| Tags | `y`, `z`, `order`, `amount`, `hash`, `t`, `currency`, `network` |
**Event Kind Rationale:**
Why kind 8383 (Regular Event)?
- ✅ **Complete History:** Every payment is a separate, permanent event
- ✅ **Third-Party Auditing:** Anyone can query all historical payments
- ✅ **Total Calculation:** Sum all `amount` tags to get total dev fund contributions
- ✅ **Immutable Record:** Events cannot be replaced or deleted
- ✅ **Standard Compliance:** Follows NIP-01 application-specific event range (1000-9999)
**Event Structure Example:**
```json
{
"kind": 8383,
"content": {
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"dev_fee_sats": 100,
"payment_hash": "abc123...",
"payment_timestamp": 1234567890,
"destination": "dev@getalby.com",
"order_amount_sats": 10000,
"order_fiat_amount": 50,
"order_fiat_code": "USD",
"status": "success"
},
"tags": [
["y", "mostro"],
["z", "dev-fee-payment"],
["order", "550e8400-e29b-..."],
["amount", "100"],
["hash", "abc123..."],
["t", "audit"],
["t", "dev-fund"],
["currency", "USD"],
["network", "mainnet"]
]
}
```
**Query Examples:**
```javascript
// Get all dev fee payments
const filter = {
kinds: [8383],
"#y": ["mostro"],
"#z": ["dev-fee-payment"]
};
// Calculate total dev fund contributions
let total = 0;
events.forEach(event => {
const amountTag = event.tags.find(t => t[0] === "amount");
if (amountTag) total += parseInt(amountTag[1]);
});
// Filter by currency
const usdPayments = {
kinds: [8383],
"#currency": ["USD"]
};
// Find payments for specific order
const orderPayments = {
kinds: [8383],
"#order": ["550e8400-e29b-41d4-a716-446655440000"]
};
```
**Implementation Details:**
**Location:** `src/scheduler.rs::job_process_dev_fee_payment()` (after payment success)
**Function:** `publish_dev_fee_audit_event(order: &Order, payment_hash: &str)`
**Error Handling:** Audit event failures are logged but don't fail the payment transaction
**Retry Logic:** None - if event publish fails, payment still succeeds (prioritize financial reliability)
**Privacy Considerations:**
- Order ID included for transparency
- Buyer/seller pubkeys NOT included (privacy)
- Only aggregate payment data published
**Benefits:**
1. **Transparency:** Anyone can verify dev fund contributions
2. **Accountability:** Public record of all fee payments
3. **Analytics:** Query by currency, date range, network
4. **Trust:** Third-party auditing without Mostro access
5. **Compliance:** Verifiable fee collection for reporting
**Testing:**
```bash
# Query dev fee events from relay
nostr-cli -k 8383 --tag y=mostro --tag z=dev-fee-payment
# Calculate total contributions
**Status:** ✅ Complete - Dev fee audit events are now published to Nostr relays
**Implementation:**
1. ✅ Added `DEV_FEE_AUDIT_EVENT_KIND` constant to `src/config/constants.rs`
2. ✅ Created `publish_dev_fee_audit_event()` function in `src/util.rs`
3. ✅ Integrated event publishing in `src/scheduler.rs` after successful payment
4. ✅ Non-blocking implementation - event failures don't affect payment
5. ✅ Events are queryable via standard Nostr clients and relays
**Future Enhancements:**
- Aggregate statistics event (kind 38100) updated monthly with totals
- Dashboard for visualizing dev fund contributions
- NIP-05 verification for Mostro's audit event pubkey
## Monitoring and Operations
### Statistics Queries
**Unpaid Development Fees:**
```sql
-- Same query used by find_unpaid_dev_fees() in src/db.rs:895-908
SELECT id, dev_fee, created_at, status
FROM orders
WHERE (status = 'settled-hold-invoice' OR status = 'success')
AND dev_fee > 0
AND dev_fee_paid = 0
ORDER BY created_at DESC;
-- IMPORTANT: Query checks BOTH statuses (settled-hold-invoice AND success)
-- Reason: Handles race condition where buyer payment completes while dev fee payment is processing
-- Without both statuses: Orders could get stuck with unpaid dev fees if buyer pays before dev fee completes
-- Result: Ensures all dev fees are eventually collected regardless of payment timing
```
**Development Fee Summary:**
```sql
SELECT
COUNT(*) as total_orders,
SUM(dev_fee) as total_dev_fees_sats,
SUM(CASE WHEN dev_fee_paid = 1 THEN dev_fee ELSE 0 END) as paid_sats,
SUM(CASE WHEN dev_fee_paid = 0 THEN dev_fee ELSE 0 END) as unpaid_sats,
ROUND(100.0 * SUM(CASE WHEN dev_fee_paid = 1 THEN 1 ELSE 0 END) / COUNT(*), 2) as success_rate
FROM orders
WHERE status = 'success' AND dev_fee > 0;
```
**Recent Failures:**
```sql
-- Orders with unpaid dev fees older than 5 minutes (potential issues):
-- IMPORTANT: Uses dual-status query like find_unpaid_dev_fees()
SELECT id, dev_fee, dev_fee_paid, status, failed_payment, payment_attempts, created_at
FROM orders
WHERE (status = 'settled-hold-invoice' OR status = 'success')
AND dev_fee > 0
AND dev_fee_paid = 0
AND created_at < strftime('%s', 'now', '-5 minutes')
ORDER BY created_at DESC;
-- Orders with unpaid dev fees in last 24 hours:
-- Useful for monitoring recent payment failures
SELECT id, dev_fee, created_at, status, dev_fee_paid
FROM orders
WHERE (status = 'settled-hold-invoice' OR status = 'success')
AND dev_fee > 0
AND dev_fee_paid = 0
AND created_at > strftime('%s', 'now', '-24 hours')
ORDER BY created_at DESC;
-- Check for orders stuck in settled-hold-invoice with unpaid dev fees:
-- These should be picked up by scheduler every 60 seconds
SELECT id, dev_fee, status, dev_fee_paid, created_at,
(strftime('%s', 'now') - created_at) / 60 as minutes_since_creation
FROM orders
WHERE status = 'settled-hold-invoice'
AND dev_fee > 0
AND dev_fee_paid = 0
ORDER BY created_at DESC;
```
### Log Filtering
**View all dev fee logs:**
```bash
RUST_LOG="dev_fee=debug" mostrod
```
**View only errors:**
```bash
RUST_LOG="dev_fee=error" mostrod
```
**Log Examples:**
Success:
```
[INFO dev_fee] order_id=550e8400-e29b-41d4-a716-446655440000 amount_sats=300 destination=<dev@lightning.address> Initiating development fee payment
[INFO dev_fee] order_id=550e8400-e29b-41d4-a716-446655440000 payment_hash=abcd1234... Development fee payment succeeded
```
Failure:
```
[ERROR dev_fee] order_id=550e8400-e29b-41d4-a716-446655440000 error=LnAddressParseError stage=address_resolution Failed to resolve development Lightning Address
[ERROR dev_fee] order_id=550e8400-e29b-41d4-a716-446655440000 dev_fee=300 Development fee payment failed - order completing anyway
```
## Troubleshooting
### Common Issues
**1. Daemon Won't Start - Invalid Configuration**
```
Error: Configuration error: dev_fee_percentage (0.05) is below minimum (0.10)
```
**Solution:** Set `dev_fee_percentage` to at least 0.10 in settings.toml
**2. High Failure Rate**
- Check Lightning node connectivity
- Verify `<dev@lightning.address>` is reachable
- Check routing capacity to destination
- Review error logs: `RUST_LOG="dev_fee=error" mostrod`
**3. Payment Timeouts**
- LNURL resolution timeout: 15 seconds (indicates DNS/network issues)
- send_payment timeout: 5 seconds (indicates LND hanging, often self-payment attempts)
- Payment result timeout: 25 seconds (indicates routing issues or network congestion)
- Total scheduler timeout: 50 seconds
- Orders still complete successfully regardless of dev fee payment failures
- Failed payments automatically retry every 60 seconds via scheduler
**4. Market Price Orders With Zero Dev Fee**
**Symptom:**
```sql
-- Orders with fee but no dev_fee (indicates bug in market price flow)
SELECT id, amount, fee, dev_fee, price_from_api, status
FROM orders
WHERE fee > 0
AND dev_fee = 0
AND price_from_api = 1;
```
**Cause:** Market price order was taken but `dev_fee` was not calculated
**Impact:** Dev fee not collected from these orders (revenue loss)
**Fix:** Ensure both `take_buy.rs` and `take_sell.rs` calculate `dev_fee` when updating `amount` and `fee` for market price orders (see Market Price Order Dev Fee Calculation section)
**Verification:**
```sql
-- All market price orders should have consistent fees
SELECT
COUNT(*) as total_market_orders,
SUM(CASE WHEN fee > 0 AND dev_fee = 0 THEN 1 ELSE 0 END) as broken_orders,
SUM(CASE WHEN fee > 0 AND dev_fee > 0 THEN 1 ELSE 0 END) as correct_orders
FROM orders
WHERE price_from_api = 1
AND amount > 0; -- Only count taken orders
```
**5. Market Price Orders with Stale dev_fee After Timeout** ✅ FIXED
**Status:** This bug was fixed in commit `c803471`. The `update_order_to_initial_state()`
function now properly persists `dev_fee = 0` to the database.
**Historical Issue (Pre-Fix):**
The function set `dev_fee = 0` in memory but didn't include it in the SQL UPDATE statement,
causing stale values to remain in the database. When `edit_pubkeys_order()` fetched the
order from the database, it would return the old dev_fee value.
**Symptom (Before Fix):**
```sql
-- Orders that timed out but dev_fee wasn't reset in database
SELECT id, amount, fee, dev_fee, price_from_api, status
FROM orders
WHERE status = 'pending'
AND price_from_api = 1
AND amount = 0
AND fee = 0
AND dev_fee != 0; -- BUG: Should be 0
```
**Cause:** `update_order_to_initial_state()` didn't persist dev_fee to database
**Impact:** Next taker would be charged incorrect dev_fee from previous attempt
**Fix Applied:** Added `dev_fee` parameter to `update_order_to_initial_state()` and
included it in the SQL UPDATE statement:
- `src/db.rs`: Function signature and SQL UPDATE modified
- `src/app/cancel.rs`: Explicit cancellation handler
**Prevention:** Both paths now include `order.dev_fee = 0` for market price orders. See "Taker Abandonment and Order Reset" section for details.
**Verification:**
```sql
-- All pending market price orders should have dev_fee = 0
SELECT COUNT(*) as stale_dev_fee_orders
FROM orders
WHERE status = 'pending'
AND price_from_api = 1
AND amount = 0
AND fee = 0
AND dev_fee != 0;
-- Should return 0 if fix is working correctly
```
### Manual Retry Procedure
For orders with unpaid dev fees:
1. Identify unpaid fees:
```sql
SELECT id, dev_fee FROM orders WHERE dev_fee_paid = 0 AND dev_fee > 0;
```
2. Use Lightning CLI to manually pay:
```bash
lncli payinvoice <invoice_from_lnurl>
```
3. Update database:
```sql
UPDATE orders
SET dev_fee_paid = 1, dev_fee_payment_hash = '<payment_hash>'
WHERE id = '<order_id>';
```
## Security Considerations
### Hardcoded Values
- Lightning Address: `<dev@lightning.address>` (cannot be changed without recompiling)
- Minimum fee: 10% (enforced at startup)
- Prevents misconfiguration or malicious changes
### Payment Isolation
- Dev fee payment errors don't affect core order functionality
- Failed payments logged for audit but don't halt operations
- Ensures platform reliability while maintaining transparency
### Audit Trail
- All fees recorded in database
- Payment hashes enable verification
- Logs provide forensic evidence
- Operators can reconcile payments independently
## Performance Impact
### Latency
- LNURL resolution: ~1-3 seconds (15s timeout)
- LND send_payment call: ~100-500ms (5s timeout)
- Payment execution: ~2-5 seconds (25s timeout)
- Total payment time: ~3-8 seconds typical, 45s maximum
- Scheduler processing interval: 60 seconds
- **Total order delay:** None (payment runs asynchronously via scheduler after buyer receives sats)
### Resource Usage
- Minimal CPU overhead (single calculation per order)
- Negligible memory impact
- Database: 3 additional columns per order (~76 bytes)
## Testing Specification
### Unit Tests
**Status:** ✅ IMPLEMENTED
**Location:** `src/util.rs::tests` module
**Tests Implemented:**
1. **`test_get_dev_fee_basic`**
- Purpose: Standard percentage calculation
- Test: 1,000 sats @ 30% = 300 sats
- Status: ✓ Passing
2. **`test_get_dev_fee_rounding`**
- Purpose: Rounding behavior
- Test: 333 sats @ 30% = 99.9 → rounds to 100 sats
- Status: ✓ Passing
3. **`test_get_dev_fee_zero`**
- Purpose: Zero fee handling
- Test: 0 sats @ 30% = 0 sats
- Status: ✓ Passing
4. **`test_get_dev_fee_tiny_amounts`**
- Purpose: Small amount edge cases
- Test: 1 sat @ 30% = 0.3 → rounds to 0 sats
- Status: ✓ Passing
**All tests use `calculate_dev_fee()` directly with explicit percentage (0.30) to avoid dependency on global Settings.**
**Run tests:**
```bash
cargo test test_get_dev_fee
# Output: test result: ok. 4 passed; 0 failed
```
**Test Coverage:**
- ✅ Standard calculations
- ✅ Rounding behavior (both up and down)
- ✅ Zero fee edge case
- ✅ Tiny amounts (rounds to zero)
- ✅ All tests passing in CI
### Integration Testing
**Manual Test Checklist:**
1. **Configuration Validation:**
- Set `dev_fee_percentage = 0.05` → Daemon refuses to start ✓
- Set `dev_fee_percentage = 1.5` → Daemon refuses to start ✓
- Set `dev_fee_percentage = 0.30` → Daemon starts ✓
2. **Fee Calculation:**
- Create 100,000 sat order with 1% Mostro fee
- Verify seller hold invoice: 100,500 sats (100k + 500)
- Verify buyer receives: 99,500 sats (100k - 500)
- Verify total dev fee: 300 sats (paid by mostrod from its earnings)
3. **Payment Flow:**
- Complete order successfully
- Check database: `dev_fee_paid = 1`, `dev_fee_payment_hash != NULL`
- Verify logs show successful payment
4. **Error Handling:**
- Simulate payment failure (disconnect Lightning node)
- Verify order still completes with `status = 'success'`
- Check `dev_fee_paid = 0` in database
## Migration Guide
### For Existing Installations
1. **Backup database:**
```bash
cp ~/.mostro/mostro.db ~/.mostro/mostro.db.backup
```
2. **Update Mostro:**
```bash
git pull origin main
cargo build --release
```
3. **Update settings.toml:**
```toml
[mostro]
dev_fee_percentage = 0.30 # Add this line
```
4. **Restart daemon:**
```bash
mostrod
```
5. **Verify:**
```bash
grep "Settings correctly loaded" mostrod.log
sqlite3 ~/.mostro/mostro.db "PRAGMA table_info(orders);" | grep dev_fee
```
### Rollback Procedure
If issues arise:
1. Stop daemon
2. Restore backup: `cp ~/.mostro/mostro.db.backup ~/.mostro/mostro.db`
3. Checkout previous version: `git checkout <previous_commit>`
4. Rebuild: `cargo build --release`
5. Restart daemon