Skip to content

Simplify API surface: Learn from phoenixd's minimalist design #11

@okjodom

Description

@okjodom

Problem Statement

The current fmcd API exposes Fedimint's internal module structure (mint, lightning, onchain) through nested, module-based routing (/v2/ln/invoice, /v2/mint/spend, etc.). This requires users to understand Fedimint's architecture to use the API effectively.

In contrast, phoenixd (ACINQ's Lightning server) demonstrates that a minimalist, action-oriented API focusing on user intentions (pay, receive money) drives adoption and reduces support burden.

Proposed Solution

Redesign fmcd's API to adopt phoenixd's simplicity while maintaining Fedimint-specific capabilities:

  1. Reduce from 30+ to ~15 core endpoints
  2. Implement universal /pay endpoint that handles all payment types transparently
  3. Remove versioning and module nesting from paths
  4. Add two-tier authentication (read-only vs payment operations)
  5. Standardize responses with user-friendly naming

Design Document

FMCD API Redesign: Learning from Phoenixd

Executive Summary

This document analyzes the API design differences between phoenixd (ACINQ's Lightning server) and fmcd (Fedimint client daemon), proposing a new API design that combines phoenixd's simplicity with fmcd's Fedimint-specific requirements. The key finding is that phoenixd succeeds through its minimalist, action-oriented API design that focuses on core payment operations without exposing internal complexity. FMCD can adopt this approach while maintaining its multi-federation capabilities.

Key Recommendations

  1. Simplify endpoint structure - Move from nested module-based routing to flat, action-oriented endpoints
  2. Unify payment operations - Single /pay endpoint that handles all payment types transparently
  3. Streamline authentication - Adopt phoenixd's simple password-based auth with clear access levels
  4. Improve response consistency - Standardize response formats across all endpoints
  5. Hide complexity - Abstract away Fedimint internals from the public API

Detailed API Comparison

1. Overall API Philosophy

Aspect Phoenixd FMCD (Current) Analysis
Endpoint Structure Flat, action-based (/payinvoice, /createinvoice) Nested, module-based (/v2/ln/invoice, /v2/mint/spend) Phoenixd's flat structure is more intuitive
API Versioning No versioning in URLs /v2/ prefix everywhere Phoenixd keeps it simple, versions via headers if needed
Operation Focus User actions (pay, receive, check balance) Technical modules (mint, ln, onchain) Phoenixd focuses on what users want to do
Complexity Exposure Hides Lightning complexity Exposes Fedimint modules Phoenixd provides better abstraction

2. Authentication Comparison

Aspect Phoenixd FMCD (Current)
Method Basic Auth with two levels Basic Auth with single password
Access Levels full-access and limited-access All or nothing
Password Storage In config file In config file
WebSocket Auth Via protocol headers Custom WebSocket auth

Phoenixd's Approach:

// Two distinct access levels
val fullAccessPassword: String
val limitedAccessPassword: String

// Different endpoints require different access
authenticate("full-access") {
    post("payinvoice") { ... }  // Requires full access
}
authenticate {
    get("getinfo") { ... }  // Works with limited access
}

3. Core Payment Operations

Phoenixd's Unified Approach

Phoenixd provides multiple payment endpoints that all follow a similar pattern:

  • /payinvoice - Pay a Lightning invoice
  • /payoffer - Pay a BOLT12 offer
  • /paylnaddress - Pay a Lightning Address
  • /lnurlpay - Pay via LNURL

Key Design Pattern: Each payment type has its own endpoint, but they all return the same response structure.

FMCD's Current Approach

FMCD exposes the underlying module structure:

  • /v2/ln/pay - Lightning payments
  • /v2/mint/spend - Ecash operations
  • /v2/onchain/withdraw - On-chain operations

Problem: Users need to understand Fedimint's internal architecture to use the API.

4. Response Structure Comparison

Phoenixd's Response

{
  "paymentId": "uuid",
  "paymentHash": "hash",
  "preimage": "preimage",
  "recipientAmountSat": 1000,
  "routingFeeSat": 10
}

FMCD's Response

{
  "operation_id": "id",
  "payment_type": "Lightning",
  "contract_id": "contract",
  "fee": 10,
  "preimage": "preimage"
}

Analysis: Phoenixd uses clearer field names and consistent structure. FMCD exposes internal concepts like "contract_id" that users don't need to know about.

5. Error Handling

Aspect Phoenixd FMCD
Error Format Simple text with HTTP status Complex error objects
User Feedback Clear, actionable messages Technical error details
Status Codes Consistent HTTP status mapping Mixed approaches

Proposed New API Design for FMCD

Design Principles

  1. User-Centric: Focus on what users want to accomplish, not how Fedimint works internally
  2. Simple by Default: Hide complexity behind sensible defaults
  3. Progressive Disclosure: Advanced features available but not required
  4. Consistency: Uniform patterns across all endpoints

Proposed Endpoint Structure

# Core Payment Operations (Public API)
POST   /pay                  # Universal payment endpoint
POST   /invoice              # Create invoice
GET    /balance              # Get balance across all federations
GET    /info                 # Basic node information

# Payment Status
GET    /payment/{id}         # Get payment details
GET    /payments             # List payments with filtering

# On-chain Operations  
POST   /deposit              # Generate deposit address
POST   /withdraw             # Withdraw to on-chain address

# Federation Management (Admin API)
POST   /federation/join      # Join a federation
GET    /federations          # List federations
DELETE /federation/{id}      # Leave a federation

# Advanced Operations (Optional)
POST   /ecash/mint           # Direct ecash operations
POST   /ecash/melt           # Direct ecash operations

Universal Payment Endpoint

The /pay endpoint should handle all payment types automatically:

// Request
POST /pay
{
  "destination": "lnbc...|lnurl1...|bob@strike.me|offer1...",
  "amount_sat": 1000,  // Optional, uses invoice amount if not specified
  "comment": "Pizza payment",  // Optional
  "federation_id": "fed123"  // Optional, auto-selects if not specified
}

// Response
{
  "payment_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "amount_sat": 1000,
  "fee_sat": 10,
  "destination_type": "lightning_invoice",
  "completed_at": "2024-01-20T15:30:00Z",
  "preimage": "abc123..."  // Only for Lightning
}

Simplified Invoice Creation

// Request
POST /invoice
{
  "amount_sat": 1000,
  "description": "Coffee payment",
  "expiry_seconds": 3600  // Optional, defaults to 24 hours
}

// Response
{
  "invoice_id": "inv_123",
  "invoice": "lnbc1000...",
  "amount_sat": 1000,
  "expires_at": "2024-01-20T16:30:00Z",
  "status": "pending"
}

Balance Response

// Response
GET /balance
{
  "total_sat": 50000,
  "available_sat": 49000,
  "pending_sat": 1000,
  "federations": [
    {
      "id": "fed123",
      "name": "My Federation",
      "balance_sat": 30000
    }
  ]
}

Migration Plan

Phase 1: API Wrapper Layer (Week 1-2)

  1. Create new simplified endpoints that wrap existing functionality
  2. Map new endpoint requests to existing internal handlers
  3. Transform responses to new simplified format
  4. Deploy alongside existing /v2 API

Phase 2: Internal Refactoring (Week 3-4)

  1. Refactor internal core to align with new API structure
  2. Consolidate payment logic into universal payment handler
  3. Simplify error handling and response generation
  4. Update documentation

Phase 3: Testing & Documentation (Week 5)

  1. Comprehensive testing of new endpoints
  2. Update API documentation
  3. Create migration guide for existing users
  4. Add deprecation notices to old endpoints

Phase 4: Gradual Migration (Week 6+)

  1. Mark /v2 endpoints as deprecated
  2. Provide 3-month migration period
  3. Monitor usage and assist users with migration
  4. Remove old endpoints in next major version

Implementation Roadmap

Immediate Actions (Sprint 1)

  • Create /pay universal payment endpoint
  • Implement simplified /invoice endpoint
  • Add /balance endpoint with federation breakdown
  • Implement two-tier authentication system

Short-term Goals (Sprint 2-3)

  • Refactor response structures for consistency
  • Implement streaming endpoints for real-time updates
  • Add comprehensive error messages
  • Create API documentation site

Medium-term Goals (Sprint 4-6)

  • Deprecate module-specific endpoints
  • Add webhook support for payment notifications
  • Implement rate limiting and API keys
  • Add metrics and monitoring endpoints

Long-term Vision

  • GraphQL API option for complex queries
  • Plugin system for custom modules
  • Multi-language SDKs
  • API gateway for enterprise features

Technical Implementation Details

1. New Router Structure

// src/api/routes.rs
pub fn create_router(state: AppState) -> Router {
    Router::new()
        // Public endpoints (limited access)
        .route("/info", get(handlers::get_info))
        .route("/balance", get(handlers::get_balance))
        .route("/invoice", post(handlers::create_invoice))
        .route("/payment/:id", get(handlers::get_payment))
        
        // Protected endpoints (full access)
        .route("/pay", post(handlers::universal_pay))
        .route("/withdraw", post(handlers::withdraw))
        
        // Admin endpoints
        .nest("/federation", federation_routes())
        
        // Apply auth middleware
        .layer(auth_middleware)
        .with_state(state)
}

2. Universal Payment Handler

// src/handlers/pay.rs
pub async fn universal_pay(
    State(app): State<AppState>,
    Json(req): Json<PayRequest>,
) -> Result<Json<PayResponse>, ApiError> {
    // Detect payment type
    let payment_type = detect_payment_type(&req.destination)?;
    
    // Route to appropriate handler
    match payment_type {
        PaymentType::Invoice(invoice) => pay_invoice(app, invoice, req.amount_sat).await,
        PaymentType::LnUrl(url) => pay_lnurl(app, url, req.amount_sat).await,
        PaymentType::LnAddress(addr) => pay_ln_address(app, addr, req.amount_sat).await,
        PaymentType::Offer(offer) => pay_offer(app, offer, req.amount_sat).await,
        PaymentType::Ecash(notes) => pay_ecash(app, notes).await,
    }
}

3. Simplified Response Types

// src/api/types.rs
#[derive(Serialize)]
pub struct PayResponse {
    pub payment_id: Uuid,
    pub status: PaymentStatus,
    pub amount_sat: u64,
    pub fee_sat: u64,
    pub destination_type: String,
    pub completed_at: Option<DateTime<Utc>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub preimage: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
pub enum PaymentStatus {
    Pending,
    Completed,
    Failed,
}

Benefits of the New Design

For Users

  1. Easier to learn - Fewer endpoints to understand
  2. Intuitive operations - Endpoints match user intentions
  3. Consistent experience - Uniform patterns across all operations
  4. Better error messages - Clear, actionable feedback

For Developers

  1. Simpler integration - Less complexity to handle
  2. Cleaner SDKs - Easier to build client libraries
  3. Better documentation - Simpler API is easier to document
  4. Future-proof - Room to grow without breaking changes

For Maintainers

  1. Cleaner codebase - Less duplication and clearer structure
  2. Easier testing - Fewer edge cases to handle
  3. Better monitoring - Clearer metrics and logging
  4. Reduced support burden - Simpler API means fewer user issues

Conclusion

By adopting phoenixd's design philosophy while maintaining Fedimint's unique capabilities, FMCD can provide a significantly improved developer experience. The proposed design:

  1. Reduces complexity from 30+ endpoints to ~15 core endpoints
  2. Improves usability with intuitive, action-based operations
  3. Maintains flexibility for advanced users and future features
  4. Ensures compatibility with gradual migration path

The key insight from phoenixd is that simplicity drives adoption. Users shouldn't need to understand the underlying protocol to make payments. By hiding Fedimint's complexity behind a clean API, FMCD can become the go-to solution for developers wanting to integrate Fedimint into their applications.

Appendix: API Endpoint Mapping

Current FMCD Endpoint Proposed New Endpoint Notes
/v2/ln/invoice /invoice Simplified path
/v2/ln/pay /pay Universal payment
/v2/mint/spend /pay Handled by universal pay
/v2/mint/reissue /ecash/mint Advanced operation
/v2/onchain/withdraw /withdraw Simplified path
/v2/onchain/deposit-address /deposit Simplified name
/v2/admin/join /federation/join Clearer grouping
/v2/admin/info /info Public endpoint
/v2/admin/backup /backup Admin endpoint
/v2/admin/restore /restore Admin endpoint

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions