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)
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
Implemented Components:
calculate_dev_fee()pure function insrc/util.rsget_dev_fee()wrapper function insrc/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
Implemented Components:
send_dev_fee_payment()function insrc/app/release.rs- Scheduler job
process_dev_fee_payment()insrc/scheduler.rs - Database query function
find_unpaid_dev_fees()insrc/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 schedulerdev_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
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:
-
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
-
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
-
Enhanced Logging:
- BEFORE/AFTER/VERIFY pattern provides complete audit trail
- Essential for debugging database persistence issues
- Helps identify race conditions and timing problems
-
Validation Before Payment:
- Fail-fast approach for invalid amounts
- Reduces unnecessary network calls
- Clearer error messages for debugging
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
-
Configuration Layer (
src/config/)constants.rs: Hardcoded constraints (10-100%, Lightning Address)types.rs: MostroSettings with dev_fee_percentageutil.rs: Startup validation
-
Fee Calculation (
src/util.rs)get_dev_fee(): Computes percentage of Mostro feeprepare_new_order(): Calculates fees during order creationshow_hold_invoice(): Includes dev fee in seller's hold invoice
-
Payment Execution (
src/app/release.rs)send_dev_fee_payment(): LNURL resolution and paymentpayment_success(): Integration point after buyer payment
-
Database Schema (
migrations/20251126120000_dev_fee.sql)dev_fee: Amount in satoshisdev_fee_paid: Boolean (0/1)dev_fee_payment_hash: Payment hash for reconciliation
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>";[mostro]
# Development sustainability fee
# Percentage of Mostro fee sent to development fund (minimum 10%)
dev_fee_percentage = 0.30Validation Rules:
- Must be between 0.10 (10%) and 1.0 (100%)
- Validated on daemon startup
- Invalid values cause startup failure with error message
dev_fee_percentage: 0.30 (30%)- Configured in
src/config/types.rs::MostroSettings::default()
Current State: ✅ IMPLEMENTED - Two functions provide fee calculation with proper satoshi handling.
Implementation:
Two-function approach in src/util.rs:
- Pure calculation function:
/// Pure function for calculating dev fee - useful for testing
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
}- Settings wrapper:
/// Wrapper that uses configured dev_fee_percentage from Settings
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 configget_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 ✓
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:
- Calculate Mostro fee:
fee = get_fee(amount)(for fixed price orders where amount > 0) - Set
dev_fee = 0for ALL orders (both fixed price and market price) - Store in Order struct with
dev_fee_paid = falseanddev_fee_payment_hash = None
Dev Fee Calculation at Take Time:
- ALL orders (both fixed price and market price) have
dev_fee = 0at creation - Dev fee is calculated when order is taken (see "Dev Fee Calculation at Take Time" section)
- Implemented in
take_buy.rsandtake_sell.rs - Formula:
dev_fee = get_dev_fee(fee * 2)where fee is calculated based on the order amount
Critical Behavior for Market Price Orders:
When a user takes a market price order, the following sequence occurs:
-
Order Taken (status changes from
pendingtowaiting-buyer-invoiceorwaiting-payment):- Dev fee is calculated based on current market price
order.dev_feefield is updated with the calculated amount- Order waits for taker to provide invoice (sell order) or make payment (buy order)
-
Taker Abandons Order (doesn't provide invoice or make payment):
- Order status is reset back to
pending - CRITICAL:
order.dev_feefield MUST be reset to0 - This prevents incorrect dev fee charges if order is re-taken at different price
- Order status is reset back to
-
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.):
// 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:
-- 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;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):
// 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:
- Unified Behavior: Both fixed price and market price orders calculate
dev_feeat the same time (when taken) - Consistency: All pending orders have
dev_fee = 0, all taken orders havedev_fee > 0 - Simplicity: Single point of calculation makes the code easier to understand and maintain
- 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 ✓
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:
-
Explicit Cancellation (
src/app/cancel.rs::reset_api_quotes()):- Taker explicitly calls cancel action
- Resets:
amount = 0,fee = 0,dev_fee = 0 - Status: ✅ Implemented
-
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
- Scheduler detects taker hasn't proceeded within
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_feevalue 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()functionsrc/scheduler.rs: Automatic timeout handler injob_cancel_orders()- Both paths use same logic:
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_secondsin settings - Default: Orders return to pending after taker hasn't proceeded for configured time
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:
// 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_feestores 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_feefrom its earnings after collecting the Mostro fee
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:
seller_order.amount = order.amount.saturating_add(order.fee);Buyer Messages:
buyer_order.amount = order.amount.saturating_sub(order.fee);Critical Implementation Locations:
-
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
-
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
-
src/app/release.rs::check_failure_retries()- Purpose: Payment failure handling
- Buyer amount:
order.amount - order.fee(no dev_fee) - Impact: Failure notification amounts
-
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:
// No dev_fee split needed - users only pay Mostro fee
seller_amount = order.amount + order.fee;
buyer_amount = order.amount - order.fee;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)
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, thendev_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
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:
-
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
-
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():
// [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:
Ok(hash) => {
order.dev_fee_paid = true;
order.dev_fee_payment_hash = Some(hash);
// Database updated, won't be retried
}Failure Response:
Err(e) => {
order.dev_fee_paid = false;
// Logged for audit
// Will be retried on next scheduler cycle (60 seconds)
}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.
When Initialized: During order creation in src/util.rs::prepare_new_order()
Initialization:
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).
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:
- Seller releases order (
status = 'settled-hold-invoice') - Scheduler runs every 60 seconds
- Query identifies unpaid orders:
SELECT * FROM orders WHERE (status = 'settled-hold-invoice' OR status = 'success') AND dev_fee > 0 AND dev_fee_paid = 0 - Scheduler calls
send_dev_fee_payment()for each unpaid order - On payment success, scheduler updates:
order.dev_fee_paid = true(stored as1in database) - Important: Query includes BOTH statuses to handle race conditions where buyer payment completes during dev fee payment failure
Database Update:
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)
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:
// 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_hashFormat: 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)
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
The actual implementation in src/scheduler.rs::job_process_dev_fee_payment() includes enhanced logging:
/// 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:
- Atomic Updates: Always update
dev_fee_paidanddev_fee_payment_hashtogether in a single database transaction - Only Update on Success: Never update these fields on payment failure - leave them unchanged for retry
- Dual Status Query: Query checks BOTH
'settled-hold-invoice'AND'success'statuses to handle race conditions - Enhanced Logging: BEFORE/AFTER/VERIFY pattern provides complete diagnostic trail
- Database Verification: After update, re-query database to confirm fields were persisted correctly
- Automatic Retry: Failed payments remain with
dev_fee_paid = 0, causing them to be retried on the next cycle (60s) - Non-Blocking: Order completion is never blocked by dev fee payment attempts
- Error Categorization: Separate handling for payment failures vs timeouts with specific error messages
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:
-
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. -
LNURL Resolution Failures: Network issues or DNS problems resolving
DEV_FEE_LIGHTNING_ADDRESS. 15-second timeout ensures fast failure. -
Routing Failures: No route found to destination or insufficient liquidity. Payment fails after attempting routing for up to 25 seconds.
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);| Column | Type | Default | Description |
|---|---|---|---|
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 |
- Existing orders: dev_fee = 0, dev_fee_paid = 0
- No migration required for existing data
- Daemon handles NULL/zero values gracefully
This section provides a checklist for implementing the remaining phases of the development fee feature.
Prerequisites: Phase 1 complete ✅
Implementation Tasks:
- Implement
calculate_dev_fee()pure function insrc/util.rs- Input:
total_mostro_fee: i64, percentage: f64 - Output:
i64(rounded dev fee amount) - Logic:
(total_mostro_fee as f64) * percentage, rounded
- Input:
- Implement
get_dev_fee()wrapper function insrc/util.rs- Input:
total_mostro_fee: i64 - Output:
i64(calls calculate_dev_fee with Settings percentage)
- Input:
- 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
- Formula:
- Update message creation in
src/flow.rs::hold_invoice_paid()- Seller amount:
order.amount + order.fee - Buyer amount:
order.amount - order.fee
- Seller amount:
- Update message creation in
src/app/add_invoice.rs::add_invoice_action()- No dev_fee in user-facing amounts
- Update payment calculation in
src/app/release.rs::check_failure_retries() - Update Lightning payment in
src/app/release.rs::do_payment()⚠️ CRITICAL- Dev fee paid separately by mostrod
- Add unit tests for
calculate_dev_fee()insrc/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 ✓
- Test
- 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
Prerequisites: Phase 2 complete ✅
Completed Implementation (Commits: f508669, 102cfed, eaf3319, 42253f1, 2655943):
- Implement
send_dev_fee_payment()insrc/app/release.rs- Step 0: Dev fee amount validation (
dev_fee > 0check) - 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
- Call:
- Step 2: Create LND connector
- Call:
LndConnector::new().await
- Call:
- 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
- Call:
- 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
- Step 0: Dev fee amount validation (
- Create scheduler job
job_process_dev_fee_payment()insrc/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 = 0for retry
- Call
- Schedule: Run every 60 seconds
- Uses
- 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
- 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)
- Market price order dev_fee calculation (commit 2655943)
- Implemented in
take_buy.rsandtake_sell.rs - Calculates
dev_fee = get_dev_fee(fee * 2)when order amount is determined - Fixes bug where market price orders had
dev_fee = 0permanently
- Implemented in
- 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
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:
| Property | Value |
|---|---|
| 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
amounttags 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:
{
"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:
// 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:
- Transparency: Anyone can verify dev fund contributions
- Accountability: Public record of all fee payments
- Analytics: Query by currency, date range, network
- Trust: Third-party auditing without Mostro access
- Compliance: Verifiable fee collection for reporting
Testing:
# Query dev fee events from relay
nostr-cli -k 8383 --tag y=mostro --tag z=dev-fee-payment
# Calculate total contributions
nostr-cli -k 8383 --tag y=mostro | jq '[.[] | .tags[] | select(.[0]=="amount") | .[1] | tonumber] | add'Status: ✅ Complete - Dev fee audit events are now published to Nostr relays
Implementation:
- ✅ Added
DEV_FEE_AUDIT_EVENT_KINDconstant tosrc/config/constants.rs - ✅ Created
publish_dev_fee_audit_event()function insrc/util.rs - ✅ Integrated event publishing in
src/scheduler.rsafter successful payment - ✅ Non-blocking implementation - event failures don't affect payment
- ✅ 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
Unpaid Development Fees:
-- 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 timingDevelopment Fee Summary:
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:
-- 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;View all dev fee logs:
RUST_LOG="dev_fee=debug" mostrodView only errors:
RUST_LOG="dev_fee=error" mostrodLog 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
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:
-- 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:
-- 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 orders5. 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):
-- 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 0Cause: 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 modifiedsrc/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:
-- 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 correctlyFor orders with unpaid dev fees:
- Identify unpaid fees:
SELECT id, dev_fee FROM orders WHERE dev_fee_paid = 0 AND dev_fee > 0;- Use Lightning CLI to manually pay:
lncli payinvoice <invoice_from_lnurl>- Update database:
UPDATE orders
SET dev_fee_paid = 1, dev_fee_payment_hash = '<payment_hash>'
WHERE id = '<order_id>';- Lightning Address:
<dev@lightning.address>(cannot be changed without recompiling) - Minimum fee: 10% (enforced at startup)
- Prevents misconfiguration or malicious changes
- 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
- All fees recorded in database
- Payment hashes enable verification
- Logs provide forensic evidence
- Operators can reconcile payments independently
- 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)
- Minimal CPU overhead (single calculation per order)
- Negligible memory impact
- Database: 3 additional columns per order (~76 bytes)
Status: ✅ IMPLEMENTED
Location: src/util.rs::tests module
Tests Implemented:
-
test_get_dev_fee_basic- Purpose: Standard percentage calculation
- Test: 1,000 sats @ 30% = 300 sats
- Status: ✓ Passing
-
test_get_dev_fee_rounding- Purpose: Rounding behavior
- Test: 333 sats @ 30% = 99.9 → rounds to 100 sats
- Status: ✓ Passing
-
test_get_dev_fee_zero- Purpose: Zero fee handling
- Test: 0 sats @ 30% = 0 sats
- Status: ✓ Passing
-
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:
cargo test test_get_dev_fee
# Output: test result: ok. 4 passed; 0 failedTest Coverage:
- ✅ Standard calculations
- ✅ Rounding behavior (both up and down)
- ✅ Zero fee edge case
- ✅ Tiny amounts (rounds to zero)
- ✅ All tests passing in CI
Manual Test Checklist:
-
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 ✓
- Set
-
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)
-
Payment Flow:
- Complete order successfully
- Check database:
dev_fee_paid = 1,dev_fee_payment_hash != NULL - Verify logs show successful payment
-
Error Handling:
- Simulate payment failure (disconnect Lightning node)
- Verify order still completes with
status = 'success' - Check
dev_fee_paid = 0in database
- Backup database:
cp ~/.mostro/mostro.db ~/.mostro/mostro.db.backup- Update Mostro:
git pull origin main
cargo build --release- Update settings.toml:
[mostro]
dev_fee_percentage = 0.30 # Add this line- Restart daemon:
mostrod- Verify:
# Check settings loaded
grep "Settings correctly loaded" mostrod.log
# Check migration applied
sqlite3 ~/.mostro/mostro.db "PRAGMA table_info(orders);" | grep dev_feeIf issues arise:
- Stop daemon
- Restore backup:
cp ~/.mostro/mostro.db.backup ~/.mostro/mostro.db - Checkout previous version:
git checkout <previous_commit> - Rebuild:
cargo build --release - Restart daemon