Pārlūkot izejas kodu

feat: support custom payment methods and unified router (#1251)

Adds support for custom payment methods and refactors the payment routing architecture to be generic and pluggable.

- **Custom Router:** Added `CustomRouter` and `CustomHandlers` in `cdk-axum` to enable pluggable payment method handlers with custom routes.
- **Unified Routing:** Refactored wallet mint and melt operations to use a unified router that supports Bolt11, Bolt12, and custom methods dynamically.
- **Typed Settings:** Migrated the settings endpoint to use a strongly-typed `SettingsResponse` for improved validation.
- **NUT-19 Caching:** Implemented caching for custom mint and melt handlers to support idempotent retries.
- **Proof Recovery:** Restored and fixed proof recovery mechanisms (`try_proof_operation_or_reclaim`) for melt operations.
- **FFI & Docs:** Updated FFI bindings and Swagger documentation to reflect the new API structure.
asmo 2 nedēļas atpakaļ
vecāks
revīzija
255db0c3ae
71 mainītis faili ar 4386 papildinājumiem un 1305 dzēšanām
  1. 1 0
      Cargo.lock
  2. 245 85
      crates/cashu/src/nuts/auth/nut21.rs
  3. 24 9
      crates/cashu/src/nuts/auth/nut22.rs
  4. 6 2
      crates/cashu/src/nuts/mod.rs
  5. 163 33
      crates/cashu/src/nuts/nut00/mod.rs
  6. 123 4
      crates/cashu/src/nuts/nut04.rs
  7. 126 4
      crates/cashu/src/nuts/nut05.rs
  8. 8 4
      crates/cashu/src/nuts/nut06.rs
  9. 12 5
      crates/cashu/src/nuts/nut15.rs
  10. 68 9
      crates/cashu/src/nuts/nut17/mod.rs
  11. 41 14
      crates/cashu/src/nuts/nut19.rs
  12. 0 213
      crates/cdk-axum/src/bolt12_router.rs
  13. 570 0
      crates/cdk-axum/src/custom_handlers.rs
  14. 224 0
      crates/cdk-axum/src/custom_router.rs
  15. 26 48
      crates/cdk-axum/src/lib.rs
  16. 3 311
      crates/cdk-axum/src/router_handlers.rs
  17. 1 0
      crates/cdk-cli/Cargo.toml
  18. 19 3
      crates/cdk-cli/src/sub_commands/mint.rs
  19. 33 12
      crates/cdk-cln/src/lib.rs
  20. 92 41
      crates/cdk-common/src/database/mint/test/mint.rs
  21. 4 3
      crates/cdk-common/src/database/wallet/test/mod.rs
  22. 10 2
      crates/cdk-common/src/melt.rs
  23. 43 2
      crates/cdk-common/src/mint.rs
  24. 90 16
      crates/cdk-common/src/payment.rs
  25. 0 2
      crates/cdk-common/src/wallet.rs
  26. 27 12
      crates/cdk-fake-wallet/src/lib.rs
  27. 41 21
      crates/cdk-ffi/src/types/mint.rs
  28. 109 7
      crates/cdk-ffi/src/types/quote.rs
  29. 76 0
      crates/cdk-ffi/src/wallet.rs
  30. 26 7
      crates/cdk-integration-tests/src/init_auth_mint.rs
  31. 25 2
      crates/cdk-integration-tests/src/init_pure_tests.rs
  32. 7 4
      crates/cdk-integration-tests/tests/mint.rs
  33. 4 0
      crates/cdk-integration-tests/tests/regtest.rs
  34. 22 8
      crates/cdk-ldk-node/src/lib.rs
  35. 19 13
      crates/cdk-lnbits/src/lib.rs
  36. 25 15
      crates/cdk-lnd/src/lib.rs
  37. 14 13
      crates/cdk-mint-rpc/src/proto/server.rs
  38. 5 0
      crates/cdk-mintd/example.config.toml
  39. 298 108
      crates/cdk-mintd/src/lib.rs
  40. 51 4
      crates/cdk-payment-processor/src/proto/client.rs
  41. 18 2
      crates/cdk-payment-processor/src/proto/mod.rs
  42. 73 20
      crates/cdk-payment-processor/src/proto/payment_processor.proto
  43. 50 3
      crates/cdk-payment-processor/src/proto/server.rs
  44. 3 1
      crates/cdk-redb/src/wallet/mod.rs
  45. 1 0
      crates/cdk-sql-common/src/mint/quotes.rs
  46. 7 6
      crates/cdk-sqlite/src/wallet/mod.rs
  47. 1 1
      crates/cdk/src/mint/auth/mod.rs
  48. 459 40
      crates/cdk/src/mint/builder.rs
  49. 100 61
      crates/cdk/src/mint/issue/mod.rs
  50. 3 3
      crates/cdk/src/mint/ln.rs
  51. 2 1
      crates/cdk/src/mint/melt/melt_saga/mod.rs
  52. 221 44
      crates/cdk/src/mint/melt/melt_saga/tests.rs
  53. 132 10
      crates/cdk/src/mint/melt/mod.rs
  54. 33 1
      crates/cdk/src/mint/mod.rs
  55. 20 18
      crates/cdk/src/mint/subscription.rs
  56. 2 1
      crates/cdk/src/test_helpers/mint.rs
  57. 9 5
      crates/cdk/src/wallet/issue/bolt11.rs
  58. 6 2
      crates/cdk/src/wallet/issue/bolt12.rs
  59. 237 0
      crates/cdk/src/wallet/issue/custom.rs
  60. 72 2
      crates/cdk/src/wallet/issue/mod.rs
  61. 15 7
      crates/cdk/src/wallet/melt/bolt11.rs
  62. 2 1
      crates/cdk/src/wallet/melt/bolt12.rs
  63. 51 0
      crates/cdk/src/wallet/melt/custom.rs
  64. 38 4
      crates/cdk/src/wallet/melt/mod.rs
  65. 104 25
      crates/cdk/src/wallet/mint_connector/http_client.rs
  66. 16 3
      crates/cdk/src/wallet/mint_connector/mod.rs
  67. 1 1
      crates/cdk/src/wallet/mint_connector/transport/tor_transport.rs
  68. 0 1
      crates/cdk/src/wallet/receive.rs
  69. 4 4
      crates/cdk/src/wallet/streams/mod.rs
  70. 2 2
      crates/cdk/src/wallet/streams/proof.rs
  71. 23 0
      misc/mintd_payment_processor.sh

+ 1 - 0
Cargo.lock

@@ -1234,6 +1234,7 @@ dependencies = [
  "bip39",
  "bitcoin 0.32.8",
  "cdk",
+ "cdk-common",
  "cdk-redb",
  "cdk-sqlite",
  "clap",

+ 245 - 85
crates/cashu/src/nuts/auth/nut21.rs

@@ -5,8 +5,6 @@ use std::str::FromStr;
 
 use regex::Regex;
 use serde::{Deserialize, Serialize};
-use strum::IntoEnumIterator;
-use strum_macros::EnumIter;
 use thiserror::Error;
 
 /// NUT21 Error
@@ -93,7 +91,7 @@ impl<'de> Deserialize<'de> for Settings {
 }
 
 /// List of the methods and paths that are protected
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct ProtectedEndpoint {
     /// HTTP Method
@@ -121,71 +119,135 @@ pub enum Method {
 }
 
 /// Route path
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-#[serde(rename_all = "snake_case")]
 pub enum RoutePath {
-    /// Bolt11 Mint Quote
-    #[serde(rename = "/v1/mint/quote/bolt11")]
-    MintQuoteBolt11,
-    /// Bolt11 Mint
-    #[serde(rename = "/v1/mint/bolt11")]
-    MintBolt11,
-    /// Bolt11 Melt Quote
-    #[serde(rename = "/v1/melt/quote/bolt11")]
-    MeltQuoteBolt11,
-    /// Bolt11 Melt
-    #[serde(rename = "/v1/melt/bolt11")]
-    MeltBolt11,
+    /// Mint Quote for a specific payment method
+    MintQuote(String),
+    /// Mint for a specific payment method
+    Mint(String),
+    /// Melt Quote for a specific payment method
+    MeltQuote(String),
+    /// Melt for a specific payment method
+    Melt(String),
     /// Swap
-    #[serde(rename = "/v1/swap")]
     Swap,
     /// Checkstate
-    #[serde(rename = "/v1/checkstate")]
     Checkstate,
     /// Restore
-    #[serde(rename = "/v1/restore")]
     Restore,
     /// Mint Blind Auth
-    #[serde(rename = "/v1/auth/blind/mint")]
     MintBlindAuth,
-    /// Bolt12 Mint Quote
-    #[serde(rename = "/v1/mint/quote/bolt12")]
-    MintQuoteBolt12,
-    /// Bolt12 Mint
-    #[serde(rename = "/v1/mint/bolt12")]
-    MintBolt12,
-    /// Bolt12 Melt Quote
-    #[serde(rename = "/v1/melt/quote/bolt12")]
-    MeltQuoteBolt12,
-    /// Bolt12 Quote
-    #[serde(rename = "/v1/melt/bolt12")]
-    MeltBolt12,
-
     /// WebSocket
-    #[serde(rename = "/v1/ws")]
     Ws,
 }
 
+impl Serialize for RoutePath {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(&self.to_string())
+    }
+}
+
+impl<'de> Deserialize<'de> for RoutePath {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+
+        // Try to parse as a known static path first
+        match s.as_str() {
+            "/v1/swap" => Ok(RoutePath::Swap),
+            "/v1/checkstate" => Ok(RoutePath::Checkstate),
+            "/v1/restore" => Ok(RoutePath::Restore),
+            "/v1/auth/blind/mint" => Ok(RoutePath::MintBlindAuth),
+            "/v1/ws" => Ok(RoutePath::Ws),
+            _ => {
+                // Try to parse as a payment method route
+                if let Some(method) = s.strip_prefix("/v1/mint/quote/") {
+                    Ok(RoutePath::MintQuote(method.to_string()))
+                } else if let Some(method) = s.strip_prefix("/v1/mint/") {
+                    Ok(RoutePath::Mint(method.to_string()))
+                } else if let Some(method) = s.strip_prefix("/v1/melt/quote/") {
+                    Ok(RoutePath::MeltQuote(method.to_string()))
+                } else if let Some(method) = s.strip_prefix("/v1/melt/") {
+                    Ok(RoutePath::Melt(method.to_string()))
+                } else {
+                    // Unknown path - this might be an old database value or config
+                    // Provide a helpful error message
+                    Err(serde::de::Error::custom(format!(
+                        "Unknown route path: {}. Valid paths are: /v1/mint/quote/{{method}}, /v1/mint/{{method}}, /v1/melt/quote/{{method}}, /v1/melt/{{method}}, /v1/swap, /v1/checkstate, /v1/restore, /v1/auth/blind/mint, /v1/ws",
+                        s
+                    )))
+                }
+            }
+        }
+    }
+}
+
+impl RoutePath {
+    /// Get all non-payment-method route paths
+    /// These are routes that don't depend on payment methods
+    pub fn static_paths() -> Vec<RoutePath> {
+        vec![
+            RoutePath::Swap,
+            RoutePath::Checkstate,
+            RoutePath::Restore,
+            RoutePath::MintBlindAuth,
+            RoutePath::Ws,
+        ]
+    }
+
+    /// Get all route paths for common payment methods (bolt11, bolt12)
+    /// This is used for regex matching in configuration
+    pub fn common_payment_method_paths() -> Vec<RoutePath> {
+        let methods = vec!["bolt11", "bolt12"];
+        let mut paths = Vec::new();
+
+        for method in methods {
+            paths.push(RoutePath::MintQuote(method.to_string()));
+            paths.push(RoutePath::Mint(method.to_string()));
+            paths.push(RoutePath::MeltQuote(method.to_string()));
+            paths.push(RoutePath::Melt(method.to_string()));
+        }
+
+        paths
+    }
+
+    /// Get all paths for regex matching (static + common payment methods)
+    pub fn all_known_paths() -> Vec<RoutePath> {
+        let mut paths = Self::static_paths();
+        paths.extend(Self::common_payment_method_paths());
+        paths
+    }
+}
+
 /// Returns [`RoutePath`]s that match regex
+/// Matches against all known static paths and common payment methods (bolt11, bolt12)
 pub fn matching_route_paths(pattern: &str) -> Result<Vec<RoutePath>, Error> {
     let regex = Regex::from_str(pattern)?;
 
-    Ok(RoutePath::iter()
+    Ok(RoutePath::all_known_paths()
+        .into_iter()
         .filter(|path| regex.is_match(&path.to_string()))
         .collect())
 }
-
 impl std::fmt::Display for RoutePath {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        // Use serde to serialize to a JSON string, then extract the value without quotes
-        let json_str = match serde_json::to_string(self) {
-            Ok(s) => s,
-            Err(_) => return write!(f, "<error>"),
-        };
-        // Remove the quotes from the JSON string
-        let path = json_str.trim_matches('"');
-        write!(f, "{path}")
+        match self {
+            RoutePath::MintQuote(method) => write!(f, "/v1/mint/quote/{}", method),
+            RoutePath::Mint(method) => write!(f, "/v1/mint/{}", method),
+            RoutePath::MeltQuote(method) => write!(f, "/v1/melt/quote/{}", method),
+            RoutePath::Melt(method) => write!(f, "/v1/melt/{}", method),
+            RoutePath::Swap => write!(f, "/v1/swap"),
+            RoutePath::Checkstate => write!(f, "/v1/checkstate"),
+            RoutePath::Restore => write!(f, "/v1/restore"),
+            RoutePath::MintBlindAuth => write!(f, "/v1/auth/blind/mint"),
+            RoutePath::Ws => write!(f, "/v1/ws"),
+        }
     }
 }
 
@@ -193,26 +255,40 @@ impl std::fmt::Display for RoutePath {
 mod tests {
 
     use super::*;
+    use crate::nut00::KnownMethod;
+    use crate::PaymentMethod;
 
     #[test]
     fn test_matching_route_paths_all() {
         // Regex that matches all paths
         let paths = matching_route_paths(".*").unwrap();
 
-        // Should match all variants
-        assert_eq!(paths.len(), RoutePath::iter().count());
+        // Should match all known variants
+        assert_eq!(paths.len(), RoutePath::all_known_paths().len());
 
         // Verify all variants are included
-        assert!(paths.contains(&RoutePath::MintQuoteBolt11));
-        assert!(paths.contains(&RoutePath::MintBolt11));
-        assert!(paths.contains(&RoutePath::MeltQuoteBolt11));
-        assert!(paths.contains(&RoutePath::MeltBolt11));
+        assert!(paths.contains(&RoutePath::MintQuote(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
+        assert!(paths.contains(&RoutePath::Mint(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
+        assert!(paths.contains(&RoutePath::MeltQuote(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
+        assert!(paths.contains(&RoutePath::Melt(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
+        assert!(paths.contains(&RoutePath::MintQuote(
+            PaymentMethod::Known(KnownMethod::Bolt12).to_string()
+        )));
+        assert!(paths.contains(&RoutePath::Mint(
+            PaymentMethod::Known(KnownMethod::Bolt12).to_string()
+        )));
         assert!(paths.contains(&RoutePath::Swap));
         assert!(paths.contains(&RoutePath::Checkstate));
         assert!(paths.contains(&RoutePath::Restore));
         assert!(paths.contains(&RoutePath::MintBlindAuth));
-        assert!(paths.contains(&RoutePath::MintQuoteBolt12));
-        assert!(paths.contains(&RoutePath::MintBolt12));
     }
 
     #[test]
@@ -220,18 +296,34 @@ mod tests {
         // Regex that matches only mint paths
         let paths = matching_route_paths("^/v1/mint/.*").unwrap();
 
-        // Should match only mint paths
+        // Should match only mint paths (4 paths: mint quote and mint for bolt11 and bolt12)
         assert_eq!(paths.len(), 4);
-        assert!(paths.contains(&RoutePath::MintQuoteBolt11));
-        assert!(paths.contains(&RoutePath::MintBolt11));
-        assert!(paths.contains(&RoutePath::MintQuoteBolt12));
-        assert!(paths.contains(&RoutePath::MintBolt12));
+        assert!(paths.contains(&RoutePath::MintQuote(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
+        assert!(paths.contains(&RoutePath::Mint(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
+        assert!(paths.contains(&RoutePath::MintQuote(
+            PaymentMethod::Known(KnownMethod::Bolt12).to_string()
+        )));
+        assert!(paths.contains(&RoutePath::Mint(
+            PaymentMethod::Known(KnownMethod::Bolt12).to_string()
+        )));
 
         // Should not match other paths
-        assert!(!paths.contains(&RoutePath::MeltQuoteBolt11));
-        assert!(!paths.contains(&RoutePath::MeltBolt11));
-        assert!(!paths.contains(&RoutePath::MeltQuoteBolt12));
-        assert!(!paths.contains(&RoutePath::MeltBolt12));
+        assert!(!paths.contains(&RoutePath::MeltQuote(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
+        assert!(!paths.contains(&RoutePath::Melt(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
+        assert!(!paths.contains(&RoutePath::MeltQuote(
+            PaymentMethod::Known(KnownMethod::Bolt12).to_string()
+        )));
+        assert!(!paths.contains(&RoutePath::Melt(
+            PaymentMethod::Known(KnownMethod::Bolt12).to_string()
+        )));
         assert!(!paths.contains(&RoutePath::Swap));
     }
 
@@ -240,16 +332,28 @@ mod tests {
         // Regex that matches only quote paths
         let paths = matching_route_paths(".*/quote/.*").unwrap();
 
-        // Should match only quote paths
+        // Should match only quote paths (4 paths: mint quote and melt quote for bolt11 and bolt12)
         assert_eq!(paths.len(), 4);
-        assert!(paths.contains(&RoutePath::MintQuoteBolt11));
-        assert!(paths.contains(&RoutePath::MeltQuoteBolt11));
-        assert!(paths.contains(&RoutePath::MintQuoteBolt12));
-        assert!(paths.contains(&RoutePath::MeltQuoteBolt12));
+        assert!(paths.contains(&RoutePath::MintQuote(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
+        assert!(paths.contains(&RoutePath::MeltQuote(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
+        assert!(paths.contains(&RoutePath::MintQuote(
+            PaymentMethod::Known(KnownMethod::Bolt12).to_string()
+        )));
+        assert!(paths.contains(&RoutePath::MeltQuote(
+            PaymentMethod::Known(KnownMethod::Bolt12).to_string()
+        )));
 
         // Should not match non-quote paths
-        assert!(!paths.contains(&RoutePath::MintBolt11));
-        assert!(!paths.contains(&RoutePath::MeltBolt11));
+        assert!(!paths.contains(&RoutePath::Mint(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
+        assert!(!paths.contains(&RoutePath::Melt(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
     }
 
     #[test]
@@ -263,12 +367,14 @@ mod tests {
 
     #[test]
     fn test_matching_route_paths_quote_bolt11_only() {
-        // Regex that matches only quote paths
+        // Regex that matches only mint quote bolt11 path
         let paths = matching_route_paths("/v1/mint/quote/bolt11").unwrap();
 
-        // Should match only quote paths
+        // Should match only this specific path
         assert_eq!(paths.len(), 1);
-        assert!(paths.contains(&RoutePath::MintQuoteBolt11));
+        assert!(paths.contains(&RoutePath::MintQuote(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        )));
     }
 
     #[test]
@@ -285,15 +391,25 @@ mod tests {
     fn test_route_path_to_string() {
         // Test that to_string() returns the correct path strings
         assert_eq!(
-            RoutePath::MintQuoteBolt11.to_string(),
+            RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
             "/v1/mint/quote/bolt11"
         );
-        assert_eq!(RoutePath::MintBolt11.to_string(), "/v1/mint/bolt11");
         assert_eq!(
-            RoutePath::MeltQuoteBolt11.to_string(),
+            RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
+            "/v1/mint/bolt11"
+        );
+        assert_eq!(
+            RoutePath::MeltQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
             "/v1/melt/quote/bolt11"
         );
-        assert_eq!(RoutePath::MeltBolt11.to_string(), "/v1/melt/bolt11");
+        assert_eq!(
+            RoutePath::Melt(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
+            "/v1/melt/bolt11"
+        );
+        assert_eq!(
+            RoutePath::MintQuote("paypal".to_string()).to_string(),
+            "/v1/mint/quote/paypal"
+        );
         assert_eq!(RoutePath::Swap.to_string(), "/v1/swap");
         assert_eq!(RoutePath::Checkstate.to_string(), "/v1/checkstate");
         assert_eq!(RoutePath::Restore.to_string(), "/v1/restore");
@@ -301,6 +417,35 @@ mod tests {
     }
 
     #[test]
+    fn test_route_path_serialization() {
+        // Test serialization of payment method paths
+        let json = serde_json::to_string(&RoutePath::Mint(
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string(),
+        ))
+        .unwrap();
+        assert_eq!(json, "\"/v1/mint/bolt11\"");
+
+        let json = serde_json::to_string(&RoutePath::MintQuote("paypal".to_string())).unwrap();
+        assert_eq!(json, "\"/v1/mint/quote/paypal\"");
+
+        // Test deserialization of payment method paths
+        let path: RoutePath = serde_json::from_str("\"/v1/mint/bolt11\"").unwrap();
+        assert_eq!(
+            path,
+            RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string())
+        );
+
+        let path: RoutePath = serde_json::from_str("\"/v1/melt/quote/venmo\"").unwrap();
+        assert_eq!(path, RoutePath::MeltQuote("venmo".to_string()));
+
+        // Test round-trip serialization
+        let original = RoutePath::Melt(PaymentMethod::Known(KnownMethod::Bolt12).to_string());
+        let json = serde_json::to_string(&original).unwrap();
+        let deserialized: RoutePath = serde_json::from_str(&json).unwrap();
+        assert_eq!(original, deserialized);
+    }
+
+    #[test]
     fn test_settings_deserialize_direct_paths() {
         let json = r#"{
             "openid_discovery": "https://example.com/.well-known/openid-configuration",
@@ -330,9 +475,12 @@ mod tests {
         let paths = settings
             .protected_endpoints
             .iter()
-            .map(|ep| (ep.method, ep.path))
+            .map(|ep| (ep.method, ep.path.clone()))
             .collect::<Vec<_>>();
-        assert!(paths.contains(&(Method::Get, RoutePath::MintBolt11)));
+        assert!(paths.contains(&(
+            Method::Get,
+            RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string())
+        )));
         assert!(paths.contains(&(Method::Post, RoutePath::Swap)));
     }
 
@@ -360,14 +508,26 @@ mod tests {
             "https://example.com/.well-known/openid-configuration"
         );
         assert_eq!(settings.client_id, "client123");
-        assert_eq!(settings.protected_endpoints.len(), 5); // 3 mint paths + 1 swap path
+        assert_eq!(settings.protected_endpoints.len(), 5); // 4 mint paths (bolt11+bolt12 quote+mint) + 1 swap path
 
         let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
             ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
-            ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
-            ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
-            ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt12),
-            ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt12),
+            ProtectedEndpoint::new(
+                Method::Get,
+                RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+            ),
+            ProtectedEndpoint::new(
+                Method::Get,
+                RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+            ),
+            ProtectedEndpoint::new(
+                Method::Get,
+                RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
+            ),
+            ProtectedEndpoint::new(
+                Method::Get,
+                RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
+            ),
         ]);
 
         let deserlized_protected = settings.protected_endpoints.into_iter().collect();
@@ -410,7 +570,7 @@ mod tests {
         assert_eq!(settings.protected_endpoints[0].method, Method::Get);
         assert_eq!(
             settings.protected_endpoints[0].path,
-            RoutePath::MintQuoteBolt11
+            RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string())
         );
     }
 
@@ -430,7 +590,7 @@ mod tests {
         let settings: Settings = serde_json::from_str(json).unwrap();
         assert_eq!(
             settings.protected_endpoints.len(),
-            RoutePath::iter().count()
+            RoutePath::all_known_paths().len()
         );
     }
 }

+ 24 - 9
crates/cashu/src/nuts/auth/nut22.rs

@@ -275,10 +275,10 @@ impl MintAuthRequest {
 mod tests {
     use std::collections::HashSet;
 
-    use strum::IntoEnumIterator;
-
     use super::super::nut21::{Method, RoutePath};
     use super::*;
+    use crate::nut00::KnownMethod;
+    use crate::PaymentMethod;
 
     #[test]
     fn test_settings_deserialize_direct_paths() {
@@ -305,9 +305,12 @@ mod tests {
         let paths = settings
             .protected_endpoints
             .iter()
-            .map(|ep| (ep.method, ep.path))
+            .map(|ep| (ep.method, ep.path.clone()))
             .collect::<Vec<_>>();
-        assert!(paths.contains(&(Method::Get, RoutePath::MintBolt11)));
+        assert!(paths.contains(&(
+            Method::Get,
+            RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string())
+        )));
         assert!(paths.contains(&(Method::Post, RoutePath::Swap)));
     }
 
@@ -334,10 +337,22 @@ mod tests {
 
         let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
             ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
-            ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
-            ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
-            ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt12),
-            ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt12),
+            ProtectedEndpoint::new(
+                Method::Get,
+                RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+            ),
+            ProtectedEndpoint::new(
+                Method::Get,
+                RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+            ),
+            ProtectedEndpoint::new(
+                Method::Get,
+                RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
+            ),
+            ProtectedEndpoint::new(
+                Method::Get,
+                RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
+            ),
         ]);
 
         let deserialized_protected = settings.protected_endpoints.into_iter().collect();
@@ -376,7 +391,7 @@ mod tests {
         let settings: Settings = serde_json::from_str(json).unwrap();
         assert_eq!(
             settings.protected_endpoints.len(),
-            RoutePath::iter().count()
+            RoutePath::all_known_paths().len()
         );
     }
 }

+ 6 - 2
crates/cashu/src/nuts/mod.rs

@@ -47,9 +47,13 @@ pub use nut02::{Id, KeySet, KeySetInfo, KeysetResponse};
 #[cfg(feature = "wallet")]
 pub use nut03::PreSwap;
 pub use nut03::{SwapRequest, SwapResponse};
-pub use nut04::{MintMethodSettings, MintRequest, MintResponse, Settings as NUT04Settings};
+pub use nut04::{
+    MintMethodSettings, MintQuoteCustomRequest, MintQuoteCustomResponse, MintRequest, MintResponse,
+    Settings as NUT04Settings,
+};
 pub use nut05::{
-    MeltMethodSettings, MeltRequest, QuoteState as MeltQuoteState, Settings as NUT05Settings,
+    MeltMethodSettings, MeltQuoteCustomRequest, MeltQuoteCustomResponse, MeltRequest,
+    QuoteState as MeltQuoteState, Settings as NUT05Settings,
 };
 pub use nut06::{ContactInfo, MintInfo, MintVersion, Nuts};
 pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};

+ 163 - 33
crates/cashu/src/nuts/nut00/mod.rs

@@ -9,7 +9,7 @@ use std::hash::{Hash, Hasher};
 use std::str::FromStr;
 use std::string::FromUtf8Error;
 
-use serde::{de, Deserialize, Deserializer, Serialize};
+use serde::{Deserialize, Deserializer, Serialize};
 use thiserror::Error;
 
 use super::nut02::ShortKeysetId;
@@ -639,46 +639,166 @@ impl<'de> Deserialize<'de> for CurrencyUnit {
     }
 }
 
-/// Payment Method
-#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
+/// Known payment methods
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-pub enum PaymentMethod {
-    /// Bolt11 payment type
-    #[default]
+pub enum KnownMethod {
+    /// Lightning BOLT11
     Bolt11,
-    /// Bolt12
+    /// Lightning BOLT12
     Bolt12,
-    /// Custom
-    Custom(String),
 }
 
-impl FromStr for PaymentMethod {
+impl KnownMethod {
+    /// Get the method name as a string
+    pub fn as_str(&self) -> &str {
+        match self {
+            Self::Bolt11 => "bolt11",
+            Self::Bolt12 => "bolt12",
+        }
+    }
+}
+
+impl fmt::Display for KnownMethod {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str())
+    }
+}
+
+impl FromStr for KnownMethod {
     type Err = Error;
     fn from_str(value: &str) -> Result<Self, Self::Err> {
         match value.to_lowercase().as_str() {
             "bolt11" => Ok(Self::Bolt11),
             "bolt12" => Ok(Self::Bolt12),
-            c => Ok(Self::Custom(c.to_string())),
+            _ => Err(Error::UnsupportedPaymentMethod),
         }
     }
 }
 
-impl fmt::Display for PaymentMethod {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+/// Payment Method
+///
+/// Represents either a known payment method (bolt11, bolt12) or a custom payment method.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub enum PaymentMethod {
+    /// Known payment method (bolt11, bolt12)
+    Known(KnownMethod),
+    /// Custom payment method (e.g., "paypal", "stripe")
+    Custom(String),
+}
+
+impl PaymentMethod {
+    /// BOLT11 payment method
+    pub const BOLT11: Self = Self::Known(KnownMethod::Bolt11);
+    /// BOLT12 payment method
+    pub const BOLT12: Self = Self::Known(KnownMethod::Bolt12);
+
+    /// Create a new PaymentMethod from a string
+    pub fn new(method: String) -> Self {
+        Self::from_str(&method).unwrap_or_else(|_| Self::Custom(method.to_lowercase()))
+    }
+
+    /// Get the method name as a string
+    pub fn as_str(&self) -> &str {
         match self {
-            PaymentMethod::Bolt11 => write!(f, "bolt11"),
-            PaymentMethod::Bolt12 => write!(f, "bolt12"),
-            PaymentMethod::Custom(p) => write!(f, "{p}"),
+            Self::Known(known) => known.as_str(),
+            Self::Custom(custom) => custom.as_str(),
+        }
+    }
+
+    /// Check if this is a known method
+    pub fn is_known(&self) -> bool {
+        matches!(self, Self::Known(_))
+    }
+
+    /// Check if this is a custom method
+    pub fn is_custom(&self) -> bool {
+        matches!(self, Self::Custom(_))
+    }
+
+    /// Check if this is bolt11
+    pub fn is_bolt11(&self) -> bool {
+        matches!(self, Self::Known(KnownMethod::Bolt11))
+    }
+
+    /// Check if this is bolt12
+    pub fn is_bolt12(&self) -> bool {
+        matches!(self, Self::Known(KnownMethod::Bolt12))
+    }
+}
+
+impl FromStr for PaymentMethod {
+    type Err = Error;
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        match KnownMethod::from_str(value) {
+            Ok(known) => Ok(Self::Known(known)),
+            Err(_) => Ok(Self::Custom(value.to_lowercase())),
         }
     }
 }
 
+impl fmt::Display for PaymentMethod {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str())
+    }
+}
+
+impl From<String> for PaymentMethod {
+    fn from(s: String) -> Self {
+        Self::from_str(&s).unwrap_or_else(|_| Self::Custom(s.to_lowercase()))
+    }
+}
+
+impl From<&str> for PaymentMethod {
+    fn from(s: &str) -> Self {
+        Self::from_str(s).unwrap_or_else(|_| Self::Custom(s.to_lowercase()))
+    }
+}
+
+impl From<KnownMethod> for PaymentMethod {
+    fn from(known: KnownMethod) -> Self {
+        Self::Known(known)
+    }
+}
+
+// Implement PartialEq with &str for ergonomic comparisons
+impl PartialEq<&str> for PaymentMethod {
+    fn eq(&self, other: &&str) -> bool {
+        self.as_str() == *other
+    }
+}
+
+impl PartialEq<str> for PaymentMethod {
+    fn eq(&self, other: &str) -> bool {
+        self.as_str() == other
+    }
+}
+
+impl PartialEq<PaymentMethod> for &str {
+    fn eq(&self, other: &PaymentMethod) -> bool {
+        *self == other.as_str()
+    }
+}
+
+impl PartialEq<KnownMethod> for PaymentMethod {
+    fn eq(&self, other: &KnownMethod) -> bool {
+        matches!(self, Self::Known(k) if k == other)
+    }
+}
+
+impl PartialEq<PaymentMethod> for KnownMethod {
+    fn eq(&self, other: &PaymentMethod) -> bool {
+        matches!(other, PaymentMethod::Known(k) if k == self)
+    }
+}
+
 impl Serialize for PaymentMethod {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: serde::Serializer,
     {
-        serializer.serialize_str(&self.to_string())
+        serializer.serialize_str(self.as_str())
     }
 }
 
@@ -688,7 +808,7 @@ impl<'de> Deserialize<'de> for PaymentMethod {
         D: Deserializer<'de>,
     {
         let payment_method: String = String::deserialize(deserializer)?;
-        Self::from_str(&payment_method).map_err(|_| de::Error::custom("Unsupported payment method"))
+        Ok(Self::from_str(&payment_method).unwrap_or(Self::Custom(payment_method)))
     }
 }
 
@@ -992,47 +1112,57 @@ mod tests {
 
     #[test]
     fn test_payment_method_parsing() {
-        // Test standard variants
+        // Test known methods (case insensitive)
         assert_eq!(
             PaymentMethod::from_str("bolt11").unwrap(),
-            PaymentMethod::Bolt11
+            PaymentMethod::BOLT11
         );
         assert_eq!(
             PaymentMethod::from_str("BOLT11").unwrap(),
-            PaymentMethod::Bolt11
+            PaymentMethod::BOLT11
         );
         assert_eq!(
             PaymentMethod::from_str("Bolt11").unwrap(),
-            PaymentMethod::Bolt11
+            PaymentMethod::Known(KnownMethod::Bolt11)
         );
 
         assert_eq!(
             PaymentMethod::from_str("bolt12").unwrap(),
-            PaymentMethod::Bolt12
+            PaymentMethod::BOLT12
         );
         assert_eq!(
             PaymentMethod::from_str("BOLT12").unwrap(),
-            PaymentMethod::Bolt12
-        );
-        assert_eq!(
-            PaymentMethod::from_str("Bolt12").unwrap(),
-            PaymentMethod::Bolt12
+            PaymentMethod::Known(KnownMethod::Bolt12)
         );
 
-        // Test custom variants
+        // Test custom methods
         assert_eq!(
             PaymentMethod::from_str("custom").unwrap(),
             PaymentMethod::Custom("custom".to_string())
         );
         assert_eq!(
-            PaymentMethod::from_str("CUSTOM").unwrap(),
-            PaymentMethod::Custom("custom".to_string())
+            PaymentMethod::from_str("PAYPAL").unwrap(),
+            PaymentMethod::Custom("paypal".to_string())
         );
 
+        // Test string conversion
+        assert_eq!(PaymentMethod::BOLT11.as_str(), "bolt11");
+        assert_eq!(PaymentMethod::BOLT12.as_str(), "bolt12");
+        assert_eq!(PaymentMethod::from("paypal").as_str(), "paypal");
+
+        // Test ergonomic comparisons with strings
+        assert!(PaymentMethod::BOLT11 == "bolt11");
+        assert!(PaymentMethod::BOLT12 == "bolt12");
+        assert!(PaymentMethod::Custom("paypal".to_string()) == "paypal");
+
+        // Test comparison with KnownMethod
+        assert!(PaymentMethod::BOLT11 == KnownMethod::Bolt11);
+        assert!(PaymentMethod::BOLT12 == KnownMethod::Bolt12);
+
         // Test serialization/deserialization consistency
         let methods = vec![
-            PaymentMethod::Bolt11,
-            PaymentMethod::Bolt12,
+            PaymentMethod::BOLT11,
+            PaymentMethod::BOLT12,
             PaymentMethod::Custom("test".to_string()),
         ];
 

+ 123 - 4
crates/cashu/src/nuts/nut04.rs

@@ -12,11 +12,13 @@ use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
 use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
+use crate::nut00::KnownMethod;
+use crate::nut23::QuoteState;
 #[cfg(feature = "mint")]
 use crate::quote_id::QuoteId;
 #[cfg(feature = "mint")]
 use crate::quote_id::QuoteIdError;
-use crate::Amount;
+use crate::{Amount, PublicKey};
 
 /// NUT04 Error
 #[derive(Debug, Error)]
@@ -79,7 +81,7 @@ pub struct MintResponse {
 }
 
 /// Mint Method Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct MintMethodSettings {
     /// Payment Method e.g. bolt11
@@ -214,7 +216,7 @@ impl<'de> Visitor<'de> for MintMethodSettingsVisitor {
         let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
 
         // Create options based on the method and the description flag
-        let options = if method == PaymentMethod::Bolt11 {
+        let options = if method == PaymentMethod::Known(KnownMethod::Bolt11) {
             description.map(|description| MintMethodOptions::Bolt11 { description })
         } else {
             None
@@ -249,6 +251,8 @@ pub enum MintMethodOptions {
         /// Mint supports setting bolt11 description
         description: bool,
     },
+    /// Custom Options
+    Custom {},
 }
 
 /// Mint Settings
@@ -305,11 +309,126 @@ impl Settings {
     }
 }
 
+/// Custom payment method mint quote request
+///
+/// This is a generic request type that works for any custom payment method.
+/// The method name is provided in the URL path, not in the request body.
+///
+/// The `extra` field allows payment-method-specific fields to be included
+/// without being nested. When serialized, extra fields merge into the parent JSON.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MintQuoteCustomRequest {
+    /// Amount to mint
+    pub amount: Amount,
+    /// Currency unit
+    pub unit: CurrencyUnit,
+    /// Optional description
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub description: Option<String>,
+    /// NUT-19 Pubkey
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pubkey: Option<PublicKey>,
+    /// Extra payment-method-specific fields
+    ///
+    /// These fields are flattened into the JSON representation, allowing
+    /// custom payment methods to include additional data (e.g., ehash share).
+    /// This enables proper validation layering: the mint verifies well-defined
+    /// fields while passing extra through to the payment processor.
+    #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
+    #[cfg_attr(feature = "swagger", schema(value_type = Object, additional_properties = true))]
+    pub extra: serde_json::Value,
+}
+
+/// Custom payment method mint quote response
+///
+/// This is a generic response type for custom payment methods.
+///
+/// The `extra` field allows payment-method-specific fields to be included
+/// without being nested. When serialized, extra fields merge into the parent JSON:
+/// ```json
+/// {
+///   "quote": "abc123",
+///   "state": "UNPAID",
+///   "amount": 1000,
+///   "paypal_link": "https://paypal.me/merchant",
+///   "paypal_email": "merchant@example.com"
+/// }
+/// ```
+///
+/// This separation enables proper validation layering: the mint verifies
+/// well-defined fields (amount, unit, state, etc.) while passing extra through
+/// to the gRPC payment processor for method-specific validation.
+///
+/// It also provides a clean upgrade path: when a payment method becomes speced,
+/// its fields can be promoted from `extra` to well-defined struct fields without
+/// breaking existing clients (e.g., bolt12's `amount_paid` and `amount_issued`).
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(bound = "Q: Serialize + for<'a> Deserialize<'a>")]
+pub struct MintQuoteCustomResponse<Q> {
+    /// Quote ID
+    pub quote: Q,
+    /// Payment request string (method-specific format)
+    pub request: String,
+    /// Amount
+    pub amount: Option<Amount>,
+    /// Currency unit
+    pub unit: Option<CurrencyUnit>,
+    /// Quote State
+    pub state: QuoteState,
+    /// Unix timestamp until the quote is valid
+    pub expiry: Option<u64>,
+    /// NUT-19 Pubkey
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pubkey: Option<PublicKey>,
+    /// Extra payment-method-specific fields
+    ///
+    /// These fields are flattened into the JSON representation, allowing
+    /// custom payment methods to include additional data without nesting.
+    #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
+    #[cfg_attr(feature = "swagger", schema(value_type = Object, additional_properties = true))]
+    pub extra: serde_json::Value,
+}
+
+#[cfg(feature = "mint")]
+impl<Q: ToString> MintQuoteCustomResponse<Q> {
+    /// Convert the MintQuoteCustomResponse with a quote type Q to a String
+    pub fn to_string_id(&self) -> MintQuoteCustomResponse<String> {
+        MintQuoteCustomResponse {
+            quote: self.quote.to_string(),
+            request: self.request.clone(),
+            amount: self.amount,
+            state: self.state,
+            unit: self.unit.clone(),
+            expiry: self.expiry,
+            pubkey: self.pubkey,
+            extra: self.extra.clone(),
+        }
+    }
+}
+
+#[cfg(feature = "mint")]
+impl From<MintQuoteCustomResponse<QuoteId>> for MintQuoteCustomResponse<String> {
+    fn from(value: MintQuoteCustomResponse<QuoteId>) -> Self {
+        Self {
+            quote: value.quote.to_string(),
+            request: value.request,
+            amount: value.amount,
+            unit: value.unit,
+            expiry: value.expiry,
+            state: value.state,
+            pubkey: value.pubkey,
+            extra: value.extra,
+        }
+    }
+}
 #[cfg(test)]
 mod tests {
     use serde_json::{from_str, json, to_string};
 
     use super::*;
+    use crate::nut00::KnownMethod;
 
     #[test]
     fn test_mint_method_settings_top_level_description() {
@@ -326,7 +445,7 @@ mod tests {
         let settings: MintMethodSettings = from_str(json_str).unwrap();
 
         // Check that description was correctly moved to options
-        assert_eq!(settings.method, PaymentMethod::Bolt11);
+        assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Bolt11));
         assert_eq!(settings.unit, CurrencyUnit::Sat);
         assert_eq!(settings.min_amount, Some(Amount::from(0)));
         assert_eq!(settings.max_amount, Some(Amount::from(10000)));

+ 126 - 4
crates/cashu/src/nuts/nut05.rs

@@ -10,8 +10,9 @@ use serde::ser::{SerializeStruct, Serializer};
 use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
-use super::nut00::{BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
+use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
 use super::ProofsMethods;
+use crate::nut00::KnownMethod;
 #[cfg(feature = "mint")]
 use crate::quote_id::QuoteId;
 use crate::Amount;
@@ -190,7 +191,7 @@ where
 }
 
 /// Melt Method Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct MeltMethodSettings {
     /// Payment Method e.g. bolt11
@@ -325,7 +326,8 @@ impl<'de> Visitor<'de> for MeltMethodSettingsVisitor {
         let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
 
         // Create options based on the method and the amountless flag
-        let options = if method == PaymentMethod::Bolt11 && amountless.is_some() {
+        let options = if method == PaymentMethod::Known(KnownMethod::Bolt11) && amountless.is_some()
+        {
             amountless.map(|amountless| MeltMethodOptions::Bolt11 { amountless })
         } else {
             None
@@ -418,11 +420,131 @@ impl Settings {
     }
 }
 
+/// Custom payment method melt quote request
+///
+/// This is a generic request type for melting tokens with custom payment methods.
+///
+/// The `extra` field allows payment-method-specific fields to be included
+/// without being nested. When serialized, extra fields merge into the parent JSON.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MeltQuoteCustomRequest {
+    /// Custom payment method name
+    pub method: String,
+    /// Payment request string (method-specific format)
+    pub request: String,
+    /// Currency unit
+    pub unit: CurrencyUnit,
+    /// Extra payment-method-specific fields
+    ///
+    /// These fields are flattened into the JSON representation, allowing
+    /// custom payment methods to include additional data.
+    #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
+    #[cfg_attr(feature = "swagger", schema(value_type = Object, additional_properties = true))]
+    pub extra: serde_json::Value,
+}
+
+/// Custom payment method melt quote response
+///
+/// This is a generic response type for custom payment methods.
+///
+/// The `extra` field allows payment-method-specific fields to be included
+/// without being nested. When serialized, extra fields merge into the parent JSON:
+/// ```json
+/// {
+///   "quote": "abc123",
+///   "state": "UNPAID",
+///   "amount": 1000,
+///   "fee_reserve": 10,
+///   "custom_field": "value"
+/// }
+/// ```
+///
+/// This separation enables proper validation layering: the mint verifies
+/// well-defined fields (amount, fee_reserve, state, etc.) while passing extra
+/// through to the gRPC payment processor for method-specific validation.
+///
+/// It also provides a clean upgrade path: when a payment method becomes speced,
+/// its fields can be promoted from `extra` to well-defined struct fields without
+/// breaking existing clients.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(bound = "Q: Serialize + for<'a> Deserialize<'a>")]
+pub struct MeltQuoteCustomResponse<Q> {
+    /// Quote ID
+    pub quote: Q,
+    /// Amount to be melted
+    pub amount: Amount,
+    /// Fee reserve required
+    pub fee_reserve: Amount,
+    /// Quote State
+    pub state: QuoteState,
+    /// Unix timestamp until the quote is valid
+    pub expiry: u64,
+    /// Payment preimage (if payment completed)
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub payment_preimage: Option<String>,
+    /// Change (blinded signatures for overpaid amount)
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub change: Option<Vec<BlindSignature>>,
+    /// Payment request (optional, for reference)
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub request: Option<String>,
+    /// Currency unit
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub unit: Option<CurrencyUnit>,
+    /// Extra payment-method-specific fields
+    ///
+    /// These fields are flattened into the JSON representation, allowing
+    /// custom payment methods to include additional data without nesting.
+    #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
+    #[cfg_attr(feature = "swagger", schema(value_type = Object, additional_properties = true))]
+    pub extra: serde_json::Value,
+}
+
+#[cfg(feature = "mint")]
+impl<Q: ToString> MeltQuoteCustomResponse<Q> {
+    /// Convert the MeltQuoteCustomResponse with a quote type Q to a String
+    pub fn to_string_id(&self) -> MeltQuoteCustomResponse<String> {
+        MeltQuoteCustomResponse {
+            quote: self.quote.to_string(),
+            amount: self.amount,
+            fee_reserve: self.fee_reserve,
+            state: self.state,
+            expiry: self.expiry,
+            payment_preimage: self.payment_preimage.clone(),
+            change: self.change.clone(),
+            request: self.request.clone(),
+            unit: self.unit.clone(),
+            extra: self.extra.clone(),
+        }
+    }
+}
+
+#[cfg(feature = "mint")]
+impl From<MeltQuoteCustomResponse<QuoteId>> for MeltQuoteCustomResponse<String> {
+    fn from(value: MeltQuoteCustomResponse<QuoteId>) -> Self {
+        Self {
+            quote: value.quote.to_string(),
+            amount: value.amount,
+            fee_reserve: value.fee_reserve,
+            state: value.state,
+            expiry: value.expiry,
+            payment_preimage: value.payment_preimage,
+            change: value.change,
+            request: value.request,
+            unit: value.unit,
+            extra: value.extra,
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use serde_json::{from_str, json, to_string};
 
     use super::*;
+    use crate::nut00::KnownMethod;
 
     #[test]
     fn test_melt_method_settings_top_level_amountless() {
@@ -439,7 +561,7 @@ mod tests {
         let settings: MeltMethodSettings = from_str(json_str).unwrap();
 
         // Check that amountless was correctly moved to options
-        assert_eq!(settings.method, PaymentMethod::Bolt11);
+        assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Bolt11));
         assert_eq!(settings.unit, CurrencyUnit::Sat);
         assert_eq!(settings.min_amount, Some(Amount::from(0)));
         assert_eq!(settings.max_amount, Some(Amount::from(10000)));

+ 8 - 4
crates/cashu/src/nuts/nut06.rs

@@ -226,13 +226,13 @@ impl MintInfo {
 
         if let Some(nut21_settings) = &self.nuts.nut21 {
             for endpoint in nut21_settings.protected_endpoints.iter() {
-                protected_endpoints.insert(*endpoint, AuthRequired::Clear);
+                protected_endpoints.insert(endpoint.clone(), AuthRequired::Clear);
             }
         }
 
         if let Some(nut22_settings) = &self.nuts.nut22 {
             for endpoint in nut22_settings.protected_endpoints.iter() {
-                protected_endpoints.insert(*endpoint, AuthRequired::Blind);
+                protected_endpoints.insert(endpoint.clone(), AuthRequired::Blind);
             }
         }
         protected_endpoints
@@ -506,6 +506,7 @@ impl ContactInfo {
 mod tests {
 
     use super::*;
+    use crate::nut00::KnownMethod;
     use crate::nut04::MintMethodOptions;
 
     #[test]
@@ -668,7 +669,10 @@ mod tests {
         let t = mint_info
             .nuts
             .nut04
-            .get_settings(&crate::CurrencyUnit::Sat, &crate::PaymentMethod::Bolt11)
+            .get_settings(
+                &crate::CurrencyUnit::Sat,
+                &crate::PaymentMethod::Known(KnownMethod::Bolt11),
+            )
             .unwrap();
 
         let t = t.options.unwrap();
@@ -697,7 +701,7 @@ mod tests {
         let mint_info_with_nut15 = MintInfo {
             name: Some("Test Mint".to_string()),
             nuts: Nuts::default().nut15(vec![MppMethodSettings {
-                method: crate::PaymentMethod::Bolt11,
+                method: crate::PaymentMethod::Known(KnownMethod::Bolt11),
                 unit: crate::CurrencyUnit::Sat,
             }]),
             ..Default::default()

+ 12 - 5
crates/cashu/src/nuts/nut15.rs

@@ -17,7 +17,7 @@ pub struct Mpp {
 }
 
 /// Mpp Method Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct MppMethodSettings {
     /// Payment Method e.g. bolt11
@@ -65,6 +65,7 @@ impl<'de> Deserialize<'de> for Settings {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::nut00::KnownMethod;
     use crate::PaymentMethod;
 
     #[test]
@@ -73,14 +74,20 @@ mod tests {
         let array_json = r#"[{"method":"bolt11","unit":"sat"}]"#;
         let settings: Settings = serde_json::from_str(array_json).unwrap();
         assert_eq!(settings.methods.len(), 1);
-        assert_eq!(settings.methods[0].method, PaymentMethod::Bolt11);
+        assert_eq!(
+            settings.methods[0].method,
+            PaymentMethod::Known(KnownMethod::Bolt11)
+        );
         assert_eq!(settings.methods[0].unit, CurrencyUnit::Sat);
 
         // Test object format
         let object_json = r#"{"methods":[{"method":"bolt11","unit":"sat"}]}"#;
         let settings: Settings = serde_json::from_str(object_json).unwrap();
         assert_eq!(settings.methods.len(), 1);
-        assert_eq!(settings.methods[0].method, PaymentMethod::Bolt11);
+        assert_eq!(
+            settings.methods[0].method,
+            PaymentMethod::Known(KnownMethod::Bolt11)
+        );
         assert_eq!(settings.methods[0].unit, CurrencyUnit::Sat);
     }
 
@@ -88,7 +95,7 @@ mod tests {
     fn test_nut15_settings_serialization() {
         let settings = Settings {
             methods: vec![MppMethodSettings {
-                method: PaymentMethod::Bolt11,
+                method: PaymentMethod::Known(KnownMethod::Bolt11),
                 unit: CurrencyUnit::Sat,
             }],
         };
@@ -104,7 +111,7 @@ mod tests {
 
         let settings_with_data = Settings {
             methods: vec![MppMethodSettings {
-                method: PaymentMethod::Bolt11,
+                method: PaymentMethod::Known(KnownMethod::Bolt11),
                 unit: CurrencyUnit::Sat,
             }],
         };

+ 68 - 9
crates/cashu/src/nuts/nut17/mod.rs

@@ -3,6 +3,7 @@ use serde::de::DeserializeOwned;
 use serde::{Deserialize, Serialize};
 
 use super::PublicKey;
+use crate::nut00::KnownMethod;
 use crate::nuts::{
     CurrencyUnit, MeltQuoteBolt11Response, MintQuoteBolt11Response, PaymentMethod, ProofState,
 };
@@ -63,7 +64,7 @@ impl SupportedMethods {
         ];
 
         Self {
-            method: PaymentMethod::Bolt11,
+            method: PaymentMethod::Known(KnownMethod::Bolt11),
             unit,
             commands,
         }
@@ -78,33 +79,91 @@ impl SupportedMethods {
         ];
 
         Self {
-            method: PaymentMethod::Bolt12,
+            method: PaymentMethod::Known(KnownMethod::Bolt12),
+            unit,
+            commands,
+        }
+    }
+
+    /// Create [`SupportedMethods`] for custom payment method with all supported commands
+    pub fn default_custom(method: PaymentMethod, unit: CurrencyUnit) -> Self {
+        let method_name = method.to_string();
+        let commands = vec![
+            WsCommand::Custom(format!("{}_mint_quote", method_name)),
+            WsCommand::Custom(format!("{}_melt_quote", method_name)),
+            WsCommand::ProofState,
+        ];
+
+        Self {
+            method,
             unit,
             commands,
         }
     }
 }
 
+impl WsCommand {
+    /// Create a custom mint quote command for a payment method
+    pub fn custom_mint_quote(method: &str) -> Self {
+        WsCommand::Custom(format!("{}_mint_quote", method))
+    }
+
+    /// Create a custom melt quote command for a payment method
+    pub fn custom_melt_quote(method: &str) -> Self {
+        WsCommand::Custom(format!("{}_melt_quote", method))
+    }
+}
+
 /// WebSocket commands supported by the Cashu mint
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-#[serde(rename_all = "snake_case")]
 pub enum WsCommand {
     /// Command to request a Lightning invoice for minting tokens
-    #[serde(rename = "bolt11_mint_quote")]
     Bolt11MintQuote,
     /// Command to request a Lightning payment for melting tokens
-    #[serde(rename = "bolt11_melt_quote")]
     Bolt11MeltQuote,
     /// Websocket support for Bolt12 Mint Quote
-    #[serde(rename = "bolt12_mint_quote")]
     Bolt12MintQuote,
     /// Websocket support for Bolt12 Melt Quote
-    #[serde(rename = "bolt12_melt_quote")]
     Bolt12MeltQuote,
     /// Command to check the state of a proof
-    #[serde(rename = "proof_state")]
     ProofState,
+    /// Custom payment method command
+    Custom(String),
+}
+
+impl Serialize for WsCommand {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let s = match self {
+            WsCommand::Bolt11MintQuote => "bolt11_mint_quote",
+            WsCommand::Bolt11MeltQuote => "bolt11_melt_quote",
+            WsCommand::Bolt12MintQuote => "bolt12_mint_quote",
+            WsCommand::Bolt12MeltQuote => "bolt12_melt_quote",
+            WsCommand::ProofState => "proof_state",
+            WsCommand::Custom(custom) => custom.as_str(),
+        };
+        serializer.serialize_str(s)
+    }
+}
+
+impl<'de> Deserialize<'de> for WsCommand {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        Ok(match s.as_str() {
+            "bolt11_mint_quote" => WsCommand::Bolt11MintQuote,
+            "bolt11_melt_quote" => WsCommand::Bolt11MeltQuote,
+            "bolt12_mint_quote" => WsCommand::Bolt12MintQuote,
+            "bolt12_melt_quote" => WsCommand::Bolt12MeltQuote,
+            "proof_state" => WsCommand::ProofState,
+            custom => WsCommand::Custom(custom.to_string()),
+        })
+    }
 }
 
 impl<T> From<MintQuoteBolt12Response<T>> for NotificationPayload<T>

+ 41 - 14
crates/cashu/src/nuts/nut19.rs

@@ -31,6 +31,18 @@ impl CachedEndpoint {
     }
 }
 
+impl Path {
+    /// Create a custom mint path for a payment method
+    pub fn custom_mint(method: &str) -> Self {
+        Path::Custom(format!("/v1/mint/{}", method))
+    }
+
+    /// Create a custom melt path for a payment method
+    pub fn custom_melt(method: &str) -> Self {
+        Path::Custom(format!("/v1/melt/{}", method))
+    }
+}
+
 /// HTTP method
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
 #[serde(rename_all = "UPPERCASE")]
@@ -43,22 +55,37 @@ pub enum Method {
 }
 
 /// Route path
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub enum Path {
-    /// Bolt11 Mint
-    #[serde(rename = "/v1/mint/bolt11")]
-    MintBolt11,
-    /// Bolt11 Melt
-    #[serde(rename = "/v1/melt/bolt11")]
-    MeltBolt11,
     /// Swap
-    #[serde(rename = "/v1/swap")]
     Swap,
-    /// Bolt12 Mint
-    #[serde(rename = "/v1/mint/bolt12")]
-    MintBolt12,
-    /// Bolt12 Melt
-    #[serde(rename = "/v1/melt/bolt12")]
-    MeltBolt12,
+    /// Custom payment method path (including bolt11, bolt12, and other methods)
+    Custom(String),
+}
+
+impl Serialize for Path {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let s = match self {
+            Path::Swap => "/v1/swap",
+            Path::Custom(custom) => custom.as_str(),
+        };
+        serializer.serialize_str(s)
+    }
+}
+
+impl<'de> Deserialize<'de> for Path {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        Ok(match s.as_str() {
+            "/v1/swap" => Path::Swap,
+            custom => Path::Custom(custom.to_string()),
+        })
+    }
 }

+ 0 - 213
crates/cdk-axum/src/bolt12_router.rs

@@ -1,213 +0,0 @@
-use anyhow::Result;
-use axum::extract::{Json, Path, State};
-use axum::response::Response;
-#[cfg(feature = "swagger")]
-use cdk::error::ErrorResponse;
-use cdk::mint::QuoteId;
-#[cfg(feature = "auth")]
-use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
-use cdk::nuts::{
-    MeltQuoteBolt11Response, MeltQuoteBolt12Request, MeltRequest, MintQuoteBolt12Request,
-    MintQuoteBolt12Response, MintRequest, MintResponse,
-};
-use paste::paste;
-use tracing::instrument;
-
-#[cfg(feature = "auth")]
-use crate::auth::AuthHeader;
-use crate::{into_response, post_cache_wrapper, MintState};
-
-post_cache_wrapper!(post_mint_bolt12, MintRequest<QuoteId>, MintResponse);
-post_cache_wrapper!(
-    post_melt_bolt12,
-    MeltRequest<QuoteId>,
-    MeltQuoteBolt11Response<QuoteId>
-);
-
-#[cfg_attr(feature = "swagger", utoipa::path(
-    get,
-    context_path = "/v1",
-    path = "/mint/quote/bolt12",
-    responses(
-        (status = 200, description = "Successful response", body = MintQuoteBolt12Response<String>, content_type = "application/json")
-    )
-))]
-/// Get mint bolt12 quote
-#[instrument(skip_all, fields(amount = ?payload.amount))]
-pub async fn post_mint_bolt12_quote(
-    #[cfg(feature = "auth")] auth: AuthHeader,
-    State(state): State<MintState>,
-    Json(payload): Json<MintQuoteBolt12Request>,
-) -> Result<Json<MintQuoteBolt12Response<QuoteId>>, Response> {
-    #[cfg(feature = "auth")]
-    {
-        state
-            .mint
-            .verify_auth(
-                auth.into(),
-                &ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt12),
-            )
-            .await
-            .map_err(into_response)?;
-    }
-
-    let quote = state
-        .mint
-        .get_mint_quote(payload.into())
-        .await
-        .map_err(into_response)?;
-
-    Ok(Json(quote.try_into().map_err(into_response)?))
-}
-
-#[cfg_attr(feature = "swagger", utoipa::path(
-    get,
-    context_path = "/v1",
-    path = "/mint/quote/bolt12/{quote_id}",
-    params(
-        ("quote_id" = String, description = "The quote ID"),
-    ),
-    responses(
-        (status = 200, description = "Successful response", body = MintQuoteBolt12Response<String>, content_type = "application/json"),
-        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
-    )
-))]
-/// Get mint bolt12 quote
-#[instrument(skip_all, fields(quote_id = ?quote_id))]
-pub async fn get_check_mint_bolt12_quote(
-    #[cfg(feature = "auth")] auth: AuthHeader,
-    State(state): State<MintState>,
-    Path(quote_id): Path<QuoteId>,
-) -> Result<Json<MintQuoteBolt12Response<QuoteId>>, Response> {
-    #[cfg(feature = "auth")]
-    {
-        state
-            .mint
-            .verify_auth(
-                auth.into(),
-                &ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt12),
-            )
-            .await
-            .map_err(into_response)?;
-    }
-
-    let quote = state
-        .mint
-        .check_mint_quote(&quote_id)
-        .await
-        .map_err(into_response)?;
-
-    Ok(Json(quote.try_into().map_err(into_response)?))
-}
-
-#[cfg_attr(feature = "swagger", utoipa::path(
-    post,
-    context_path = "/v1",
-    path = "/mint/bolt12",
-    request_body(content = MintRequest<String>, description = "Request params", content_type = "application/json"),
-    responses(
-        (status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
-        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
-    )
-))]
-/// Request a quote for melting tokens
-#[instrument(skip_all, fields(quote_id = ?payload.quote))]
-pub async fn post_mint_bolt12(
-    #[cfg(feature = "auth")] auth: AuthHeader,
-    State(state): State<MintState>,
-    Json(payload): Json<MintRequest<QuoteId>>,
-) -> Result<Json<MintResponse>, Response> {
-    #[cfg(feature = "auth")]
-    {
-        state
-            .mint
-            .verify_auth(
-                auth.into(),
-                &ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt12),
-            )
-            .await
-            .map_err(into_response)?;
-    }
-
-    let res = state
-        .mint
-        .process_mint_request(payload)
-        .await
-        .map_err(|err| {
-            tracing::error!("Could not process mint: {}", err);
-            into_response(err)
-        })?;
-
-    Ok(Json(res))
-}
-
-#[cfg_attr(feature = "swagger", utoipa::path(
-    post,
-    context_path = "/v1",
-    path = "/melt/quote/bolt12",
-    request_body(content = MeltQuoteBolt12Request, description = "Quote params", content_type = "application/json"),
-    responses(
-        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
-        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
-    )
-))]
-pub async fn post_melt_bolt12_quote(
-    #[cfg(feature = "auth")] auth: AuthHeader,
-    State(state): State<MintState>,
-    Json(payload): Json<MeltQuoteBolt12Request>,
-) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
-    #[cfg(feature = "auth")]
-    {
-        state
-            .mint
-            .verify_auth(
-                auth.into(),
-                &ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt12),
-            )
-            .await
-            .map_err(into_response)?;
-    }
-
-    let quote = state
-        .mint
-        .get_melt_quote(payload.into())
-        .await
-        .map_err(into_response)?;
-
-    Ok(Json(quote))
-}
-
-#[cfg_attr(feature = "swagger", utoipa::path(
-    post,
-    context_path = "/v1",
-    path = "/melt/bolt12",
-    request_body(content = MeltRequest<String>, description = "Melt params", content_type = "application/json"),
-    responses(
-        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
-        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
-    )
-))]
-/// Melt tokens for a Bitcoin payment that the mint will make for the user in exchange
-///
-/// Requests tokens to be destroyed and sent out via Lightning.
-pub async fn post_melt_bolt12(
-    #[cfg(feature = "auth")] auth: AuthHeader,
-    State(state): State<MintState>,
-    Json(payload): Json<MeltRequest<QuoteId>>,
-) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
-    #[cfg(feature = "auth")]
-    {
-        state
-            .mint
-            .verify_auth(
-                auth.into(),
-                &ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt12),
-            )
-            .await
-            .map_err(into_response)?;
-    }
-
-    let res = state.mint.melt(&payload).await.map_err(into_response)?;
-
-    Ok(Json(res))
-}

+ 570 - 0
crates/cdk-axum/src/custom_handlers.rs

@@ -0,0 +1,570 @@
+//! Generic handlers for custom payment methods
+//!
+//! These handlers work for ANY custom payment method without requiring
+//! method-specific validation or request parsing.
+//!
+//! Special handling for bolt11 and bolt12:
+//! When the method parameter is "bolt11" or "bolt12", these handlers use the
+//! specific Bolt11/Bolt12 request/response types instead of the generic custom types.
+
+use axum::extract::{FromRequestParts, Json, Path, State};
+use axum::http::request::Parts;
+use axum::http::StatusCode;
+use axum::response::{IntoResponse, Response};
+use cdk::mint::QuoteId;
+#[cfg(feature = "auth")]
+use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
+use cdk::nuts::{
+    MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteBolt12Request,
+    MeltQuoteCustomRequest, MintQuoteBolt11Request, MintQuoteBolt11Response,
+    MintQuoteBolt12Request, MintQuoteBolt12Response, MintQuoteCustomRequest, MintRequest,
+    MintResponse,
+};
+use serde_json::Value;
+use tracing::instrument;
+
+#[cfg(feature = "auth")]
+use crate::auth::AuthHeader;
+use crate::router_handlers::into_response;
+use crate::MintState;
+
+const PREFER_HEADER_KEY: &str = "Prefer";
+
+/// Header extractor for the Prefer header
+///
+/// This extractor checks for the `Prefer: respond-async` header
+/// to determine if the client wants asynchronous processing
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct PreferHeader {
+    pub respond_async: bool,
+}
+
+impl<S> FromRequestParts<S> for PreferHeader
+where
+    S: Send + Sync,
+{
+    type Rejection = (StatusCode, String);
+
+    async fn from_request_parts(
+        parts: &mut Parts,
+        _state: &S,
+    ) -> anyhow::Result<Self, Self::Rejection> {
+        // Check for Prefer header
+        if let Some(prefer_value) = parts.headers.get(PREFER_HEADER_KEY) {
+            let value = prefer_value.to_str().map_err(|_| {
+                (
+                    StatusCode::BAD_REQUEST,
+                    "Invalid Prefer header value".to_string(),
+                )
+            })?;
+
+            // Check if it contains "respond-async"
+            let respond_async = value.to_lowercase().contains("respond-async");
+
+            return Ok(PreferHeader { respond_async });
+        }
+
+        // No Prefer header found - default to synchronous processing
+        Ok(PreferHeader {
+            respond_async: false,
+        })
+    }
+}
+/// Generic handler for custom payment method mint quotes
+///
+/// This handler works for ANY custom payment method (e.g., paypal, venmo, cashapp, bolt11, bolt12).
+/// For bolt11/bolt12, it handles the specific request/response types.
+/// For other methods, it passes the request data directly to the payment processor.
+#[instrument(skip_all, fields(method = ?method))]
+pub async fn post_mint_custom_quote(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    State(state): State<MintState>,
+    Path(method): Path<String>,
+    Json(payload): Json<Value>,
+) -> Result<Response, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::MintQuote(method.clone())),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
+    match method.as_str() {
+        "bolt11" => {
+            let bolt11_request: MintQuoteBolt11Request =
+                serde_json::from_value(payload).map_err(|e| {
+                    tracing::error!("Failed to parse bolt11 request: {}", e);
+                    into_response(cdk::Error::InvalidPaymentMethod)
+                })?;
+
+            let quote = state
+                .mint
+                .get_mint_quote(bolt11_request.into())
+                .await
+                .map_err(into_response)?;
+
+            let response: MintQuoteBolt11Response<QuoteId> =
+                quote.try_into().map_err(into_response)?;
+            Ok(Json(response).into_response())
+        }
+        "bolt12" => {
+            let bolt12_request: MintQuoteBolt12Request =
+                serde_json::from_value(payload).map_err(|e| {
+                    tracing::error!("Failed to parse bolt12 request: {}", e);
+                    into_response(cdk::Error::InvalidPaymentMethod)
+                })?;
+
+            let quote = state
+                .mint
+                .get_mint_quote(bolt12_request.into())
+                .await
+                .map_err(into_response)?;
+
+            let response: MintQuoteBolt12Response<QuoteId> =
+                quote.try_into().map_err(into_response)?;
+            Ok(Json(response).into_response())
+        }
+        _ => {
+            let custom_request: MintQuoteCustomRequest =
+                serde_json::from_value(payload).map_err(|e| {
+                    tracing::error!("Failed to parse custom request: {}", e);
+                    into_response(cdk::Error::InvalidPaymentMethod)
+                })?;
+
+            let quote_request = cdk::mint::MintQuoteRequest::Custom {
+                method,
+                request: custom_request,
+            };
+
+            let response = state
+                .mint
+                .get_mint_quote(quote_request)
+                .await
+                .map_err(into_response)?;
+
+            match response {
+                cdk::mint::MintQuoteResponse::Custom { response, .. } => {
+                    Ok(Json(response).into_response())
+                }
+                _ => Err(into_response(cdk::Error::InvalidPaymentMethod)),
+            }
+        }
+    }
+}
+
+/// Get custom payment method mint quote status
+#[instrument(skip_all, fields(method = ?method, quote_id = ?quote_id))]
+pub async fn get_check_mint_custom_quote(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    State(state): State<MintState>,
+    Path((method, quote_id)): Path<(String, QuoteId)>,
+) -> Result<Response, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Get, RoutePath::MintQuote(method.clone())),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
+    let quote_response = state
+        .mint
+        .check_mint_quote(&quote_id)
+        .await
+        .map_err(into_response)?;
+
+    match method.as_str() {
+        "bolt11" => {
+            let response: MintQuoteBolt11Response<QuoteId> =
+                quote_response.try_into().map_err(into_response)?;
+            Ok(Json(response).into_response())
+        }
+        "bolt12" => {
+            let response: MintQuoteBolt12Response<QuoteId> =
+                quote_response.try_into().map_err(into_response)?;
+            Ok(Json(response).into_response())
+        }
+        _ => {
+            // Extract and verify it's a Custom payment method
+            match quote_response {
+                cdk::mint::MintQuoteResponse::Custom {
+                    method: quote_method,
+                    response,
+                } => {
+                    if quote_method != method {
+                        return Err(into_response(cdk::Error::InvalidPaymentMethod));
+                    }
+                    Ok(Json(response).into_response())
+                }
+                _ => Err(into_response(cdk::Error::InvalidPaymentMethod)),
+            }
+        }
+    }
+}
+
+/// Mint tokens with custom payment method
+#[instrument(skip_all, fields(method = ?method, quote_id = ?payload.quote))]
+pub async fn post_mint_custom(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    State(state): State<MintState>,
+    Path(method): Path<String>,
+    Json(payload): Json<MintRequest<QuoteId>>,
+) -> Result<Json<MintResponse>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::Mint(method.clone())),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
+    // Note: process_mint_request will validate the quote internally
+    // including checking if it's paid and matches the expected payment method
+    let res = state
+        .mint
+        .process_mint_request(payload)
+        .await
+        .map_err(into_response)?;
+
+    Ok(Json(res))
+}
+
+/// Request a melt quote for custom payment method
+#[instrument(skip_all, fields(method = ?method))]
+pub async fn post_melt_custom_quote(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    State(state): State<MintState>,
+    Path(method): Path<String>,
+    Json(payload): Json<Value>,
+) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuote(method.clone())),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
+    let response = match method.as_str() {
+        "bolt11" => {
+            let bolt11_request: MeltQuoteBolt11Request =
+                serde_json::from_value(payload).map_err(|e| {
+                    tracing::error!("Failed to parse bolt11 melt request: {}", e);
+                    into_response(cdk::Error::InvalidPaymentMethod)
+                })?;
+
+            state
+                .mint
+                .get_melt_quote(bolt11_request.into())
+                .await
+                .map_err(into_response)?
+        }
+        "bolt12" => {
+            let bolt12_request: MeltQuoteBolt12Request =
+                serde_json::from_value(payload).map_err(|e| {
+                    tracing::error!("Failed to parse bolt12 melt request: {}", e);
+                    into_response(cdk::Error::InvalidPaymentMethod)
+                })?;
+
+            state
+                .mint
+                .get_melt_quote(bolt12_request.into())
+                .await
+                .map_err(into_response)?
+        }
+        _ => {
+            let custom_request: MeltQuoteCustomRequest =
+                serde_json::from_value(payload).map_err(|e| {
+                    tracing::error!("Failed to parse custom melt request: {}", e);
+                    into_response(cdk::Error::InvalidPaymentMethod)
+                })?;
+
+            state
+                .mint
+                .get_melt_quote(custom_request.into())
+                .await
+                .map_err(into_response)?
+        }
+    };
+
+    Ok(Json(response))
+}
+
+/// Get custom payment method melt quote status
+#[instrument(skip_all, fields(method = ?method, quote_id = ?quote_id))]
+pub async fn get_check_melt_custom_quote(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    State(state): State<MintState>,
+    Path((method, quote_id)): Path<(String, QuoteId)>,
+) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuote(method.clone())),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
+    // Note: check_melt_quote returns the response directly
+    // The payment method validation is done when the quote was created
+    let quote = state
+        .mint
+        .check_melt_quote(&quote_id)
+        .await
+        .map_err(into_response)?;
+
+    Ok(Json(quote))
+}
+
+/// Melt tokens with custom payment method
+#[instrument(skip_all, fields(method = ?method))]
+pub async fn post_melt_custom(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    prefer: PreferHeader,
+    State(state): State<MintState>,
+    Path(method): Path<String>,
+    Json(payload): Json<cdk::nuts::MeltRequest<QuoteId>>,
+) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::Melt(method.clone())),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
+    let res = if prefer.respond_async {
+        // Asynchronous processing - return immediately after setup
+        state
+            .mint
+            .melt_async(&payload)
+            .await
+            .map_err(into_response)?
+    } else {
+        // Synchronous processing - wait for completion
+        state.mint.melt(&payload).await.map_err(into_response)?
+    };
+
+    Ok(Json(res))
+}
+
+// ============================================================================
+// CACHED HANDLERS FOR NUT-19 SUPPORT
+// ============================================================================
+
+/// Cached version of post_mint_custom for NUT-19 caching support
+#[instrument(skip_all, fields(method = ?method, quote_id = ?payload.quote))]
+pub async fn cache_post_mint_custom(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    state: State<MintState>,
+    method: Path<String>,
+    payload: Json<MintRequest<QuoteId>>,
+) -> Result<Json<MintResponse>, Response> {
+    use std::ops::Deref;
+
+    let State(mint_state) = state.clone();
+    let json_extracted_payload = payload.deref();
+
+    let cache_key = match mint_state.cache.calculate_key(json_extracted_payload) {
+        Some(key) => key,
+        None => {
+            // Could not calculate key, just return the handler result
+            #[cfg(feature = "auth")]
+            return post_mint_custom(auth, state, method, payload).await;
+            #[cfg(not(feature = "auth"))]
+            return post_mint_custom(state, method, payload).await;
+        }
+    };
+
+    if let Some(cached_response) = mint_state.cache.get::<MintResponse>(&cache_key).await {
+        return Ok(Json(cached_response));
+    }
+
+    #[cfg(feature = "auth")]
+    let result = post_mint_custom(auth, state, method, payload).await?;
+    #[cfg(not(feature = "auth"))]
+    let result = post_mint_custom(state, method, payload).await?;
+
+    // Cache the response
+    mint_state.cache.set(cache_key, result.deref()).await;
+
+    Ok(result)
+}
+
+#[cfg(test)]
+mod tests {
+    use axum::http::{HeaderValue, Request, StatusCode};
+
+    use super::*;
+
+    fn create_test_request(prefer_header: Option<&str>) -> Request<()> {
+        let mut req = Request::builder()
+            .method("POST")
+            .uri("/test")
+            .body(())
+            .unwrap();
+
+        if let Some(header_value) = prefer_header {
+            req.headers_mut().insert(
+                PREFER_HEADER_KEY,
+                HeaderValue::from_str(header_value).unwrap(),
+            );
+        }
+
+        req
+    }
+
+    fn create_test_request_with_bytes(bytes: &[u8]) -> Request<()> {
+        let mut req = Request::builder()
+            .method("POST")
+            .uri("/test")
+            .body(())
+            .unwrap();
+
+        req.headers_mut()
+            .insert(PREFER_HEADER_KEY, HeaderValue::from_bytes(bytes).unwrap());
+
+        req
+    }
+
+    #[tokio::test]
+    async fn test_prefer_header_respond_async() {
+        let req = create_test_request(Some("respond-async"));
+        let (mut parts, _) = req.into_parts();
+
+        let result = PreferHeader::from_request_parts(&mut parts, &()).await;
+        assert!(result.is_ok());
+        assert!(result.unwrap().respond_async);
+    }
+
+    #[tokio::test]
+    async fn test_prefer_header_respond_async_with_other_values() {
+        let req = create_test_request(Some("respond-async; wait=10"));
+        let (mut parts, _) = req.into_parts();
+
+        let result = PreferHeader::from_request_parts(&mut parts, &()).await;
+        assert!(result.is_ok());
+        assert!(result.unwrap().respond_async);
+    }
+
+    #[tokio::test]
+    async fn test_prefer_header_case_insensitive() {
+        let req = create_test_request(Some("RESPOND-ASYNC"));
+        let (mut parts, _) = req.into_parts();
+
+        let result = PreferHeader::from_request_parts(&mut parts, &()).await;
+        assert!(result.is_ok());
+        assert!(result.unwrap().respond_async);
+    }
+
+    #[tokio::test]
+    async fn test_prefer_header_no_respond_async() {
+        let req = create_test_request(Some("wait=10"));
+        let (mut parts, _) = req.into_parts();
+
+        let result = PreferHeader::from_request_parts(&mut parts, &()).await;
+        assert!(result.is_ok());
+        assert!(!result.unwrap().respond_async);
+    }
+
+    #[tokio::test]
+    async fn test_prefer_header_missing() {
+        let req = create_test_request(None);
+        let (mut parts, _) = req.into_parts();
+
+        let result = PreferHeader::from_request_parts(&mut parts, &()).await;
+        assert!(result.is_ok());
+        assert!(!result.unwrap().respond_async);
+    }
+
+    #[tokio::test]
+    async fn test_prefer_header_invalid_value() {
+        let req = create_test_request_with_bytes(&[0xFF, 0xFE]);
+        let (mut parts, _) = req.into_parts();
+
+        let result = PreferHeader::from_request_parts(&mut parts, &()).await;
+        assert!(result.is_err());
+        let (status, message) = result.unwrap_err();
+        assert_eq!(status, StatusCode::BAD_REQUEST);
+        assert_eq!(message, "Invalid Prefer header value");
+    }
+
+    #[tokio::test]
+    async fn test_prefer_header_empty_value() {
+        let req = create_test_request(Some(""));
+        let (mut parts, _) = req.into_parts();
+
+        let result = PreferHeader::from_request_parts(&mut parts, &()).await;
+        assert!(result.is_ok());
+        assert!(!result.unwrap().respond_async);
+    }
+}
+
+/// Cached version of post_melt_custom for NUT-19 caching support
+#[instrument(skip_all, fields(method = ?method))]
+pub async fn cache_post_melt_custom(
+    #[cfg(feature = "auth")] auth: AuthHeader,
+    prefer: PreferHeader,
+    state: State<MintState>,
+    method: Path<String>,
+    payload: Json<cdk::nuts::MeltRequest<QuoteId>>,
+) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
+    use std::ops::Deref;
+
+    let State(mint_state) = state.clone();
+    let json_extracted_payload = payload.deref();
+
+    let cache_key = match mint_state.cache.calculate_key(json_extracted_payload) {
+        Some(key) => key,
+        None => {
+            // Could not calculate key, just return the handler result
+            #[cfg(feature = "auth")]
+            return post_melt_custom(auth, prefer, state, method, payload).await;
+            #[cfg(not(feature = "auth"))]
+            return post_melt_custom(prefer, state, method, payload).await;
+        }
+    };
+
+    if let Some(cached_response) = mint_state
+        .cache
+        .get::<MeltQuoteBolt11Response<QuoteId>>(&cache_key)
+        .await
+    {
+        return Ok(Json(cached_response));
+    }
+
+    #[cfg(feature = "auth")]
+    let result = post_melt_custom(auth, prefer, state, method, payload).await?;
+    #[cfg(not(feature = "auth"))]
+    let result = post_melt_custom(prefer, state, method, payload).await?;
+
+    // Cache the response
+    mint_state.cache.set(cache_key, result.deref()).await;
+
+    Ok(result)
+}

+ 224 - 0
crates/cdk-axum/src/custom_router.rs

@@ -0,0 +1,224 @@
+//! Dynamic router creation for custom payment methods
+//!
+//! Creates dedicated routes for each configured custom payment method,
+//! matching the URL pattern of bolt11/bolt12 routes (e.g., /v1/mint/quote/paypal).
+
+use axum::routing::{get, post};
+use axum::Router;
+
+use crate::custom_handlers::{
+    cache_post_melt_custom, cache_post_mint_custom, get_check_melt_custom_quote,
+    get_check_mint_custom_quote, post_melt_custom_quote, post_mint_custom_quote,
+};
+use crate::MintState;
+
+/// Creates routers for all configured custom payment methods
+///
+/// Creates a single set of parameterized routes that handle all custom methods:
+/// - `/mint/quote/{method}` - POST: Create mint quote
+/// - `/mint/quote/{method}/{quote_id}` - GET: Check mint quote status
+/// - `/mint/{method}` - POST: Mint tokens
+/// - `/melt/quote/{method}` - POST: Create melt quote
+/// - `/melt/quote/{method}/{quote_id}` - GET: Check melt quote status
+/// - `/melt/{method}` - POST: Melt tokens
+///
+/// The {method} parameter captures the payment method name dynamically.
+pub fn create_custom_routers(state: MintState, custom_methods: Vec<String>) -> Router<MintState> {
+    tracing::info!(
+        "Creating routes for {} custom payment methods: {:?}",
+        custom_methods.len(),
+        custom_methods
+    );
+
+    // Create a single router with parameterized routes that handle all custom methods
+    // Use cached versions for mint/melt to support NUT-19 caching
+    Router::new()
+        .route("/mint/quote/{method}", post(post_mint_custom_quote))
+        .route(
+            "/mint/quote/{method}/{quote_id}",
+            get(get_check_mint_custom_quote),
+        )
+        .route("/mint/{method}", post(cache_post_mint_custom))
+        .route("/melt/quote/{method}", post(post_melt_custom_quote))
+        .route(
+            "/melt/quote/{method}/{quote_id}",
+            get(get_check_melt_custom_quote),
+        )
+        .route("/melt/{method}", post(cache_post_melt_custom))
+        .with_state(state)
+}
+
+/// Validates that custom method names are valid
+///
+/// Previously, bolt11 and bolt12 were reserved, but now they can be handled
+/// through the custom router if the payment processor supports them.
+pub fn validate_custom_method_names(methods: &[String]) -> Result<(), String> {
+    for method in methods {
+        // Validate method name contains only URL-safe characters
+        if !method
+            .chars()
+            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
+        {
+            return Err(format!(
+                "Custom payment method name '{}' contains invalid characters. Only alphanumeric, '-', and '_' are allowed.",
+                method
+            ));
+        }
+
+        // Validate method name is not empty
+        if method.is_empty() {
+            return Err("Custom payment method name cannot be empty".to_string());
+        }
+    }
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use cdk::nuts::nut00::KnownMethod;
+    use cdk::nuts::PaymentMethod;
+
+    use super::*;
+
+    #[test]
+    fn test_validate_custom_method_names_valid() {
+        assert!(validate_custom_method_names(&["paypal".to_string()]).is_ok());
+        assert!(
+            validate_custom_method_names(&["venmo".to_string(), "cashapp".to_string()]).is_ok()
+        );
+        assert!(validate_custom_method_names(&["my-method".to_string()]).is_ok());
+        assert!(validate_custom_method_names(&["my_method".to_string()]).is_ok());
+        assert!(validate_custom_method_names(&["method123".to_string()]).is_ok());
+    }
+
+    #[test]
+    fn test_validate_custom_method_names_bolt11_bolt12_allowed() {
+        // bolt11 and bolt12 are now allowed as custom methods
+        assert!(validate_custom_method_names(&[
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        ])
+        .is_ok());
+        assert!(validate_custom_method_names(&[
+            PaymentMethod::Known(KnownMethod::Bolt12).to_string()
+        ])
+        .is_ok());
+        assert!(validate_custom_method_names(&[
+            "paypal".to_string(),
+            PaymentMethod::Known(KnownMethod::Bolt11).to_string()
+        ])
+        .is_ok());
+    }
+
+    #[test]
+    fn test_validate_custom_method_names_invalid_chars() {
+        assert!(validate_custom_method_names(&["pay/pal".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["pay pal".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["pay@pal".to_string()]).is_err());
+    }
+
+    #[test]
+    fn test_validate_custom_method_names_empty() {
+        assert!(validate_custom_method_names(&["".to_string()]).is_err());
+    }
+
+    #[test]
+    fn test_validate_custom_method_names_multiple_invalid() {
+        assert!(validate_custom_method_names(&[
+            "valid".to_string(),
+            "in valid".to_string(),
+            "also-valid".to_string()
+        ])
+        .is_err());
+    }
+
+    #[test]
+    fn test_validate_custom_method_names_special_chars() {
+        // Test various special characters that should fail
+        assert!(validate_custom_method_names(&["pay.pal".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["pay+pal".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["pay$pal".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["pay%pal".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["pay&pal".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["pay*pal".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["pay(pal".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["pay)pal".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["pay=pal".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["pay#pal".to_string()]).is_err());
+    }
+
+    #[test]
+    fn test_validate_custom_method_names_edge_cases() {
+        // Single character names
+        assert!(validate_custom_method_names(&["a".to_string()]).is_ok());
+        assert!(validate_custom_method_names(&["1".to_string()]).is_ok());
+        assert!(validate_custom_method_names(&["-".to_string()]).is_ok());
+        assert!(validate_custom_method_names(&["_".to_string()]).is_ok());
+
+        // Names with only dashes or underscores
+        assert!(validate_custom_method_names(&["---".to_string()]).is_ok());
+        assert!(validate_custom_method_names(&["___".to_string()]).is_ok());
+
+        // Long names
+        let long_name = "a".repeat(100);
+        assert!(validate_custom_method_names(&[long_name]).is_ok());
+    }
+
+    #[test]
+    fn test_validate_custom_method_names_mixed_valid() {
+        assert!(validate_custom_method_names(&[
+            "paypal".to_string(),
+            "cash-app".to_string(),
+            "venmo_pay".to_string(),
+            "method123".to_string(),
+            "UPPERCASE".to_string(),
+        ])
+        .is_ok());
+    }
+
+    #[test]
+    fn test_validate_custom_method_names_error_messages() {
+        // Test that error messages are descriptive
+        let result = validate_custom_method_names(&["pay/pal".to_string()]);
+        assert!(result.is_err());
+        let err = result.unwrap_err();
+        assert!(err.contains("pay/pal"));
+        assert!(err.contains("invalid characters"));
+
+        let result = validate_custom_method_names(&["".to_string()]);
+        assert!(result.is_err());
+        let err = result.unwrap_err();
+        assert!(err.contains("empty"));
+    }
+
+    #[test]
+    fn test_validate_custom_method_names_unicode() {
+        // Unicode characters should fail (not ASCII alphanumeric)
+        assert!(validate_custom_method_names(&["café".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["北京".to_string()]).is_err());
+        assert!(validate_custom_method_names(&["🚀".to_string()]).is_err());
+    }
+
+    #[test]
+    fn test_validate_custom_method_names_empty_list() {
+        // Empty list should be valid (no methods to validate)
+        assert!(validate_custom_method_names(&[]).is_ok());
+    }
+
+    #[test]
+    fn test_create_custom_routers_method_list() {
+        // This test verifies the method list formatting
+        let custom_methods = vec!["paypal".to_string(), "venmo".to_string()];
+
+        let methods_str = custom_methods
+            .iter()
+            .map(|m| m.as_str())
+            .collect::<Vec<_>>()
+            .join(", ");
+
+        // Verify the method string is formatted correctly
+        assert!(methods_str.contains("paypal"));
+        assert!(methods_str.contains("venmo"));
+        assert_eq!(methods_str, "paypal, venmo");
+    }
+}

+ 26 - 48
crates/cdk-axum/src/lib.rs

@@ -19,8 +19,9 @@ mod metrics;
 
 #[cfg(feature = "auth")]
 mod auth;
-mod bolt12_router;
 pub mod cache;
+mod custom_handlers;
+mod custom_router;
 mod router_handlers;
 mod ws;
 
@@ -55,11 +56,6 @@ mod swagger_imports {
 #[cfg(feature = "swagger")]
 use swagger_imports::*;
 
-use crate::bolt12_router::{
-    cache_post_melt_bolt12, cache_post_mint_bolt12, get_check_mint_bolt12_quote,
-    post_melt_bolt12_quote, post_mint_bolt12_quote,
-};
-
 /// CDK Mint State
 #[derive(Clone)]
 pub struct MintState {
@@ -87,12 +83,6 @@ macro_rules! define_api_doc {
                 get_keyset_pubkeys,
                 get_keysets,
                 get_mint_info,
-                post_mint_bolt11_quote,
-                get_check_mint_bolt11_quote,
-                post_mint_bolt11,
-                post_melt_bolt11_quote,
-                get_check_melt_bolt11_quote,
-                post_melt_bolt11,
                 post_swap,
                 post_check,
                 post_restore
@@ -224,8 +214,11 @@ define_api_doc! {
 }
 
 /// Create mint [`Router`] with required endpoints for cashu mint with the default cache
-pub async fn create_mint_router(mint: Arc<Mint>, include_bolt12: bool) -> Result<Router> {
-    create_mint_router_with_custom_cache(mint, Default::default(), include_bolt12).await
+///
+/// The `custom_methods` parameter should include all custom payment methods supported
+/// by the payment processor, including "bolt11" and "bolt12" if they are supported.
+pub async fn create_mint_router(mint: Arc<Mint>, custom_methods: Vec<String>) -> Result<Router> {
+    create_mint_router_with_custom_cache(mint, Default::default(), custom_methods).await
 }
 
 async fn cors_middleware(
@@ -276,10 +269,13 @@ async fn cors_middleware(
 
 /// Create mint [`Router`] with required endpoints for cashu mint with a custom
 /// backend for cache
+///
+/// The `custom_methods` parameter should include all custom payment methods supported
+/// by the payment processor, including "bolt11" and "bolt12" if they are supported.
 pub async fn create_mint_router_with_custom_cache(
     mint: Arc<Mint>,
     cache: HttpCache,
-    include_bolt12: bool,
+    custom_methods: Vec<String>,
 ) -> Result<Router> {
     let state = MintState {
         mint,
@@ -291,19 +287,7 @@ pub async fn create_mint_router_with_custom_cache(
         .route("/keysets", get(get_keysets))
         .route("/keys/{keyset_id}", get(get_keyset_pubkeys))
         .route("/swap", post(cache_post_swap))
-        .route("/mint/quote/bolt11", post(post_mint_bolt11_quote))
-        .route(
-            "/mint/quote/bolt11/{quote_id}",
-            get(get_check_mint_bolt11_quote),
-        )
-        .route("/mint/bolt11", post(cache_post_mint_bolt11))
-        .route("/melt/quote/bolt11", post(post_melt_bolt11_quote))
         .route("/ws", get(ws_handler))
-        .route(
-            "/melt/quote/bolt11/{quote_id}",
-            get(get_check_melt_bolt11_quote),
-        )
-        .route("/melt/bolt11", post(cache_post_melt_bolt11))
         .route("/checkstate", post(post_check))
         .route("/info", get(get_mint_info))
         .route("/restore", post(post_restore));
@@ -316,10 +300,21 @@ pub async fn create_mint_router_with_custom_cache(
         mint_router.nest("/v1", auth_router)
     };
 
-    // Conditionally create and merge bolt12_router
-    let mint_router = if include_bolt12 {
-        let bolt12_router = create_bolt12_router(state.clone());
-        mint_router.nest("/v1", bolt12_router)
+    // Create and merge custom payment method routers
+    // This now includes bolt11 and bolt12 if they are in custom_methods
+    let mint_router = if !custom_methods.is_empty() {
+        // Validate custom method names
+        custom_router::validate_custom_method_names(&custom_methods)
+            .map_err(|e| anyhow::anyhow!("Invalid custom method names: {}", e))?;
+
+        tracing::info!(
+            "Creating routes for {} payment methods: {:?}",
+            custom_methods.len(),
+            custom_methods
+        );
+
+        let custom_router = custom_router::create_custom_routers(state.clone(), custom_methods);
+        mint_router.nest("/v1", custom_router)
     } else {
         mint_router
     };
@@ -335,20 +330,3 @@ pub async fn create_mint_router_with_custom_cache(
 
     Ok(mint_router)
 }
-
-fn create_bolt12_router(state: MintState) -> Router<MintState> {
-    Router::new()
-        .route("/melt/quote/bolt12", post(post_melt_bolt12_quote))
-        .route(
-            "/melt/quote/bolt12/{quote_id}",
-            get(get_check_melt_bolt11_quote),
-        )
-        .route("/melt/bolt12", post(cache_post_melt_bolt12))
-        .route("/mint/quote/bolt12", post(post_mint_bolt12_quote))
-        .route(
-            "/mint/quote/bolt12/{quote_id}",
-            get(get_check_mint_bolt12_quote),
-        )
-        .route("/mint/bolt12", post(cache_post_mint_bolt12))
-        .with_state(state)
-}

+ 3 - 311
crates/cdk-axum/src/router_handlers.rs

@@ -1,18 +1,14 @@
 use anyhow::Result;
 use axum::extract::ws::WebSocketUpgrade;
-use axum::extract::{FromRequestParts, Json, Path, State};
-use axum::http::request::Parts;
+use axum::extract::{Json, Path, State};
 use axum::http::StatusCode;
 use axum::response::{IntoResponse, Response};
 use cdk::error::{ErrorCode, ErrorResponse};
-use cdk::mint::QuoteId;
 #[cfg(feature = "auth")]
 use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
 use cdk::nuts::{
-    CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse,
-    MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
-    MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
-    SwapRequest, SwapResponse,
+    CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse, MintInfo,
+    RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
 };
 use cdk::util::unix_time;
 use paste::paste;
@@ -23,46 +19,6 @@ use crate::auth::AuthHeader;
 use crate::ws::main_websocket;
 use crate::MintState;
 
-const PREFER_HEADER_KEY: &str = "Prefer";
-
-/// Header extractor for the Prefer header
-///
-/// This extractor checks for the `Prefer: respond-async` header
-/// to determine if the client wants asynchronous processing
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct PreferHeader {
-    pub respond_async: bool,
-}
-
-impl<S> FromRequestParts<S> for PreferHeader
-where
-    S: Send + Sync,
-{
-    type Rejection = (StatusCode, String);
-
-    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
-        // Check for Prefer header
-        if let Some(prefer_value) = parts.headers.get(PREFER_HEADER_KEY) {
-            let value = prefer_value.to_str().map_err(|_| {
-                (
-                    StatusCode::BAD_REQUEST,
-                    "Invalid Prefer header value".to_string(),
-                )
-            })?;
-
-            // Check if it contains "respond-async"
-            let respond_async = value.to_lowercase().contains("respond-async");
-
-            return Ok(PreferHeader { respond_async });
-        }
-
-        // No Prefer header found - default to synchronous processing
-        Ok(PreferHeader {
-            respond_async: false,
-        })
-    }
-}
-
 /// Macro to add cache to endpoint
 #[macro_export]
 macro_rules! post_cache_wrapper {
@@ -144,12 +100,6 @@ macro_rules! post_cache_wrapper_with_prefer {
 }
 
 post_cache_wrapper!(post_swap, SwapRequest, SwapResponse);
-post_cache_wrapper!(post_mint_bolt11, MintRequest<QuoteId>, MintResponse);
-post_cache_wrapper_with_prefer!(
-    post_melt_bolt11,
-    MeltRequest<QuoteId>,
-    MeltQuoteBolt11Response<QuoteId>
-);
 
 #[cfg_attr(feature = "swagger", utoipa::path(
     get,
@@ -216,89 +166,6 @@ pub(crate) async fn get_keysets(
     Ok(Json(state.mint.keysets()))
 }
 
-#[cfg_attr(feature = "swagger", utoipa::path(
-    post,
-    context_path = "/v1",
-    path = "/mint/quote/bolt11",
-    request_body(content = MintQuoteBolt11Request, description = "Request params", content_type = "application/json"),
-    responses(
-        (status = 200, description = "Successful response", body = MintQuoteBolt11Response<String>, content_type = "application/json"),
-        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
-    )
-))]
-/// Request a quote for minting of new tokens
-///
-/// Request minting of new tokens. The mint responds with a Lightning invoice. This endpoint can be used for a Lightning invoice UX flow.
-#[instrument(skip_all, fields(amount = ?payload.amount))]
-pub(crate) async fn post_mint_bolt11_quote(
-    #[cfg(feature = "auth")] auth: AuthHeader,
-    State(state): State<MintState>,
-    Json(payload): Json<MintQuoteBolt11Request>,
-) -> Result<Json<MintQuoteBolt11Response<QuoteId>>, Response> {
-    #[cfg(feature = "auth")]
-    state
-        .mint
-        .verify_auth(
-            auth.into(),
-            &ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt11),
-        )
-        .await
-        .map_err(into_response)?;
-
-    let quote = state
-        .mint
-        .get_mint_quote(payload.into())
-        .await
-        .map_err(into_response)?;
-
-    Ok(Json(quote.try_into().map_err(into_response)?))
-}
-
-#[cfg_attr(feature = "swagger", utoipa::path(
-    get,
-    context_path = "/v1",
-    path = "/mint/quote/bolt11/{quote_id}",
-    params(
-        ("quote_id" = String, description = "The quote ID"),
-    ),
-    responses(
-        (status = 200, description = "Successful response", body = MintQuoteBolt11Response<String>, content_type = "application/json"),
-        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
-    )
-))]
-/// Get mint quote by ID
-///
-/// Get mint quote state.
-#[instrument(skip_all, fields(quote_id = ?quote_id))]
-pub(crate) async fn get_check_mint_bolt11_quote(
-    #[cfg(feature = "auth")] auth: AuthHeader,
-    State(state): State<MintState>,
-    Path(quote_id): Path<QuoteId>,
-) -> Result<Json<MintQuoteBolt11Response<QuoteId>>, Response> {
-    #[cfg(feature = "auth")]
-    {
-        state
-            .mint
-            .verify_auth(
-                auth.into(),
-                &ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
-            )
-            .await
-            .map_err(into_response)?;
-    }
-
-    let quote = state
-        .mint
-        .check_mint_quote(&quote_id)
-        .await
-        .map_err(|err| {
-            tracing::error!("Could not check mint quote {}: {}", quote_id, err);
-            into_response(err)
-        })?;
-
-    Ok(Json(quote.try_into().map_err(into_response)?))
-}
-
 #[instrument(skip_all)]
 pub(crate) async fn ws_handler(
     #[cfg(feature = "auth")] auth: AuthHeader,
@@ -320,181 +187,6 @@ pub(crate) async fn ws_handler(
     Ok(ws.on_upgrade(|ws| main_websocket(ws, state)))
 }
 
-/// Mint tokens by paying a BOLT11 Lightning invoice.
-///
-/// Requests the minting of tokens belonging to a paid payment request.
-///
-/// Call this endpoint after `POST /v1/mint/quote`.
-#[cfg_attr(feature = "swagger", utoipa::path(
-    post,
-    context_path = "/v1",
-    path = "/mint/bolt11",
-    request_body(content = MintRequest<String>, description = "Request params", content_type = "application/json"),
-    responses(
-        (status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
-        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
-    )
-))]
-#[instrument(skip_all, fields(quote_id = ?payload.quote))]
-pub(crate) async fn post_mint_bolt11(
-    #[cfg(feature = "auth")] auth: AuthHeader,
-    State(state): State<MintState>,
-    Json(payload): Json<MintRequest<QuoteId>>,
-) -> Result<Json<MintResponse>, Response> {
-    #[cfg(feature = "auth")]
-    {
-        state
-            .mint
-            .verify_auth(
-                auth.into(),
-                &ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11),
-            )
-            .await
-            .map_err(into_response)?;
-    }
-
-    let res = state
-        .mint
-        .process_mint_request(payload)
-        .await
-        .map_err(|err| {
-            tracing::error!("Could not process mint: {}", err);
-            into_response(err)
-        })?;
-
-    Ok(Json(res))
-}
-
-#[cfg_attr(feature = "swagger", utoipa::path(
-    post,
-    context_path = "/v1",
-    path = "/melt/quote/bolt11",
-    request_body(content = MeltQuoteBolt11Request, description = "Quote params", content_type = "application/json"),
-    responses(
-        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
-        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
-    )
-))]
-#[instrument(skip_all)]
-/// Request a quote for melting tokens
-pub(crate) async fn post_melt_bolt11_quote(
-    #[cfg(feature = "auth")] auth: AuthHeader,
-    State(state): State<MintState>,
-    Json(payload): Json<MeltQuoteBolt11Request>,
-) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
-    #[cfg(feature = "auth")]
-    {
-        state
-            .mint
-            .verify_auth(
-                auth.into(),
-                &ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11),
-            )
-            .await
-            .map_err(into_response)?;
-    }
-
-    let quote = state
-        .mint
-        .get_melt_quote(payload.into())
-        .await
-        .map_err(into_response)?;
-
-    Ok(Json(quote))
-}
-
-#[cfg_attr(feature = "swagger", utoipa::path(
-    get,
-    context_path = "/v1",
-    path = "/melt/quote/bolt11/{quote_id}",
-    params(
-        ("quote_id" = String, description = "The quote ID"),
-    ),
-    responses(
-        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
-        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
-    )
-))]
-/// Get melt quote by ID
-///
-/// Get melt quote state.
-#[instrument(skip_all, fields(quote_id = ?quote_id))]
-pub(crate) async fn get_check_melt_bolt11_quote(
-    #[cfg(feature = "auth")] auth: AuthHeader,
-    State(state): State<MintState>,
-    Path(quote_id): Path<QuoteId>,
-) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
-    #[cfg(feature = "auth")]
-    {
-        state
-            .mint
-            .verify_auth(
-                auth.into(),
-                &ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11),
-            )
-            .await
-            .map_err(into_response)?;
-    }
-
-    let quote = state
-        .mint
-        .check_melt_quote(&quote_id)
-        .await
-        .map_err(|err| {
-            tracing::error!("Could not check melt quote: {}", err);
-            into_response(err)
-        })?;
-
-    Ok(Json(quote))
-}
-
-#[cfg_attr(feature = "swagger", utoipa::path(
-    post,
-    context_path = "/v1",
-    path = "/melt/bolt11",
-    request_body(content = MeltRequest<String>, description = "Melt params", content_type = "application/json"),
-    responses(
-        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
-        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
-    )
-))]
-/// Melt tokens for a Bitcoin payment that the mint will make for the user in exchange
-///
-/// Requests tokens to be destroyed and sent out via Lightning.
-#[instrument(skip_all)]
-pub(crate) async fn post_melt_bolt11(
-    #[cfg(feature = "auth")] auth: AuthHeader,
-    prefer: PreferHeader,
-    State(state): State<MintState>,
-    Json(payload): Json<MeltRequest<QuoteId>>,
-) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
-    #[cfg(feature = "auth")]
-    {
-        state
-            .mint
-            .verify_auth(
-                auth.into(),
-                &ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11),
-            )
-            .await
-            .map_err(into_response)?;
-    }
-
-    let res = if prefer.respond_async {
-        // Asynchronous processing - return immediately after setup
-        state
-            .mint
-            .melt_async(&payload)
-            .await
-            .map_err(into_response)?
-    } else {
-        // Synchronous processing - wait for completion
-        state.mint.melt(&payload).await.map_err(into_response)?
-    };
-
-    Ok(Json(res))
-}
-
 #[cfg_attr(feature = "swagger", utoipa::path(
     post,
     context_path = "/v1",

+ 1 - 0
crates/cdk-cli/Cargo.toml

@@ -24,6 +24,7 @@ bitcoin.workspace = true
 cdk = { workspace = true, default-features = false, features = ["wallet", "auth", "nostr", "bip353"]}
 cdk-redb = { workspace = true, features = ["wallet"], optional = true }
 cdk-sqlite = { workspace = true, features = ["wallet"] }
+cdk-common = { workspace = true, features = ["wallet"] }
 clap.workspace = true
 serde.workspace = true
 serde_json.workspace = true

+ 19 - 3
crates/cdk-cli/src/sub_commands/mint.rs

@@ -7,6 +7,7 @@ use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::PaymentMethod;
 use cdk::wallet::MultiMintWallet;
 use cdk::{Amount, StreamExt};
+use cdk_common::nut00::KnownMethod;
 use clap::Args;
 use serde::{Deserialize, Serialize};
 
@@ -51,7 +52,7 @@ pub async fn mint(
 
     let quote = match &sub_command_args.quote_id {
         None => match payment_method {
-            PaymentMethod::Bolt11 => {
+            PaymentMethod::Known(KnownMethod::Bolt11) => {
                 let amount = sub_command_args
                     .amount
                     .ok_or(anyhow!("Amount must be defined"))?;
@@ -63,7 +64,7 @@ pub async fn mint(
 
                 quote
             }
-            PaymentMethod::Bolt12 => {
+            PaymentMethod::Known(KnownMethod::Bolt12) => {
                 let amount = sub_command_args.amount;
                 println!("{:?}", sub_command_args.single_use);
                 let quote = wallet
@@ -77,7 +78,22 @@ pub async fn mint(
                 quote
             }
             _ => {
-                todo!()
+                let amount = sub_command_args.amount;
+                println!("{:?}", sub_command_args.single_use);
+                let quote = wallet
+                    .mint_quote_unified(
+                        amount.map(|a| a.into()),
+                        payment_method.clone(),
+                        None,
+                        None,
+                    )
+                    .await?;
+
+                println!("Quote: {quote:#?}");
+
+                println!("Please pay: {}", quote.request);
+
+                quote
             }
         },
         Some(quote_id) => wallet

+ 33 - 12
crates/cdk-cln/src/lib.rs

@@ -17,9 +17,10 @@ use cdk_common::common::FeeReserve;
 use cdk_common::database::DynKVStore;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
-    self, Bolt11IncomingPaymentOptions, Bolt11Settings, Bolt12IncomingPaymentOptions,
+    self, Bolt11IncomingPaymentOptions, Bolt12IncomingPaymentOptions,
     CreateIncomingPaymentResponse, Event, IncomingPaymentOptions, MakePaymentResponse, MintPayment,
-    OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, WaitPaymentResponse,
+    OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, SettingsResponse,
+    WaitPaymentResponse,
 };
 use cdk_common::util::{hex, unix_time};
 use cdk_common::Bolt11Invoice;
@@ -35,7 +36,6 @@ use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny, Sha256};
 use cln_rpc::ClnRpc;
 use error::Error;
 use futures::{Stream, StreamExt};
-use serde_json::Value;
 use tokio_util::sync::CancellationToken;
 use tracing::instrument;
 use uuid::Uuid;
@@ -78,14 +78,18 @@ impl Cln {
 impl MintPayment for Cln {
     type Err = payment::Error;
 
-    async fn get_settings(&self) -> Result<Value, Self::Err> {
-        Ok(serde_json::to_value(Bolt11Settings {
-            mpp: true,
-            unit: CurrencyUnit::Msat,
-            invoice_description: true,
-            amountless: true,
-            bolt12: true,
-        })?)
+    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
+        use std::collections::HashMap;
+        Ok(SettingsResponse {
+            unit: CurrencyUnit::Msat.to_string(),
+            bolt11: Some(payment::Bolt11Settings {
+                mpp: true,
+                amountless: true,
+                invoice_description: true,
+            }),
+            bolt12: Some(payment::Bolt12Settings { amountless: true }),
+            custom: HashMap::new(),
+        })
     }
 
     /// Is wait invoice active
@@ -300,6 +304,9 @@ impl MintPayment for Cln {
         options: OutgoingPaymentOptions,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
         match options {
+            cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
+                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
+            }
             OutgoingPaymentOptions::Bolt11(bolt11_options) => {
                 // If we have specific amount options, use those
                 let amount_msat: Amount = if let Some(melt_options) = bolt11_options.melt_options {
@@ -467,8 +474,14 @@ impl MintPayment for Cln {
 
                 cln_response.invoice
             }
+            _ => {
+                max_fee_msat = None;
+                "".to_string()
+            }
         };
-
+        if invoice.is_empty() {
+            return Err(Error::UnknownInvoice.into());
+        }
         let cln_response = cln_client
             .call_typed(&PayRequest {
                 bolt11: invoice,
@@ -496,6 +509,9 @@ impl MintPayment for Cln {
                 };
 
                 let payment_identifier = match options {
+                    cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
+                        PaymentIdentifier::PaymentHash(*pay_response.payment_hash.as_ref())
+                    }
                     OutgoingPaymentOptions::Bolt11(_) => {
                         PaymentIdentifier::PaymentHash(*pay_response.payment_hash.as_ref())
                     }
@@ -532,6 +548,9 @@ impl MintPayment for Cln {
         options: IncomingPaymentOptions,
     ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
         match options {
+            cdk_common::payment::IncomingPaymentOptions::Custom(_) => {
+                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
+            }
             IncomingPaymentOptions::Bolt11(Bolt11IncomingPaymentOptions {
                 description,
                 amount,
@@ -569,6 +588,7 @@ impl MintPayment for Cln {
                     request_lookup_id: PaymentIdentifier::PaymentHash(*payment_hash.as_ref()),
                     request: request.to_string(),
                     expiry,
+                    extra_json: None,
                 })
             }
             IncomingPaymentOptions::Bolt12(bolt12_options) => {
@@ -619,6 +639,7 @@ impl MintPayment for Cln {
                     ),
                     request: offer_response.bolt12,
                     expiry: unix_expiry,
+                    extra_json: None,
                 })
             }
         }

+ 92 - 41
crates/cdk-common/src/database/mint/test/mint.rs

@@ -3,6 +3,7 @@
 use std::ops::Deref;
 use std::str::FromStr;
 
+use cashu::nut00::KnownMethod;
 use cashu::quote_id::QuoteId;
 use cashu::{Amount, BlindSignature, Id, SecretKey};
 
@@ -27,10 +28,11 @@ where
         None,
         0.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt12,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
         vec![],
+        None,
     );
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
@@ -53,10 +55,11 @@ where
         None,
         0.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt12,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
         vec![],
+        None,
     );
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     assert!(tx.add_mint_quote(mint_quote.clone()).await.is_ok());
@@ -82,10 +85,11 @@ where
         None,
         0.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt12,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
         vec![],
+        None,
     );
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
@@ -141,10 +145,11 @@ where
         None,
         0.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt12,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
         vec![],
+        None,
     );
 
     let p1 = unique_string();
@@ -205,10 +210,11 @@ where
         None,
         0.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt12,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
         vec![],
+        None,
     );
 
     let p1 = unique_string();
@@ -250,10 +256,11 @@ where
         None,
         0.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt12,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
         vec![],
+        None,
     );
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
@@ -298,10 +305,11 @@ where
         None,
         0.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt12,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
         vec![],
+        None,
     );
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
@@ -325,10 +333,11 @@ where
         None,
         0.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt12,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
         vec![],
+        None,
     );
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
@@ -360,10 +369,11 @@ where
         None,
         0.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt12,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
         vec![],
+        None,
     );
 
     let p1 = unique_string();
@@ -392,10 +402,11 @@ where
         None,
         0.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt12,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
         vec![],
+        None,
     );
 
     let p1 = unique_string();
@@ -437,7 +448,7 @@ where
     let blinded_messages = vec![blinded_message];
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
-    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
+    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
     tx.add_melt_quote(quote.clone()).await.unwrap();
     tx.add_melt_request(&quote.id, inputs_amount, inputs_fee)
         .await
@@ -445,7 +456,11 @@ where
     tx.add_blinded_messages(
         Some(&quote.id),
         &blinded_messages,
-        &Operation::new_melt(Amount::ZERO, Amount::ZERO, cashu::PaymentMethod::Bolt11),
+        &Operation::new_melt(
+            Amount::ZERO,
+            Amount::ZERO,
+            cashu::PaymentMethod::Known(KnownMethod::Bolt11),
+        ),
     )
     .await
     .unwrap();
@@ -502,7 +517,7 @@ where
 
     // Now try to add melt request with the same blinded message - should fail due to constraint
     let mut tx = Database::begin_transaction(&db).await.unwrap();
-    let quote2 = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
+    let quote2 = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
     tx.add_melt_quote(quote2.clone()).await.unwrap();
     tx.add_melt_request(&quote2.id, inputs_amount, inputs_fee)
         .await
@@ -511,7 +526,11 @@ where
         .add_blinded_messages(
             Some(&quote2.id),
             &blinded_messages,
-            &Operation::new_melt(Amount::ZERO, Amount::ZERO, cashu::PaymentMethod::Bolt11),
+            &Operation::new_melt(
+                Amount::ZERO,
+                Amount::ZERO,
+                cashu::PaymentMethod::Known(KnownMethod::Bolt11),
+            ),
         )
         .await;
     assert!(result.is_err() && matches!(result.unwrap_err(), Error::Duplicate));
@@ -539,7 +558,7 @@ where
 
     // First insert succeeds
     let mut tx = Database::begin_transaction(&db).await.unwrap();
-    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
+    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
     tx.add_melt_quote(quote.clone()).await.unwrap();
     tx.add_melt_request(&quote.id, inputs_amount, inputs_fee)
         .await
@@ -548,7 +567,11 @@ where
         .add_blinded_messages(
             Some(&quote.id),
             &blinded_messages,
-            &Operation::new_melt(Amount::ZERO, Amount::ZERO, cashu::PaymentMethod::Bolt11)
+            &Operation::new_melt(
+                Amount::ZERO,
+                Amount::ZERO,
+                cashu::PaymentMethod::Known(KnownMethod::Bolt11)
+            )
         )
         .await
         .is_ok());
@@ -556,7 +579,7 @@ where
 
     // Second insert with same blinded_message but different quote_id should fail due to unique constraint on blinded_message
     let mut tx = Database::begin_transaction(&db).await.unwrap();
-    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
+    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
     tx.add_melt_quote(quote.clone()).await.unwrap();
     tx.add_melt_request(&quote.id, inputs_amount, inputs_fee)
         .await
@@ -565,7 +588,11 @@ where
         .add_blinded_messages(
             Some(&quote.id),
             &blinded_messages,
-            &Operation::new_melt(Amount::ZERO, Amount::ZERO, cashu::PaymentMethod::Bolt11),
+            &Operation::new_melt(
+                Amount::ZERO,
+                Amount::ZERO,
+                cashu::PaymentMethod::Known(KnownMethod::Bolt11),
+            ),
         )
         .await;
     // Expect a database error due to unique violation
@@ -594,7 +621,7 @@ where
 
     // Insert melt request
     let mut tx1 = Database::begin_transaction(&db).await.unwrap();
-    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
+    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
     tx1.add_melt_quote(quote.clone()).await.unwrap();
     tx1.add_melt_request(&quote.id, inputs_amount, inputs_fee)
         .await
@@ -602,7 +629,11 @@ where
     tx1.add_blinded_messages(
         Some(&quote.id),
         &blinded_messages,
-        &Operation::new_melt(Amount::ZERO, Amount::ZERO, cashu::PaymentMethod::Bolt11),
+        &Operation::new_melt(
+            Amount::ZERO,
+            Amount::ZERO,
+            cashu::PaymentMethod::Known(KnownMethod::Bolt11),
+        ),
     )
     .await
     .unwrap();
@@ -643,7 +674,7 @@ where
         0,
         None,
         None,
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
     );
 
     // Add melt quote
@@ -675,7 +706,7 @@ where
         0,
         None,
         None,
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
     );
 
     // Add first melt quote
@@ -706,7 +737,7 @@ where
         0,
         None,
         None,
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
     );
 
     // Add melt quote
@@ -758,7 +789,7 @@ where
         0,
         Some(PaymentIdentifier::CustomId("old_lookup_id".to_string())),
         None,
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
     );
 
     // Add melt quote
@@ -797,10 +828,11 @@ where
         None,
         100.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
         vec![],
+        None,
     );
 
     let quote2 = MintQuote::new(
@@ -813,10 +845,11 @@ where
         None,
         200.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
         vec![],
+        None,
     );
 
     // Add quotes
@@ -847,7 +880,7 @@ where
         0,
         None,
         None,
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
     );
 
     let quote2 = MeltQuote::new(
@@ -860,7 +893,7 @@ where
         0,
         None,
         None,
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
     );
 
     // Add quotes
@@ -894,10 +927,11 @@ where
         None,
         100.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
         vec![],
+        None,
     );
 
     // Add quote
@@ -931,10 +965,11 @@ where
         None,
         100.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
         vec![],
+        None,
     );
 
     // Add quote
@@ -985,7 +1020,10 @@ where
     tx.add_blinded_messages(
         None,
         &blinded_messages,
-        &Operation::new_mint(Amount::ZERO, cashu::PaymentMethod::Bolt11),
+        &Operation::new_mint(
+            Amount::ZERO,
+            cashu::PaymentMethod::Known(KnownMethod::Bolt11),
+        ),
     )
     .await
     .unwrap();
@@ -1004,7 +1042,10 @@ where
         .add_blinded_messages(
             None,
             &[blinded_message1],
-            &Operation::new_mint(Amount::ZERO, cashu::PaymentMethod::Bolt11)
+            &Operation::new_mint(
+                Amount::ZERO,
+                cashu::PaymentMethod::Known(KnownMethod::Bolt11)
+            )
         )
         .await
         .is_ok());
@@ -1012,7 +1053,10 @@ where
         .add_blinded_messages(
             None,
             &[blinded_message2],
-            &Operation::new_mint(Amount::ZERO, cashu::PaymentMethod::Bolt11)
+            &Operation::new_mint(
+                Amount::ZERO,
+                cashu::PaymentMethod::Known(KnownMethod::Bolt11)
+            )
         )
         .await
         .is_err());
@@ -1036,10 +1080,11 @@ where
         None,
         1000.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
         vec![],
+        None,
     );
 
     // Add quote
@@ -1097,10 +1142,11 @@ where
         None,
         1000.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
         vec![],
+        None,
     );
 
     // Add quote
@@ -1167,10 +1213,11 @@ where
         None,
         100.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
         vec![],
+        None,
     );
 
     // Add quote
@@ -1203,7 +1250,7 @@ where
         0,
         None,
         None,
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
     );
 
     // Add quote
@@ -1239,10 +1286,11 @@ where
         None,
         100.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
         vec![],
+        None,
     );
 
     // Add quote
@@ -1278,10 +1326,11 @@ where
         None,
         100.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
         vec![],
+        None,
     );
 
     // Add quote
@@ -1354,10 +1403,11 @@ where
         None,
         1000.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
         vec![],
+        None,
     );
 
     // Add quote
@@ -1437,10 +1487,11 @@ where
         None,
         1000.into(),
         0.into(),
-        cashu::PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
         vec![],
+        None,
     );
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();

+ 4 - 3
crates/cdk-common/src/database/wallet/test/mod.rs

@@ -10,8 +10,9 @@ use std::str::FromStr;
 use std::sync::atomic::{AtomicU64, Ordering};
 use std::time::{SystemTime, UNIX_EPOCH};
 
+use cashu::nut00::KnownMethod;
 use cashu::secret::Secret;
-use cashu::{Amount, CurrencyUnit, PaymentMethod, SecretKey};
+use cashu::{Amount, CurrencyUnit, SecretKey};
 
 use super::*;
 use crate::common::ProofInfo;
@@ -106,7 +107,7 @@ fn test_mint_quote(mint_url: MintUrl) -> MintQuote {
     MintQuote::new(
         unique_id(),
         mint_url,
-        PaymentMethod::Bolt11,
+        cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         Some(Amount::from(1000)),
         CurrencyUnit::Sat,
         "lnbc1000...".to_string(),
@@ -126,7 +127,7 @@ fn test_melt_quote() -> MeltQuote {
         state: cashu::MeltQuoteState::Unpaid,
         expiry: 9999999999,
         payment_preimage: None,
-        payment_method: PaymentMethod::Bolt11,
+        payment_method: cashu::PaymentMethod::Known(KnownMethod::Bolt11),
     }
 }
 

+ 10 - 2
crates/cdk-common/src/melt.rs

@@ -1,16 +1,18 @@
 //! Melt types
-use cashu::{MeltQuoteBolt11Request, MeltQuoteBolt12Request};
+use cashu::{MeltQuoteBolt11Request, MeltQuoteBolt12Request, MeltQuoteCustomRequest};
 
 /// Melt quote request enum for different types of quotes
 ///
 /// This enum represents the different types of melt quote requests
-/// that can be made, either BOLT11 or BOLT12.
+/// that can be made, either BOLT11, BOLT12, or Custom.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum MeltQuoteRequest {
     /// Lightning Network BOLT11 invoice request
     Bolt11(MeltQuoteBolt11Request),
     /// Lightning Network BOLT12 offer request
     Bolt12(MeltQuoteBolt12Request),
+    /// Custom payment method request
+    Custom(MeltQuoteCustomRequest),
 }
 
 impl From<MeltQuoteBolt11Request> for MeltQuoteRequest {
@@ -24,3 +26,9 @@ impl From<MeltQuoteBolt12Request> for MeltQuoteRequest {
         MeltQuoteRequest::Bolt12(request)
     }
 }
+
+impl From<MeltQuoteCustomRequest> for MeltQuoteRequest {
+    fn from(request: MeltQuoteCustomRequest) -> Self {
+        MeltQuoteRequest::Custom(request)
+    }
+}

+ 43 - 2
crates/cdk-common/src/mint.rs

@@ -406,11 +406,16 @@ pub struct MintQuote {
     #[serde(default)]
     pub payments: Vec<IncomingPayment>,
     /// Payment Method
-    #[serde(default)]
     pub payment_method: PaymentMethod,
     /// Payment of payment(s) that filled quote
     #[serde(default)]
     pub issuance: Vec<Issuance>,
+    /// Extra payment-method-specific fields
+    ///
+    /// These fields are flattened into the JSON representation, allowing
+    /// custom payment methods to include additional data without nesting.
+    #[serde(flatten, default)]
+    pub extra_json: Option<serde_json::Value>,
     /// Accumulated changes since this quote was loaded or created.
     ///
     /// This field is not serialized and is used internally to track modifications
@@ -437,6 +442,7 @@ impl MintQuote {
         created_time: u64,
         payments: Vec<IncomingPayment>,
         issuance: Vec<Issuance>,
+        extra_json: Option<serde_json::Value>,
     ) -> Self {
         let id = id.unwrap_or_else(QuoteId::new_uuid);
 
@@ -454,6 +460,7 @@ impl MintQuote {
             payment_method,
             payments,
             issuance,
+            extra_json,
             changes: None,
         }
     }
@@ -698,7 +705,6 @@ pub struct MeltQuote {
     /// Unix time quote was paid
     pub paid_time: Option<u64>,
     /// Payment method
-    #[serde(default)]
     pub payment_method: PaymentMethod,
 }
 
@@ -826,6 +832,33 @@ impl TryFrom<MintQuote> for MintQuoteBolt12Response<String> {
     }
 }
 
+impl TryFrom<crate::mint::MintQuote> for crate::nuts::MintQuoteCustomResponse<QuoteId> {
+    type Error = crate::Error;
+
+    fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
+        Ok(crate::nuts::MintQuoteCustomResponse {
+            state: mint_quote.state(),
+            quote: mint_quote.id.clone(),
+            request: mint_quote.request,
+            expiry: Some(mint_quote.expiry),
+            pubkey: mint_quote.pubkey,
+            amount: mint_quote.amount,
+            unit: Some(mint_quote.unit),
+            extra: mint_quote.extra_json.unwrap_or_default(),
+        })
+    }
+}
+
+impl TryFrom<MintQuote> for crate::nuts::MintQuoteCustomResponse<String> {
+    type Error = crate::Error;
+
+    fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
+        let quote: crate::nuts::MintQuoteCustomResponse<QuoteId> = quote.try_into()?;
+
+        Ok(quote.into())
+    }
+}
+
 impl From<&MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
     fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
         MeltQuoteBolt11Response {
@@ -872,6 +905,13 @@ pub enum MeltPaymentRequest {
         #[serde(with = "offer_serde")]
         offer: Box<Offer>,
     },
+    /// Custom payment method
+    Custom {
+        /// Payment method name
+        method: String,
+        /// Payment request string
+        request: String,
+    },
 }
 
 impl std::fmt::Display for MeltPaymentRequest {
@@ -879,6 +919,7 @@ impl std::fmt::Display for MeltPaymentRequest {
         match self {
             MeltPaymentRequest::Bolt11 { bolt11 } => write!(f, "{bolt11}"),
             MeltPaymentRequest::Bolt12 { offer } => write!(f, "{offer}"),
+            MeltPaymentRequest::Custom { request, .. } => write!(f, "{request}"),
         }
     }
 }

+ 90 - 16
crates/cdk-common/src/payment.rs

@@ -173,6 +173,24 @@ pub struct Bolt12IncomingPaymentOptions {
     pub unix_expiry: Option<u64>,
 }
 
+/// Options for creating a custom incoming payment request
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct CustomIncomingPaymentOptions {
+    /// Payment method name (e.g., "paypal", "venmo")
+    pub method: String,
+    /// Optional description for the payment request
+    pub description: Option<String>,
+    /// Amount for the payment request
+    pub amount: Amount,
+    /// Optional expiry time as Unix timestamp in seconds
+    pub unix_expiry: Option<u64>,
+    /// Extra payment-method-specific fields as JSON string
+    ///
+    /// These fields are passed through to the payment processor for
+    /// method-specific validation (e.g., ehash share).
+    pub extra_json: Option<String>,
+}
+
 /// Options for creating an incoming payment request
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub enum IncomingPaymentOptions {
@@ -180,6 +198,8 @@ pub enum IncomingPaymentOptions {
     Bolt11(Bolt11IncomingPaymentOptions),
     /// BOLT12 payment request options
     Bolt12(Box<Bolt12IncomingPaymentOptions>),
+    /// Custom payment method options
+    Custom(Box<CustomIncomingPaymentOptions>),
 }
 
 /// Options for BOLT11 outgoing payments
@@ -208,6 +228,26 @@ pub struct Bolt12OutgoingPaymentOptions {
     pub melt_options: Option<MeltOptions>,
 }
 
+/// Options for custom outgoing payments
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct CustomOutgoingPaymentOptions {
+    /// Payment method name
+    pub method: String,
+    /// Payment request string (method-specific format)
+    pub request: String,
+    /// Maximum fee amount allowed for the payment
+    pub max_fee_amount: Option<Amount>,
+    /// Optional timeout in seconds
+    pub timeout_secs: Option<u64>,
+    /// Melt options
+    pub melt_options: Option<MeltOptions>,
+    /// Extra payment-method-specific fields as JSON string
+    ///
+    /// These fields are passed through to the payment processor for
+    /// method-specific validation.
+    pub extra_json: Option<String>,
+}
+
 /// Options for creating an outgoing payment
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub enum OutgoingPaymentOptions {
@@ -215,6 +255,8 @@ pub enum OutgoingPaymentOptions {
     Bolt11(Box<Bolt11OutgoingPaymentOptions>),
     /// BOLT12 payment options
     Bolt12(Box<Bolt12OutgoingPaymentOptions>),
+    /// Custom payment method options
+    Custom(Box<CustomOutgoingPaymentOptions>),
 }
 
 impl TryFrom<crate::mint::MeltQuote> for OutgoingPaymentOptions {
@@ -246,6 +288,16 @@ impl TryFrom<crate::mint::MeltQuote> for OutgoingPaymentOptions {
                     },
                 )))
             }
+            MeltPaymentRequest::Custom { method, request } => Ok(OutgoingPaymentOptions::Custom(
+                Box::new(CustomOutgoingPaymentOptions {
+                    method,
+                    request,
+                    max_fee_amount: Some(melt_quote.fee_reserve),
+                    timeout_secs: None,
+                    melt_options: melt_quote.options,
+                    extra_json: None,
+                }),
+            )),
         }
     }
 }
@@ -271,7 +323,7 @@ pub trait MintPayment {
     }
 
     /// Base Settings
-    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err>;
+    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err>;
 
     /// Create a new invoice
     async fn create_incoming_payment_request(
@@ -364,6 +416,12 @@ pub struct CreateIncomingPaymentResponse {
     pub request: String,
     /// Unix Expiry of Invoice
     pub expiry: Option<u64>,
+    /// Extra payment-method-specific fields
+    ///
+    /// These fields are flattened into the JSON representation, allowing
+    /// custom payment methods to include additional data without nesting.
+    #[serde(flatten, default)]
+    pub extra_json: Option<serde_json::Value>,
 }
 
 /// Payment response
@@ -396,30 +454,46 @@ pub struct PaymentQuoteResponse {
     pub state: MeltQuoteState,
 }
 
-/// Ln backend settings
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+/// BOLT11 settings
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
 pub struct Bolt11Settings {
-    /// MPP supported
+    /// Multi-part payment (MPP) supported
     pub mpp: bool,
-    /// Base unit of backend
-    pub unit: CurrencyUnit,
-    /// Invoice Description supported
+    /// Amountless invoice support
+    pub amountless: bool,
+    /// Invoice description supported
     pub invoice_description: bool,
-    /// Paying amountless invoices supported
+}
+
+/// BOLT12 settings
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
+pub struct Bolt12Settings {
+    /// Amountless offer support
     pub amountless: bool,
-    /// Bolt12 supported
-    pub bolt12: bool,
 }
 
-impl TryFrom<Bolt11Settings> for Value {
-    type Error = crate::error::Error;
+/// Payment processor settings response
+/// Mirrors the proto SettingsResponse structure
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct SettingsResponse {
+    /// Base unit of backend
+    pub unit: String,
+    /// BOLT11 settings (None if not supported)
+    pub bolt11: Option<Bolt11Settings>,
+    /// BOLT12 settings (None if not supported)
+    pub bolt12: Option<Bolt12Settings>,
+    /// Custom payment methods settings (method name -> settings data)
+    #[serde(default)]
+    pub custom: std::collections::HashMap<String, String>,
+}
 
-    fn try_from(value: Bolt11Settings) -> Result<Self, Self::Error> {
-        serde_json::to_value(value).map_err(|err| err.into())
+impl From<SettingsResponse> for Value {
+    fn from(value: SettingsResponse) -> Self {
+        serde_json::to_value(value).unwrap_or(Value::Null)
     }
 }
 
-impl TryFrom<Value> for Bolt11Settings {
+impl TryFrom<Value> for SettingsResponse {
     type Error = crate::error::Error;
 
     fn try_from(value: Value) -> Result<Self, Self::Error> {
@@ -466,7 +540,7 @@ where
 {
     type Err = T::Err;
 
-    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
+    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
         let start = std::time::Instant::now();
         METRICS.inc_in_flight_requests("get_settings");
 

+ 0 - 2
crates/cdk-common/src/wallet.rs

@@ -43,7 +43,6 @@ pub struct MintQuote {
     /// Mint Url
     pub mint_url: MintUrl,
     /// Payment method
-    #[serde(default)]
     pub payment_method: PaymentMethod,
     /// Amount of quote
     pub amount: Option<Amount>,
@@ -85,7 +84,6 @@ pub struct MeltQuote {
     /// Payment preimage
     pub payment_preimage: Option<String>,
     /// Payment method
-    #[serde(default)]
     pub payment_method: PaymentMethod,
 }
 

+ 27 - 12
crates/cdk-fake-wallet/src/lib.rs

@@ -26,9 +26,9 @@ use cdk_common::common::FeeReserve;
 use cdk_common::ensure_cdk;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
-    self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
-    MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
-    PaymentQuoteResponse, WaitPaymentResponse,
+    self, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions, MakePaymentResponse,
+    MintPayment, OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, SettingsResponse,
+    WaitPaymentResponse,
 };
 use error::Error;
 use futures::stream::StreamExt;
@@ -36,7 +36,6 @@ use futures::Stream;
 use lightning::offers::offer::OfferBuilder;
 use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
 use serde::{Deserialize, Serialize};
-use serde_json::Value;
 use tokio::sync::{Mutex, RwLock};
 use tokio::time;
 use tokio_stream::wrappers::ReceiverStream;
@@ -416,14 +415,17 @@ impl MintPayment for FakeWallet {
     type Err = payment::Error;
 
     #[instrument(skip_all)]
-    async fn get_settings(&self) -> Result<Value, Self::Err> {
-        Ok(serde_json::to_value(Bolt11Settings {
-            mpp: true,
-            unit: self.unit.clone(),
-            invoice_description: true,
-            amountless: false,
-            bolt12: true,
-        })?)
+    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
+        Ok(SettingsResponse {
+            unit: self.unit.to_string(),
+            bolt11: Some(payment::Bolt11Settings {
+                mpp: true,
+                amountless: false,
+                invoice_description: true,
+            }),
+            bolt12: Some(payment::Bolt12Settings { amountless: false }),
+            custom: std::collections::HashMap::new(),
+        })
     }
 
     #[instrument(skip_all)]
@@ -502,6 +504,10 @@ impl MintPayment for FakeWallet {
                 };
                 (amount_msat, None)
             }
+            OutgoingPaymentOptions::Custom(_) => {
+                // Custom payment methods are not supported by fake wallet
+                return Err(cdk_common::payment::Error::UnsupportedPaymentOption);
+            }
         };
 
         let amount = convert_currency_amount(
@@ -628,6 +634,10 @@ impl MintPayment for FakeWallet {
                     unit: unit.clone(),
                 })
             }
+            OutgoingPaymentOptions::Custom(_) => {
+                // Custom payment methods are not supported by fake wallet
+                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
+            }
         }
     }
 
@@ -696,6 +706,10 @@ impl MintPayment for FakeWallet {
                     expiry,
                 )
             }
+            IncomingPaymentOptions::Custom(_) => {
+                // Custom payment methods are not supported by fake wallet
+                return Err(cdk_common::payment::Error::UnsupportedPaymentOption);
+            }
         };
 
         // ALL invoices get immediate payment processing (original behavior)
@@ -756,6 +770,7 @@ impl MintPayment for FakeWallet {
             request_lookup_id: payment_hash,
             request,
             expiry,
+            extra_json: None,
         })
     }
 

+ 41 - 21
crates/cdk-ffi/src/types/mint.rs

@@ -2,6 +2,7 @@
 
 use std::str::FromStr;
 
+use cdk::nuts::nut00::{KnownMethod, PaymentMethod as NutPaymentMethod};
 use serde::{Deserialize, Serialize};
 
 use super::amount::{Amount, CurrencyUnit};
@@ -192,10 +193,11 @@ impl TryFrom<MintMethodSettings> for cdk::nuts::nut04::MintMethodSettings {
     type Error = FfiError;
 
     fn try_from(s: MintMethodSettings) -> Result<Self, Self::Error> {
-        let options = match (s.method.clone(), s.description) {
-            (PaymentMethod::Bolt11, Some(description)) => {
-                Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 { description })
-            }
+        let options = match s.method {
+            PaymentMethod::Bolt11 => s
+                .description
+                .map(|description| cdk::nuts::nut04::MintMethodOptions::Bolt11 { description }),
+            PaymentMethod::Custom { .. } => Some(cdk::nuts::nut04::MintMethodOptions::Custom {}),
             _ => None,
         };
         Ok(Self {
@@ -270,10 +272,10 @@ impl TryFrom<MeltMethodSettings> for cdk::nuts::nut05::MeltMethodSettings {
     type Error = FfiError;
 
     fn try_from(s: MeltMethodSettings) -> Result<Self, Self::Error> {
-        let options = match (s.method.clone(), s.amountless) {
-            (PaymentMethod::Bolt11, Some(amountless)) => {
-                Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless })
-            }
+        let options = match s.method {
+            PaymentMethod::Bolt11 => s
+                .amountless
+                .map(|amountless| cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless }),
             _ => None,
         };
         Ok(Self {
@@ -435,19 +437,35 @@ impl TryFrom<ProtectedEndpoint> for cdk::nuts::ProtectedEndpoint {
 
         // Convert path string to RoutePath by matching against known paths
         let route_path = match endpoint.path.as_str() {
-            "/v1/mint/quote/bolt11" => cdk::nuts::RoutePath::MintQuoteBolt11,
-            "/v1/mint/bolt11" => cdk::nuts::RoutePath::MintBolt11,
-            "/v1/melt/quote/bolt11" => cdk::nuts::RoutePath::MeltQuoteBolt11,
-            "/v1/melt/bolt11" => cdk::nuts::RoutePath::MeltBolt11,
+            "/v1/mint/quote/bolt11" => cdk::nuts::RoutePath::MintQuote(
+                NutPaymentMethod::Known(KnownMethod::Bolt11).to_string(),
+            ),
+            "/v1/mint/bolt11" => {
+                cdk::nuts::RoutePath::Mint(NutPaymentMethod::Known(KnownMethod::Bolt11).to_string())
+            }
+            "/v1/melt/quote/bolt11" => cdk::nuts::RoutePath::MeltQuote(
+                NutPaymentMethod::Known(KnownMethod::Bolt11).to_string(),
+            ),
+            "/v1/melt/bolt11" => {
+                cdk::nuts::RoutePath::Melt(NutPaymentMethod::Known(KnownMethod::Bolt11).to_string())
+            }
             "/v1/swap" => cdk::nuts::RoutePath::Swap,
             "/v1/ws" => cdk::nuts::RoutePath::Ws,
             "/v1/checkstate" => cdk::nuts::RoutePath::Checkstate,
             "/v1/restore" => cdk::nuts::RoutePath::Restore,
             "/v1/auth/blind/mint" => cdk::nuts::RoutePath::MintBlindAuth,
-            "/v1/mint/quote/bolt12" => cdk::nuts::RoutePath::MintQuoteBolt12,
-            "/v1/mint/bolt12" => cdk::nuts::RoutePath::MintBolt12,
-            "/v1/melt/quote/bolt12" => cdk::nuts::RoutePath::MeltQuoteBolt12,
-            "/v1/melt/bolt12" => cdk::nuts::RoutePath::MeltBolt12,
+            "/v1/mint/quote/bolt12" => cdk::nuts::RoutePath::MintQuote(
+                NutPaymentMethod::Known(KnownMethod::Bolt12).to_string(),
+            ),
+            "/v1/mint/bolt12" => {
+                cdk::nuts::RoutePath::Mint(NutPaymentMethod::Known(KnownMethod::Bolt12).to_string())
+            }
+            "/v1/melt/quote/bolt12" => cdk::nuts::RoutePath::MeltQuote(
+                NutPaymentMethod::Known(KnownMethod::Bolt12).to_string(),
+            ),
+            "/v1/melt/bolt12" => {
+                cdk::nuts::RoutePath::Melt(NutPaymentMethod::Known(KnownMethod::Bolt12).to_string())
+            }
             _ => {
                 return Err(FfiError::Generic {
                     msg: format!("Unknown route path: {}", endpoint.path),
@@ -683,7 +701,7 @@ mod tests {
         cdk::nuts::Nuts {
             nut04: cdk::nuts::nut04::Settings {
                 methods: vec![cdk::nuts::nut04::MintMethodSettings {
-                    method: cdk::nuts::PaymentMethod::Bolt11,
+                    method: cdk::nuts::PaymentMethod::Known(KnownMethod::Bolt11),
                     unit: cdk::nuts::CurrencyUnit::Sat,
                     min_amount: Some(cdk::Amount::from(1)),
                     max_amount: Some(cdk::Amount::from(100000)),
@@ -695,7 +713,7 @@ mod tests {
             },
             nut05: cdk::nuts::nut05::Settings {
                 methods: vec![cdk::nuts::nut05::MeltMethodSettings {
-                    method: cdk::nuts::PaymentMethod::Bolt11,
+                    method: cdk::nuts::PaymentMethod::Known(KnownMethod::Bolt11),
                     unit: cdk::nuts::CurrencyUnit::Sat,
                     min_amount: Some(cdk::Amount::from(1)),
                     max_amount: Some(cdk::Amount::from(100000)),
@@ -727,7 +745,9 @@ mod tests {
                 bat_max_mint: 100,
                 protected_endpoints: vec![cdk::nuts::ProtectedEndpoint::new(
                     cdk::nuts::Method::Post,
-                    cdk::nuts::RoutePath::MintBolt11,
+                    cdk::nuts::RoutePath::Mint(
+                        NutPaymentMethod::Known(KnownMethod::Bolt11).to_string(),
+                    ),
                 )],
             }),
         }
@@ -948,7 +968,7 @@ mod tests {
             .nut04
             .methods
             .push(cdk::nuts::nut04::MintMethodSettings {
-                method: cdk::nuts::PaymentMethod::Bolt11,
+                method: cdk::nuts::PaymentMethod::Known(KnownMethod::Bolt11),
                 unit: cdk::nuts::CurrencyUnit::Msat,
                 min_amount: Some(cdk::Amount::from(1)),
                 max_amount: Some(cdk::Amount::from(100000)),
@@ -959,7 +979,7 @@ mod tests {
             .nut05
             .methods
             .push(cdk::nuts::nut05::MeltMethodSettings {
-                method: cdk::nuts::PaymentMethod::Bolt11,
+                method: cdk::nuts::PaymentMethod::Known(KnownMethod::Bolt11),
                 unit: cdk::nuts::CurrencyUnit::Usd,
                 min_amount: None,
                 max_amount: None,

+ 109 - 7
crates/cdk-ffi/src/types/quote.rs

@@ -144,6 +144,54 @@ impl From<cdk::nuts::MintQuoteBolt11Response<String>> for MintQuoteBolt11Respons
     }
 }
 
+/// FFI-compatible MintQuoteCustomResponse
+///
+/// This is a unified response type for custom payment methods that includes
+/// extra fields for method-specific data (e.g., ehash share).
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct MintQuoteCustomResponse {
+    /// Quote ID
+    pub quote: String,
+    /// Request string
+    pub request: String,
+    /// State of the quote
+    pub state: QuoteState,
+    /// Expiry timestamp (optional)
+    pub expiry: Option<u64>,
+    /// Amount (optional)
+    pub amount: Option<Amount>,
+    /// Unit (optional)
+    pub unit: Option<CurrencyUnit>,
+    /// Pubkey (optional)
+    pub pubkey: Option<String>,
+    /// Extra payment-method-specific fields as JSON string
+    ///
+    /// These fields are flattened into the JSON representation, allowing
+    /// custom payment methods to include additional data without nesting.
+    pub extra: Option<String>,
+}
+
+impl From<cdk::nuts::MintQuoteCustomResponse<String>> for MintQuoteCustomResponse {
+    fn from(response: cdk::nuts::MintQuoteCustomResponse<String>) -> Self {
+        let extra = if response.extra.is_null() {
+            None
+        } else {
+            Some(response.extra.to_string())
+        };
+
+        Self {
+            quote: response.quote,
+            request: response.request,
+            state: response.state.into(),
+            expiry: response.expiry,
+            amount: response.amount.map(Into::into),
+            unit: response.unit.map(Into::into),
+            pubkey: response.pubkey.map(|p| p.to_string()),
+            extra,
+        }
+    }
+}
+
 /// FFI-compatible MeltQuoteBolt11Response
 #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
 pub struct MeltQuoteBolt11Response {
@@ -179,6 +227,58 @@ impl From<cdk::nuts::MeltQuoteBolt11Response<String>> for MeltQuoteBolt11Respons
         }
     }
 }
+
+/// FFI-compatible MeltQuoteCustomResponse
+///
+/// This is a unified response type for custom payment methods that includes
+/// extra fields for method-specific data.
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct MeltQuoteCustomResponse {
+    /// Quote ID
+    pub quote: String,
+    /// Amount
+    pub amount: Amount,
+    /// Fee reserve
+    pub fee_reserve: Amount,
+    /// State of the quote
+    pub state: QuoteState,
+    /// Expiry timestamp
+    pub expiry: u64,
+    /// Payment preimage (optional)
+    pub payment_preimage: Option<String>,
+    /// Request string (optional)
+    pub request: Option<String>,
+    /// Unit (optional)
+    pub unit: Option<CurrencyUnit>,
+    /// Extra payment-method-specific fields as JSON string
+    ///
+    /// These fields are flattened into the JSON representation, allowing
+    /// custom payment methods to include additional data without nesting.
+    pub extra: Option<String>,
+}
+
+impl From<cdk::nuts::MeltQuoteCustomResponse<String>> for MeltQuoteCustomResponse {
+    fn from(response: cdk::nuts::MeltQuoteCustomResponse<String>) -> Self {
+        let extra = if response.extra.is_null() {
+            None
+        } else {
+            Some(response.extra.to_string())
+        };
+
+        Self {
+            quote: response.quote,
+            amount: response.amount.into(),
+            fee_reserve: response.fee_reserve.into(),
+            state: response.state.into(),
+            expiry: response.expiry,
+            payment_preimage: response.payment_preimage,
+            request: response.request,
+            unit: response.unit.map(Into::into),
+            extra,
+        }
+    }
+}
+
 /// FFI-compatible PaymentMethod
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
 pub enum PaymentMethod {
@@ -192,10 +292,12 @@ pub enum PaymentMethod {
 
 impl From<cdk::nuts::PaymentMethod> for PaymentMethod {
     fn from(method: cdk::nuts::PaymentMethod) -> Self {
-        match method {
-            cdk::nuts::PaymentMethod::Bolt11 => Self::Bolt11,
-            cdk::nuts::PaymentMethod::Bolt12 => Self::Bolt12,
-            cdk::nuts::PaymentMethod::Custom(s) => Self::Custom { method: s },
+        match method.as_str() {
+            "bolt11" => Self::Bolt11,
+            "bolt12" => Self::Bolt12,
+            s => Self::Custom {
+                method: s.to_string(),
+            },
         }
     }
 }
@@ -203,9 +305,9 @@ impl From<cdk::nuts::PaymentMethod> for PaymentMethod {
 impl From<PaymentMethod> for cdk::nuts::PaymentMethod {
     fn from(method: PaymentMethod) -> Self {
         match method {
-            PaymentMethod::Bolt11 => Self::Bolt11,
-            PaymentMethod::Bolt12 => Self::Bolt12,
-            PaymentMethod::Custom { method } => Self::Custom(method),
+            PaymentMethod::Bolt11 => Self::from("bolt11"),
+            PaymentMethod::Bolt12 => Self::from("bolt12"),
+            PaymentMethod::Custom { method } => Self::from(method),
         }
     }
 }

+ 76 - 0
crates/cdk-ffi/src/wallet.rs

@@ -261,6 +261,30 @@ impl Wallet {
             .await?;
         Ok(quote.into())
     }
+    /// Get a mint quote using a unified interface for any payment method
+    ///
+    /// This method supports bolt11, bolt12, and custom payment methods.
+    /// For custom methods, you can pass extra JSON data that will be forwarded
+    /// to the payment processor.
+    ///
+    /// # Arguments
+    /// * `amount` - Optional amount to mint (required for bolt11)
+    /// * `method` - Payment method to use (bolt11, bolt12, or custom)
+    /// * `description` - Optional description for the quote
+    /// * `extra` - Optional JSON string with extra payment-method-specific fields (for custom methods)
+    pub async fn mint_quote_unified(
+        &self,
+        amount: Option<Amount>,
+        method: PaymentMethod,
+        description: Option<String>,
+        extra: Option<String>,
+    ) -> Result<MintQuote, FfiError> {
+        let quote = self
+            .inner
+            .mint_quote_unified(amount.map(Into::into), method.into(), description, extra)
+            .await?;
+        Ok(quote.into())
+    }
 
     /// Mint tokens using bolt12
     pub async fn mint_bolt12(
@@ -284,7 +308,27 @@ impl Wallet {
 
         Ok(proofs.into_iter().map(|p| p.into()).collect())
     }
+    pub async fn mint_unified(
+        &self,
+        quote_id: String,
+        amount: Option<Amount>,
+        amount_split_target: SplitTarget,
+        spending_conditions: Option<SpendingConditions>,
+    ) -> Result<Proofs, FfiError> {
+        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
 
+        let proofs = self
+            .inner
+            .mint_unified(
+                &quote_id,
+                amount.map(Into::into),
+                amount_split_target.into(),
+                conditions,
+            )
+            .await?;
+
+        Ok(proofs.into_iter().map(|p| p.into()).collect())
+    }
     /// Get a quote for a bolt12 melt
     pub async fn melt_bolt12_quote(
         &self,
@@ -295,7 +339,39 @@ impl Wallet {
         let quote = self.inner.melt_bolt12_quote(request, cdk_options).await?;
         Ok(quote.into())
     }
+    /// Get a melt quote using a unified interface for any payment method
+    ///
+    /// This method supports bolt11, bolt12, and custom payment methods.
+    /// For custom methods, you can pass extra JSON data that will be forwarded
+    /// to the payment processor.
+    ///
+    /// # Arguments
+    /// * `method` - Payment method to use (bolt11, bolt12, or custom)
+    /// * `request` - Payment request string (invoice, offer, or custom format)
+    /// * `options` - Optional melt options (MPP, amountless, etc.)
+    /// * `extra` - Optional JSON string with extra payment-method-specific fields (for custom methods)
+    pub async fn melt_quote_unified(
+        &self,
+        method: PaymentMethod,
+        request: String,
+        options: Option<MeltOptions>,
+        extra: Option<String>,
+    ) -> Result<MeltQuote, FfiError> {
+        // Parse the extra JSON string into a serde_json::Value
+        let extra_value = extra
+            .map(|s| serde_json::from_str(&s))
+            .transpose()
+            .map_err(|e| FfiError::Generic {
+                msg: format!("Invalid extra JSON: {}", e),
+            })?;
 
+        let cdk_options = options.map(Into::into);
+        let quote = self
+            .inner
+            .melt_quote_unified(method.into(), request, cdk_options, extra_value)
+            .await?;
+        Ok(quote.into())
+    }
     /// Swap proofs
     pub async fn swap(
         &self,

+ 26 - 7
crates/cdk-integration-tests/src/init_auth_mint.rs

@@ -3,6 +3,7 @@ use std::sync::Arc;
 
 use anyhow::Result;
 use bip39::Mnemonic;
+use cashu::nut00::KnownMethod;
 use cashu::{AuthRequired, Method, ProtectedEndpoint, RoutePath};
 use cdk::cdk_database::{self, MintAuthDatabase, MintDatabase, MintKeysDatabase};
 use cdk::mint::{MintBuilder, MintMeltLimits};
@@ -42,7 +43,7 @@ where
     mint_builder
         .add_payment_processor(
             CurrencyUnit::Sat,
-            PaymentMethod::Bolt11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             MintMeltLimits::new(1, 300),
             Arc::new(fake_wallet),
         )
@@ -58,12 +59,30 @@ where
     );
 
     let blind_auth_endpoints = vec![
-        ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt11),
-        ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11),
-        ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
-        ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11),
-        ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11),
-        ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11),
+        ProtectedEndpoint::new(
+            Method::Post,
+            RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+        ),
+        ProtectedEndpoint::new(
+            Method::Post,
+            RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+        ),
+        ProtectedEndpoint::new(
+            Method::Get,
+            RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+        ),
+        ProtectedEndpoint::new(
+            Method::Post,
+            RoutePath::MeltQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+        ),
+        ProtectedEndpoint::new(
+            Method::Get,
+            RoutePath::MeltQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+        ),
+        ProtectedEndpoint::new(
+            Method::Post,
+            RoutePath::Melt(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+        ),
         ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
         ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate),
         ProtectedEndpoint::new(Method::Post, RoutePath::Restore),

+ 25 - 2
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -8,8 +8,12 @@ use std::{env, fs};
 use anyhow::{anyhow, bail, Result};
 use async_trait::async_trait;
 use bip39::Mnemonic;
+use cashu::nut00::KnownMethod;
 use cashu::quote_id::QuoteId;
-use cashu::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
+use cashu::{
+    MeltQuoteBolt12Request, MeltQuoteCustomRequest, MintQuoteBolt12Request,
+    MintQuoteBolt12Response, MintQuoteCustomRequest, MintQuoteCustomResponse,
+};
 use cdk::amount::SplitTarget;
 use cdk::cdk_database::{self, WalletDatabase};
 use cdk::mint::{MintBuilder, MintMeltLimits};
@@ -219,6 +223,25 @@ impl MintConnector for DirectMintConnection {
         // Implementation to be added later
         Err(Error::UnsupportedPaymentMethod)
     }
+
+    /// Mint Quote for Custom Payment Method
+    async fn post_mint_custom_quote(
+        &self,
+        _method: &str,
+        _request: MintQuoteCustomRequest,
+    ) -> Result<MintQuoteCustomResponse<String>, Error> {
+        // Custom payment methods not implemented in test mock
+        Err(Error::UnsupportedPaymentMethod)
+    }
+
+    /// Melt Quote for Custom Payment Method
+    async fn post_melt_custom_quote(
+        &self,
+        _request: MeltQuoteCustomRequest,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+        // Custom payment methods not implemented in test mock
+        Err(Error::UnsupportedPaymentMethod)
+    }
 }
 
 pub fn setup_tracing() {
@@ -272,7 +295,7 @@ pub async fn create_and_start_test_mint() -> Result<Mint> {
     mint_builder
         .add_payment_processor(
             CurrencyUnit::Sat,
-            PaymentMethod::Bolt11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             MintMeltLimits::new(1, 10_000),
             Arc::new(ln_fake_backend),
         )

+ 7 - 4
crates/cdk-integration-tests/tests/mint.rs

@@ -15,8 +15,10 @@ use std::collections::{HashMap, HashSet};
 use std::sync::Arc;
 
 use bip39::Mnemonic;
+use cashu::nut00::KnownMethod;
+use cashu::PaymentMethod;
 use cdk::mint::{MintBuilder, MintMeltLimits};
-use cdk::nuts::{CurrencyUnit, PaymentMethod};
+use cdk::nuts::CurrencyUnit;
 use cdk::types::{FeeReserve, QuoteTTL};
 use cdk_fake_wallet::FakeWallet;
 use cdk_sqlite::mint::memory;
@@ -51,7 +53,7 @@ async fn test_correct_keyset() {
     mint_builder
         .add_payment_processor(
             CurrencyUnit::Sat,
-            PaymentMethod::Bolt11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             MintMeltLimits::new(1, 5_000),
             Arc::new(fake_wallet),
         )
@@ -152,7 +154,7 @@ async fn test_concurrent_duplicate_payment_handling() {
     mint_builder
         .add_payment_processor(
             CurrencyUnit::Sat,
-            PaymentMethod::Bolt11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             MintMeltLimits::new(1, 5_000),
             Arc::new(fake_wallet),
         )
@@ -179,10 +181,11 @@ async fn test_concurrent_duplicate_payment_handling() {
         None,
         Amount::ZERO,
         Amount::ZERO,
-        PaymentMethod::Bolt11,
+        PaymentMethod::Known(KnownMethod::Bolt11),
         current_time,
         vec![],
         vec![],
+        None, // extra_json
     );
 
     // Add the quote to the database

+ 4 - 0
crates/cdk-integration-tests/tests/regtest.rs

@@ -317,6 +317,10 @@ async fn test_cached_mint() {
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
     let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
     let http_client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
+
+    // Fetch mint info to populate cache support (NUT-19)
+    http_client.get_mint_info().await.unwrap();
+
     let premint_secrets = PreMintSecrets::random(
         active_keyset_id,
         100.into(),

+ 22 - 8
crates/cdk-ldk-node/src/lib.rs

@@ -454,15 +454,18 @@ impl MintPayment for CdkLdkNode {
     }
 
     /// Base Settings
-    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
-        let settings = Bolt11Settings {
-            mpp: false,
-            unit: CurrencyUnit::Msat,
-            invoice_description: true,
-            amountless: true,
-            bolt12: true,
+    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
+        let settings = SettingsResponse {
+            unit: CurrencyUnit::Msat.to_string(),
+            bolt11: Some(payment::Bolt11Settings {
+                mpp: false,
+                amountless: true,
+                invoice_description: true,
+            }),
+            bolt12: Some(payment::Bolt12Settings { amountless: true }),
+            custom: std::collections::HashMap::new(),
         };
-        Ok(serde_json::to_value(settings)?)
+        Ok(settings)
     }
 
     /// Create a new invoice
@@ -502,6 +505,7 @@ impl MintPayment for CdkLdkNode {
                     request_lookup_id: payment_identifier,
                     request: payment.to_string(),
                     expiry: Some(unix_time() + time),
+                    extra_json: None,
                 })
             }
             IncomingPaymentOptions::Bolt12(bolt12_options) => {
@@ -539,8 +543,12 @@ impl MintPayment for CdkLdkNode {
                     request_lookup_id: payment_identifier,
                     request: offer.to_string(),
                     expiry: time.map(|a| a as u64),
+                    extra_json: None,
                 })
             }
+            cdk_common::payment::IncomingPaymentOptions::Custom(_) => {
+                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
+            }
         }
     }
 
@@ -553,6 +561,9 @@ impl MintPayment for CdkLdkNode {
         options: OutgoingPaymentOptions,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
         match options {
+            cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
+                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
+            }
             OutgoingPaymentOptions::Bolt11(bolt11_options) => {
                 let bolt11 = bolt11_options.bolt11;
 
@@ -636,6 +647,9 @@ impl MintPayment for CdkLdkNode {
         options: OutgoingPaymentOptions,
     ) -> Result<MakePaymentResponse, Self::Err> {
         match options {
+            cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
+                Err(cdk_common::payment::Error::UnsupportedPaymentOption)
+            }
             OutgoingPaymentOptions::Bolt11(bolt11_options) => {
                 let bolt11 = bolt11_options.bolt11;
 

+ 19 - 13
crates/cdk-lnbits/src/lib.rs

@@ -13,9 +13,9 @@ use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
 use cdk_common::common::FeeReserve;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
-    self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
-    MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
-    PaymentQuoteResponse, WaitPaymentResponse,
+    self, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions, MakePaymentResponse,
+    MintPayment, OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, SettingsResponse,
+    WaitPaymentResponse,
 };
 use cdk_common::util::{hex, unix_time};
 use cdk_common::Bolt11Invoice;
@@ -23,7 +23,6 @@ use error::Error;
 use futures::Stream;
 use lnbits_rs::api::invoice::CreateInvoiceRequest;
 use lnbits_rs::LNBitsClient;
-use serde_json::Value;
 use tokio_util::sync::CancellationToken;
 
 pub mod error;
@@ -35,7 +34,7 @@ pub struct LNbits {
     fee_reserve: FeeReserve,
     wait_invoice_cancel_token: CancellationToken,
     wait_invoice_is_active: Arc<AtomicBool>,
-    settings: Bolt11Settings,
+    settings: SettingsResponse,
 }
 
 impl LNbits {
@@ -54,12 +53,15 @@ impl LNbits {
             fee_reserve,
             wait_invoice_cancel_token: CancellationToken::new(),
             wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
-            settings: Bolt11Settings {
-                mpp: false,
-                unit: CurrencyUnit::Sat,
-                invoice_description: true,
-                amountless: false,
-                bolt12: false,
+            settings: SettingsResponse {
+                unit: CurrencyUnit::Sat.to_string(),
+                bolt11: Some(payment::Bolt11Settings {
+                    mpp: false,
+                    amountless: false,
+                    invoice_description: true,
+                }),
+                bolt12: None,
+                custom: std::collections::HashMap::new(),
             },
         })
     }
@@ -141,8 +143,8 @@ impl LNbits {
 impl MintPayment for LNbits {
     type Err = payment::Error;
 
-    async fn get_settings(&self) -> Result<Value, Self::Err> {
-        Ok(serde_json::to_value(&self.settings)?)
+    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
+        Ok(self.settings.clone())
     }
 
     fn is_wait_invoice_active(&self) -> bool {
@@ -260,6 +262,7 @@ impl MintPayment for LNbits {
             OutgoingPaymentOptions::Bolt12(_bolt12_options) => {
                 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
             }
+            OutgoingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
         }
     }
 
@@ -321,6 +324,7 @@ impl MintPayment for LNbits {
             OutgoingPaymentOptions::Bolt12(_) => {
                 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
             }
+            OutgoingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
         }
     }
 
@@ -367,11 +371,13 @@ impl MintPayment for LNbits {
                     ),
                     request: request.to_string(),
                     expiry,
+                    extra_json: None,
                 })
             }
             IncomingPaymentOptions::Bolt12(_) => {
                 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
             }
+            IncomingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
         }
     }
 

+ 25 - 15
crates/cdk-lnd/src/lib.rs

@@ -19,9 +19,9 @@ use cdk_common::common::FeeReserve;
 use cdk_common::database::DynKVStore;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
-    self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
-    MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
-    PaymentQuoteResponse, WaitPaymentResponse,
+    self, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions, MakePaymentResponse,
+    MintPayment, OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, SettingsResponse,
+    WaitPaymentResponse,
 };
 use cdk_common::util::hex;
 use cdk_common::Bolt11Invoice;
@@ -58,7 +58,8 @@ pub struct Lnd {
     kv_store: DynKVStore,
     wait_invoice_cancel_token: CancellationToken,
     wait_invoice_is_active: Arc<AtomicBool>,
-    settings: Bolt11Settings,
+    settings: SettingsResponse,
+    unit: CurrencyUnit,
 }
 
 impl Lnd {
@@ -104,6 +105,7 @@ impl Lnd {
                 Error::Connection
             })?;
 
+        let unit = CurrencyUnit::Msat;
         Ok(Self {
             _address: address,
             _cert_file: cert_file,
@@ -113,13 +115,17 @@ impl Lnd {
             kv_store,
             wait_invoice_cancel_token: CancellationToken::new(),
             wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
-            settings: Bolt11Settings {
-                mpp: true,
-                unit: CurrencyUnit::Msat,
-                invoice_description: true,
-                amountless: true,
-                bolt12: false,
+            settings: SettingsResponse {
+                unit: unit.to_string(),
+                bolt11: Some(payment::Bolt11Settings {
+                    mpp: true,
+                    amountless: true,
+                    invoice_description: true,
+                }),
+                bolt12: None,
+                custom: std::collections::HashMap::new(),
             },
+            unit,
         })
     }
 
@@ -178,8 +184,8 @@ impl MintPayment for Lnd {
     type Err = payment::Error;
 
     #[instrument(skip_all)]
-    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
-        Ok(serde_json::to_value(&self.settings)?)
+    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
+        Ok(self.settings.clone())
     }
 
     #[instrument(skip_all)]
@@ -378,6 +384,7 @@ impl MintPayment for Lnd {
             OutgoingPaymentOptions::Bolt12(_) => {
                 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
             }
+            OutgoingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
         }
     }
 
@@ -576,6 +583,7 @@ impl MintPayment for Lnd {
             OutgoingPaymentOptions::Bolt12(_) => {
                 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
             }
+            OutgoingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
         }
     }
 
@@ -617,11 +625,13 @@ impl MintPayment for Lnd {
                     request_lookup_id: payment_identifier,
                     request: bolt11.to_string(),
                     expiry: unix_expiry,
+                    extra_json: None,
                 })
             }
             IncomingPaymentOptions::Bolt12(_) => {
                 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
             }
+            IncomingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
         }
     }
 
@@ -682,7 +692,7 @@ impl MintPayment for Lnd {
                         payment_proof: None,
                         status: MeltQuoteState::Unknown,
                         total_spent: Amount::ZERO,
-                        unit: self.settings.unit.clone(),
+                        unit: self.unit.clone(),
                     });
                 } else {
                     return Err(payment::Error::UnknownPaymentState);
@@ -701,7 +711,7 @@ impl MintPayment for Lnd {
                             payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Unknown,
                             total_spent: Amount::ZERO,
-                            unit: self.settings.unit.clone(),
+                            unit: self.unit.clone(),
                         },
                         PaymentStatus::InFlight | PaymentStatus::Initiated => {
                             // Continue waiting for the next update
@@ -725,7 +735,7 @@ impl MintPayment for Lnd {
                             payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Failed,
                             total_spent: Amount::ZERO,
-                            unit: self.settings.unit.clone(),
+                            unit: self.unit.clone(),
                         },
                     };
 

+ 14 - 13
crates/cdk-mint-rpc/src/proto/server.rs

@@ -688,19 +688,20 @@ impl CdkMint for MintRPCServer {
             _ => {
                 // Create a new quote with the same values
                 let quote = MintQuote::new(
-                    Some(mint_quote.id.clone()),          // id
-                    mint_quote.request.clone(),           // request
-                    mint_quote.unit.clone(),              // unit
-                    mint_quote.amount,                    // amount
-                    mint_quote.expiry,                    // expiry
-                    mint_quote.request_lookup_id.clone(), // request_lookup_id
-                    mint_quote.pubkey,                    // pubkey
-                    mint_quote.amount_issued(),           // amount_issued
-                    mint_quote.amount_paid(),             // amount_paid
-                    mint_quote.payment_method.clone(),    // method
-                    0,                                    // created_at
-                    vec![],                               // blinded_messages
-                    vec![],                               // payment_ids
+                    Some(mint_quote.id.clone()),
+                    mint_quote.request.clone(),
+                    mint_quote.unit.clone(),
+                    mint_quote.amount,
+                    mint_quote.expiry,
+                    mint_quote.request_lookup_id.clone(),
+                    mint_quote.pubkey,
+                    mint_quote.amount_issued(),
+                    mint_quote.amount_paid(),
+                    mint_quote.payment_method.clone(),
+                    0,
+                    vec![],
+                    vec![],
+                    None,
                 );
 
                 let mint_store = self.mint.localstore();

+ 5 - 0
crates/cdk-mintd/example.config.toml

@@ -169,6 +169,11 @@ max_delay_time = 3
 # addr = "127.0.0.1"
 # port = 50051
 # tls_dir = "/path/to/tls"
+# 
+# Note: To support custom payment methods (e.g., paypal, venmo, cashapp),
+# your gRPC payment processor should return them in the `custom` field of 
+# the get_settings() response. The mint will automatically create routes
+# for these methods (e.g., /v1/mint/quote/paypal, /v1/mint/paypal, etc.)
 
 # [auth]
 # Set to true to enable authentication features (defaults to false)

+ 298 - 108
crates/cdk-mintd/src/lib.rs

@@ -15,6 +15,7 @@ use axum::Router;
 use bip39::Mnemonic;
 use cdk::cdk_database::{self, KVStore, MintDatabase, MintKeysDatabase};
 use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
+use cdk::nuts::nut00::KnownMethod;
 #[cfg(any(
     feature = "cln",
     feature = "lnbits",
@@ -350,8 +351,18 @@ async fn configure_mint_builder(
     let mint_builder =
         configure_lightning_backend(settings, mint_builder, runtime, work_dir, kv_store).await?;
 
-    // Configure caching
-    let mint_builder = configure_cache(settings, mint_builder);
+    // Extract configured payment methods from mint_builder
+    let mint_info = mint_builder.current_mint_info();
+    let payment_methods: Vec<String> = mint_info
+        .nuts
+        .nut04
+        .methods
+        .iter()
+        .map(|m| m.method.to_string())
+        .collect();
+
+    // Configure caching with payment methods
+    let mint_builder = configure_cache(settings, mint_builder, &payment_methods);
 
     Ok(mint_builder)
 }
@@ -361,10 +372,14 @@ fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder)
     // Add contact information
     let mut contacts = Vec::new();
     if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key {
-        contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string()));
+        if !nostr_key.is_empty() {
+            contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string()));
+        }
     }
     if let Some(email) = &settings.mint_info.contact_email {
-        contacts.push(ContactInfo::new("email".to_string(), email.to_string()));
+        if !email.is_empty() {
+            contacts.push(ContactInfo::new("email".to_string(), email.to_string()));
+        }
     }
 
     // Add version information
@@ -374,14 +389,23 @@ fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder)
     );
 
     // Configure mint builder with basic info
-    let mut builder = mint_builder
-        .with_name(settings.mint_info.name.clone())
-        .with_version(mint_version)
-        .with_description(settings.mint_info.description.clone());
+    let mut builder = mint_builder.with_version(mint_version);
+
+    // Only set name if it's not empty
+    if !settings.mint_info.name.is_empty() {
+        builder = builder.with_name(settings.mint_info.name.clone());
+    }
+
+    // Only set description if it's not empty
+    if !settings.mint_info.description.is_empty() {
+        builder = builder.with_description(settings.mint_info.description.clone());
+    }
 
     // Add optional information
     if let Some(long_description) = &settings.mint_info.description_long {
-        builder = builder.with_long_description(long_description.to_string());
+        if !long_description.is_empty() {
+            builder = builder.with_long_description(long_description.to_string());
+        }
     }
 
     for contact in contacts {
@@ -393,15 +417,21 @@ fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder)
     }
 
     if let Some(icon_url) = &settings.mint_info.icon_url {
-        builder = builder.with_icon_url(icon_url.to_string());
+        if !icon_url.is_empty() {
+            builder = builder.with_icon_url(icon_url.to_string());
+        }
     }
 
     if let Some(motd) = &settings.mint_info.motd {
-        builder = builder.with_motd(motd.to_string());
+        if !motd.is_empty() {
+            builder = builder.with_motd(motd.to_string());
+        }
     }
 
     if let Some(tos_url) = &settings.mint_info.tos_url {
-        builder = builder.with_tos_url(tos_url.to_string());
+        if !tos_url.is_empty() {
+            builder = builder.with_tos_url(tos_url.to_string());
+        }
     }
 
     builder
@@ -574,59 +604,77 @@ async fn configure_backend_for_unit(
 ) -> Result<MintBuilder> {
     let payment_settings = backend.get_settings().await?;
 
-    if let Some(bolt12) = payment_settings.get("bolt12") {
-        if bolt12.as_bool().unwrap_or_default() {
-            mint_builder
-                .add_payment_processor(
-                    unit.clone(),
-                    PaymentMethod::Bolt12,
-                    mint_melt_limits,
-                    Arc::clone(&backend),
-                )
-                .await?;
+    let mut methods = Vec::new();
 
-            let nut17_supported = SupportedMethods::default_bolt12(unit.clone());
-            mint_builder = mint_builder.with_supported_websockets(nut17_supported);
-        }
+    // Add bolt11 if supported by payment processor
+    if payment_settings.bolt11.is_some() {
+        methods.push(PaymentMethod::Known(KnownMethod::Bolt11));
     }
 
-    mint_builder
-        .add_payment_processor(
-            unit.clone(),
-            PaymentMethod::Bolt11,
-            mint_melt_limits,
-            backend,
-        )
-        .await?;
+    // Add bolt12 if supported by payment processor
+    if payment_settings.bolt12.is_some() {
+        methods.push(PaymentMethod::Known(KnownMethod::Bolt12));
+    }
 
-    if let Some(input_fee) = settings.info.input_fee_ppk {
-        mint_builder.set_unit_fee(&unit, input_fee)?;
+    // Add custom methods from payment settings
+    for method_name in payment_settings.custom.keys() {
+        methods.push(PaymentMethod::from(method_name.as_str()));
     }
 
-    #[cfg(any(
-        feature = "cln",
-        feature = "lnbits",
-        feature = "lnd",
-        feature = "fakewallet",
-        feature = "grpc-processor",
-        feature = "ldk-node"
-    ))]
-    {
-        let nut17_supported = SupportedMethods::default_bolt11(unit);
+    // Add all supported payment methods to the mint builder
+    for method in &methods {
+        mint_builder
+            .add_payment_processor(
+                unit.clone(),
+                method.clone(),
+                mint_melt_limits,
+                backend.clone(),
+            )
+            .await?;
+    }
+
+    // Configure NUT17 (WebSocket support) for all payment methods
+    for method in &methods {
+        let method_str = method.to_string();
+        let nut17_supported = match method_str.as_str() {
+            "bolt11" => SupportedMethods::default_bolt11(unit.clone()),
+            "bolt12" => SupportedMethods::default_bolt12(unit.clone()),
+            _ => SupportedMethods::default_custom(method.clone(), unit.clone()),
+        };
         mint_builder = mint_builder.with_supported_websockets(nut17_supported);
     }
 
+    if let Some(input_fee) = settings.info.input_fee_ppk {
+        mint_builder.set_unit_fee(&unit, input_fee)?;
+    }
+
     Ok(mint_builder)
 }
 
-/// Configures cache settings
-fn configure_cache(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder {
-    let cached_endpoints = vec![
-        CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11),
-        CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11),
+/// Configures cache settings with support for custom payment methods
+fn configure_cache(
+    settings: &config::Settings,
+    mint_builder: MintBuilder,
+    payment_methods: &[String],
+) -> MintBuilder {
+    let mut cached_endpoints = vec![
+        // Always include swap endpoint
         CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap),
     ];
 
+    // Add cache endpoints for each configured payment method
+    for method in payment_methods {
+        // All payment methods (including bolt11, bolt12) use custom paths now
+        cached_endpoints.push(CachedEndpoint::new(
+            NUT19Method::Post,
+            NUT19Path::custom_mint(method),
+        ));
+        cached_endpoints.push(CachedEndpoint::new(
+            NUT19Method::Post,
+            NUT19Path::custom_melt(method),
+        ));
+    }
+
     let cache: HttpCache = settings.info.http_cache.clone().into();
     mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints)
 }
@@ -637,7 +685,10 @@ async fn setup_authentication(
     _work_dir: &Path,
     mut mint_builder: MintBuilder,
     _password: Option<String>,
-) -> Result<MintBuilder> {
+) -> Result<(
+    MintBuilder,
+    Option<cdk_common::database::DynMintAuthDatabase>,
+)> {
     if let Some(auth_settings) = settings.auth.clone() {
         use cdk_common::database::DynMintAuthDatabase;
 
@@ -708,7 +759,7 @@ async fn setup_authentication(
         let mint_blind_auth_endpoint =
             ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth);
 
-        protected_endpoints.insert(mint_blind_auth_endpoint, AuthRequired::Clear);
+        protected_endpoints.insert(mint_blind_auth_endpoint.clone(), AuthRequired::Clear);
 
         clear_auth_endpoints.push(mint_blind_auth_endpoint);
 
@@ -716,11 +767,11 @@ async fn setup_authentication(
         let mut add_endpoint = |endpoint: ProtectedEndpoint, auth_type: &AuthType| {
             match auth_type {
                 AuthType::Blind => {
-                    protected_endpoints.insert(endpoint, AuthRequired::Blind);
+                    protected_endpoints.insert(endpoint.clone(), AuthRequired::Blind);
                     blind_auth_endpoints.push(endpoint);
                 }
                 AuthType::Clear => {
-                    protected_endpoints.insert(endpoint, AuthRequired::Clear);
+                    protected_endpoints.insert(endpoint.clone(), AuthRequired::Clear);
                     clear_auth_endpoints.push(endpoint);
                 }
                 AuthType::None => {
@@ -729,55 +780,10 @@ async fn setup_authentication(
             };
         };
 
-        // Get mint quote endpoint
-        {
-            let mint_quote_protected_endpoint =
-                ProtectedEndpoint::new(cdk::nuts::Method::Post, RoutePath::MintQuoteBolt11);
-            add_endpoint(mint_quote_protected_endpoint, &auth_settings.get_mint_quote);
-        }
-
-        // Check mint quote endpoint
-        {
-            let check_mint_protected_endpoint =
-                ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11);
-            add_endpoint(
-                check_mint_protected_endpoint,
-                &auth_settings.check_mint_quote,
-            );
-        }
-
-        // Mint endpoint
-        {
-            let mint_protected_endpoint =
-                ProtectedEndpoint::new(cdk::nuts::Method::Post, RoutePath::MintBolt11);
-            add_endpoint(mint_protected_endpoint, &auth_settings.mint);
-        }
-
-        // Get melt quote endpoint
-        {
-            let melt_quote_protected_endpoint = ProtectedEndpoint::new(
-                cdk::nuts::Method::Post,
-                cdk::nuts::RoutePath::MeltQuoteBolt11,
-            );
-            add_endpoint(melt_quote_protected_endpoint, &auth_settings.get_melt_quote);
-        }
-
-        // Check melt quote endpoint
-        {
-            let check_melt_protected_endpoint =
-                ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11);
-            add_endpoint(
-                check_melt_protected_endpoint,
-                &auth_settings.check_melt_quote,
-            );
-        }
-
-        // Melt endpoint
-        {
-            let melt_protected_endpoint =
-                ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11);
-            add_endpoint(melt_protected_endpoint, &auth_settings.melt);
-        }
+        // Payment method endpoints (bolt11, bolt12, custom) will be added dynamically
+        // after the mint is built and we can query the payment processors for their
+        // supported methods. See the start_services_with_shutdown function where we
+        // add auth endpoints for all configured payment methods.
 
         // Swap endpoint
         {
@@ -805,6 +811,11 @@ async fn setup_authentication(
             add_endpoint(ws_protected_endpoint, &auth_settings.websocket_auth);
         }
 
+        // Custom protected_endpoints will be added dynamically after the mint is built
+        // and we can query the payment processors for their supported methods.
+        // For now, we don't add any custom endpoints here - they'll be added in the
+        // start_services_with_shutdown function after we have access to the mint instance.
+
         mint_builder = mint_builder.with_auth(
             auth_localstore.clone(),
             auth_settings.openid_discovery,
@@ -819,8 +830,11 @@ async fn setup_authentication(
         tx.remove_protected_endpoints(unprotected_endpoints).await?;
         tx.add_protected_endpoints(protected_endpoints).await?;
         tx.commit().await?;
+
+        Ok((mint_builder, Some(auth_localstore)))
+    } else {
+        Ok((mint_builder, None))
     }
-    Ok(mint_builder)
 }
 
 /// Build mints with the configured the signing method (remote signatory or local seed)
@@ -870,6 +884,7 @@ async fn start_services_with_shutdown(
     mint_builder_info: cdk::nuts::MintInfo,
     shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
     routers: Vec<Router>,
+    #[cfg(feature = "auth")] auth_localstore: Option<cdk_common::database::DynMintAuthDatabase>,
 ) -> Result<()> {
     let listen_addr = settings.info.listen_host.clone();
     let listen_port = settings.info.listen_port;
@@ -956,11 +971,183 @@ async fn start_services_with_shutdown(
     let nut04_methods = mint_info.nuts.nut04.supported_methods();
     let nut05_methods = mint_info.nuts.nut05.supported_methods();
 
-    let bolt12_supported = nut04_methods.contains(&&PaymentMethod::Bolt12)
-        || nut05_methods.contains(&&PaymentMethod::Bolt12);
+    // Get custom payment methods from payment processors
+    let mut custom_methods = mint.get_custom_payment_methods().await?;
+
+    // Add bolt11 if it's supported by any payment processor
+    let bolt11_method = PaymentMethod::Known(KnownMethod::Bolt11);
+    let bolt11_supported =
+        nut04_methods.contains(&&bolt11_method) || nut05_methods.contains(&&bolt11_method);
+    // Add bolt12 if it's supported by any payment processor
+    let bolt12_method = PaymentMethod::Known(KnownMethod::Bolt12);
+    let bolt12_supported =
+        nut04_methods.contains(&&bolt12_method) || nut05_methods.contains(&&bolt12_method);
+
+    if bolt11_supported
+        && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt11).to_string())
+    {
+        custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt11).to_string());
+    }
+    if bolt12_supported
+        && !custom_methods.contains(&PaymentMethod::Known(KnownMethod::Bolt12).to_string())
+    {
+        custom_methods.push(PaymentMethod::Known(KnownMethod::Bolt12).to_string());
+    }
+
+    tracing::info!("Payment methods: {:?}", custom_methods);
+
+    // Configure auth for custom payment methods if auth is enabled
+    #[cfg(feature = "auth")]
+    if let (Some(ref auth_settings), Some(auth_db)) = (&settings.auth, &auth_localstore) {
+        if auth_settings.auth_enabled {
+            use std::collections::HashMap;
+
+            use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
+            use cdk::nuts::AuthRequired;
+
+            use crate::config::AuthType;
+
+            // First, remove all existing payment-method-related endpoints from the database
+            // to ensure old payment methods don't persist when configuration changes
+            let existing_endpoints = auth_db.get_auth_for_endpoints().await?;
+            let payment_method_endpoints_to_remove: Vec<ProtectedEndpoint> = existing_endpoints
+                .keys()
+                .filter(|endpoint| {
+                    matches!(
+                        endpoint.path,
+                        RoutePath::MintQuote(_)
+                            | RoutePath::Mint(_)
+                            | RoutePath::MeltQuote(_)
+                            | RoutePath::Melt(_)
+                    )
+                })
+                .cloned()
+                .collect();
+
+            if !payment_method_endpoints_to_remove.is_empty() {
+                tracing::debug!(
+                    "Removing {} old payment method endpoints from database",
+                    payment_method_endpoints_to_remove.len()
+                );
+                let mut tx = auth_db.begin_transaction().await?;
+                tx.remove_protected_endpoints(payment_method_endpoints_to_remove)
+                    .await?;
+                tx.commit().await?;
+            }
+
+            // Now add endpoints for current payment methods
+            if !custom_methods.is_empty() {
+                let mut protected_endpoints = HashMap::new();
+
+                for method_name in &custom_methods {
+                    tracing::debug!("Adding auth endpoints for payment method: {}", method_name);
+
+                    // Determine auth type based on settings
+                    let mint_quote_auth = match auth_settings.get_mint_quote {
+                        AuthType::Clear => Some(AuthRequired::Clear),
+                        AuthType::Blind => Some(AuthRequired::Blind),
+                        AuthType::None => None,
+                    };
+
+                    let check_mint_quote_auth = match auth_settings.check_mint_quote {
+                        AuthType::Clear => Some(AuthRequired::Clear),
+                        AuthType::Blind => Some(AuthRequired::Blind),
+                        AuthType::None => None,
+                    };
+
+                    let mint_auth = match auth_settings.mint {
+                        AuthType::Clear => Some(AuthRequired::Clear),
+                        AuthType::Blind => Some(AuthRequired::Blind),
+                        AuthType::None => None,
+                    };
+
+                    let melt_quote_auth = match auth_settings.get_melt_quote {
+                        AuthType::Clear => Some(AuthRequired::Clear),
+                        AuthType::Blind => Some(AuthRequired::Blind),
+                        AuthType::None => None,
+                    };
+
+                    let check_melt_quote_auth = match auth_settings.check_melt_quote {
+                        AuthType::Clear => Some(AuthRequired::Clear),
+                        AuthType::Blind => Some(AuthRequired::Blind),
+                        AuthType::None => None,
+                    };
+
+                    let melt_auth = match auth_settings.melt {
+                        AuthType::Clear => Some(AuthRequired::Clear),
+                        AuthType::Blind => Some(AuthRequired::Blind),
+                        AuthType::None => None,
+                    };
+
+                    // Create endpoints for each payment method operation
+                    if let Some(auth) = mint_quote_auth {
+                        protected_endpoints.insert(
+                            ProtectedEndpoint::new(
+                                Method::Post,
+                                RoutePath::MintQuote(method_name.clone()),
+                            ),
+                            auth,
+                        );
+                    }
+                    if let Some(auth) = check_mint_quote_auth {
+                        protected_endpoints.insert(
+                            ProtectedEndpoint::new(
+                                Method::Get,
+                                RoutePath::MintQuote(method_name.clone()),
+                            ),
+                            auth,
+                        );
+                    }
+                    if let Some(auth) = mint_auth {
+                        protected_endpoints.insert(
+                            ProtectedEndpoint::new(
+                                Method::Post,
+                                RoutePath::Mint(method_name.clone()),
+                            ),
+                            auth,
+                        );
+                    }
+                    if let Some(auth) = melt_quote_auth {
+                        protected_endpoints.insert(
+                            ProtectedEndpoint::new(
+                                Method::Post,
+                                RoutePath::MeltQuote(method_name.clone()),
+                            ),
+                            auth,
+                        );
+                    }
+                    if let Some(auth) = check_melt_quote_auth {
+                        protected_endpoints.insert(
+                            ProtectedEndpoint::new(
+                                Method::Get,
+                                RoutePath::MeltQuote(method_name.clone()),
+                            ),
+                            auth,
+                        );
+                    }
+                    if let Some(auth) = melt_auth {
+                        protected_endpoints.insert(
+                            ProtectedEndpoint::new(
+                                Method::Post,
+                                RoutePath::Melt(method_name.clone()),
+                            ),
+                            auth,
+                        );
+                    }
+                }
+
+                // Add all custom endpoints in one transaction
+                if !protected_endpoints.is_empty() {
+                    let mut tx = auth_db.begin_transaction().await?;
+                    tx.add_protected_endpoints(protected_endpoints).await?;
+                    tx.commit().await?;
+                }
+            }
+        }
+    }
 
     let v1_service =
-        cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, bolt12_supported)
+        cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, custom_methods)
             .await?;
 
     let mut mint_service = Router::new()
@@ -1182,7 +1369,8 @@ pub async fn run_mintd_with_shutdown(
     let mint_builder =
         configure_mint_builder(settings, maybe_mint_builder, runtime, work_dir, Some(kv)).await?;
     #[cfg(feature = "auth")]
-    let mint_builder = setup_authentication(settings, work_dir, mint_builder, db_password).await?;
+    let (mint_builder, auth_localstore) =
+        setup_authentication(settings, work_dir, mint_builder, db_password).await?;
 
     let config_mint_info = mint_builder.current_mint_info();
 
@@ -1199,6 +1387,8 @@ pub async fn run_mintd_with_shutdown(
         config_mint_info,
         shutdown_signal,
         routers,
+        #[cfg(feature = "auth")]
+        auth_localstore,
     )
     .await
 }

+ 51 - 4
crates/cdk-payment-processor/src/proto/client.rs

@@ -10,7 +10,6 @@ use cdk_common::payment::{
     PaymentQuoteResponse as CdkPaymentQuoteResponse, WaitPaymentResponse,
 };
 use futures::{Stream, StreamExt};
-use serde_json::Value;
 use tokio_util::sync::CancellationToken;
 use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity};
 use tonic::{async_trait, Request};
@@ -87,7 +86,7 @@ impl PaymentProcessorClient {
 impl MintPayment for PaymentProcessorClient {
     type Err = cdk_common::payment::Error;
 
-    async fn get_settings(&self) -> Result<Value, Self::Err> {
+    async fn get_settings(&self) -> Result<cdk_common::payment::SettingsResponse, Self::Err> {
         let mut inner = self.inner.clone();
         let response = inner
             .get_settings(Request::new(EmptyRequest {}))
@@ -99,7 +98,22 @@ impl MintPayment for PaymentProcessorClient {
 
         let settings = response.into_inner();
 
-        Ok(serde_json::from_str(&settings.inner)?)
+        Ok(cdk_common::payment::SettingsResponse {
+            unit: settings.unit,
+            bolt11: settings
+                .bolt11
+                .map(|b| cdk_common::payment::Bolt11Settings {
+                    mpp: b.mpp,
+                    amountless: b.amountless,
+                    invoice_description: b.invoice_description,
+                }),
+            bolt12: settings
+                .bolt12
+                .map(|b| cdk_common::payment::Bolt12Settings {
+                    amountless: b.amountless,
+                }),
+            custom: settings.custom,
+        })
     }
 
     /// Create a new invoice
@@ -111,6 +125,16 @@ impl MintPayment for PaymentProcessorClient {
         let mut inner = self.inner.clone();
 
         let proto_options = match options {
+            CdkIncomingPaymentOptions::Custom(opts) => IncomingPaymentOptions {
+                options: Some(super::incoming_payment_options::Options::Custom(
+                    super::CustomIncomingPaymentOptions {
+                        description: opts.description,
+                        amount: Some(opts.amount.into()),
+                        unix_expiry: opts.unix_expiry,
+                        extra_json: opts.extra_json.clone(),
+                    },
+                )),
+            },
             CdkIncomingPaymentOptions::Bolt11(opts) => IncomingPaymentOptions {
                 options: Some(super::incoming_payment_options::Options::Bolt11(
                     super::Bolt11IncomingPaymentOptions {
@@ -157,6 +181,9 @@ impl MintPayment for PaymentProcessorClient {
         let mut inner = self.inner.clone();
 
         let request_type = match &options {
+            cdk_common::payment::OutgoingPaymentOptions::Custom(_) => {
+                OutgoingPaymentRequestType::Custom
+            }
             cdk_common::payment::OutgoingPaymentOptions::Bolt11(_) => {
                 OutgoingPaymentRequestType::Bolt11Invoice
             }
@@ -166,21 +193,29 @@ impl MintPayment for PaymentProcessorClient {
         };
 
         let proto_request = match &options {
+            cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => opts.request.to_string(),
             cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => opts.bolt11.to_string(),
             cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => opts.offer.to_string(),
         };
 
         let proto_options = match &options {
+            cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => opts.melt_options,
             cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => opts.melt_options,
             cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => opts.melt_options,
         };
 
+        let extra_json = match &options {
+            cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => opts.extra_json.clone(),
+            _ => None,
+        };
+
         let response = inner
             .get_payment_quote(Request::new(PaymentQuoteRequest {
                 request: proto_request,
                 unit: unit.to_string(),
                 options: proto_options.map(Into::into),
                 request_type: request_type.into(),
+                extra_json,
             }))
             .await
             .map_err(|err| {
@@ -199,8 +234,20 @@ impl MintPayment for PaymentProcessorClient {
         options: cdk_common::payment::OutgoingPaymentOptions,
     ) -> Result<CdkMakePaymentResponse, Self::Err> {
         let mut inner = self.inner.clone();
-
         let payment_options = match options {
+            cdk_common::payment::OutgoingPaymentOptions::Custom(opts) => {
+                super::OutgoingPaymentVariant {
+                    options: Some(super::outgoing_payment_variant::Options::Custom(
+                        super::CustomOutgoingPaymentOptions {
+                            offer: opts.request.to_string(),
+                            max_fee_amount: opts.max_fee_amount.map(Into::into),
+                            timeout_secs: opts.timeout_secs,
+                            melt_options: opts.melt_options.map(Into::into),
+                            extra_json: opts.extra_json.clone(),
+                        },
+                    )),
+                }
+            }
             cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => {
                 super::OutgoingPaymentVariant {
                     options: Some(super::outgoing_payment_variant::Options::Bolt11(

+ 18 - 2
crates/cdk-payment-processor/src/proto/mod.rs

@@ -76,6 +76,13 @@ impl TryFrom<PaymentIdentifier> for CdkPaymentIdentifier {
             (PaymentIdentifierType::CustomId, Some(payment_identifier::Value::Id(id))) => {
                 Ok(CdkPaymentIdentifier::CustomId(id))
             }
+            (PaymentIdentifierType::PaymentId, Some(payment_identifier::Value::Hash(hash))) => {
+                let decoded = hex::decode(hash)?;
+                let hash_array: [u8; 32] = decoded
+                    .try_into()
+                    .map_err(|_| crate::error::Error::InvalidHash)?;
+                Ok(CdkPaymentIdentifier::PaymentId(hash_array))
+            }
             _ => Err(crate::error::Error::InvalidPaymentIdentifier),
         }
     }
@@ -84,7 +91,9 @@ impl TryFrom<PaymentIdentifier> for CdkPaymentIdentifier {
 impl TryFrom<MakePaymentResponse> for CdkMakePaymentResponse {
     type Error = crate::error::Error;
     fn try_from(value: MakePaymentResponse) -> Result<Self, Self::Error> {
-        let status = value.status().as_str_name().parse()?;
+        // Use direct enum conversion instead of parsing string from as_str_name()
+        // as_str_name() returns "QUOTE_STATE_PAID" but MeltQuoteState::from_str expects "PAID"
+        let status: cdk_common::nuts::MeltQuoteState = value.status().into();
         let payment_proof = value.payment_proof;
         let total_spent = value.total_spent.into();
         let unit = CurrencyUnit::from_str(&value.unit)?;
@@ -109,6 +118,7 @@ impl From<CdkMakePaymentResponse> for MakePaymentResponse {
             status: QuoteState::from(value.status).into(),
             total_spent: value.total_spent.into(),
             unit: value.unit.to_string(),
+            extra_json: None,
         }
     }
 }
@@ -119,6 +129,7 @@ impl From<CreateIncomingPaymentResponse> for CreatePaymentResponse {
             request_identifier: Some(value.request_lookup_id.into()),
             request: value.request,
             expiry: value.expiry,
+            extra_json: None,
         }
     }
 }
@@ -134,10 +145,13 @@ impl TryFrom<CreatePaymentResponse> for CreateIncomingPaymentResponse {
             request_lookup_id: request_identifier.try_into()?,
             request: value.request,
             expiry: value.expiry,
+            extra_json: Some(
+                serde_json::from_str(value.extra_json.unwrap_or_default().as_str())
+                    .unwrap_or_default(),
+            ),
         })
     }
 }
-
 impl From<cdk_common::payment::PaymentQuoteResponse> for PaymentQuoteResponse {
     fn from(value: cdk_common::payment::PaymentQuoteResponse) -> Self {
         Self {
@@ -146,6 +160,7 @@ impl From<cdk_common::payment::PaymentQuoteResponse> for PaymentQuoteResponse {
             fee: value.fee.into(),
             unit: value.unit.to_string(),
             state: QuoteState::from(value.state).into(),
+            extra_json: None,
         }
     }
 }
@@ -209,6 +224,7 @@ impl From<QuoteState> for cdk_common::nuts::MeltQuoteState {
             QuoteState::Unknown => Self::Unknown,
             QuoteState::Failed => Self::Failed,
             QuoteState::Issued => Self::Unknown,
+            QuoteState::Unspecified => Self::Unknown,
         }
     }
 }

+ 73 - 20
crates/cdk-payment-processor/src/proto/payment_processor.proto

@@ -15,15 +15,36 @@ service CdkPaymentProcessor {
 message EmptyRequest {}
 
 message SettingsResponse {
-  string inner = 1;
+  string unit = 1;
+  Bolt11Settings bolt11 = 2;
+  Bolt12Settings bolt12 = 3;
+  map<string, string> custom = 4;
+}
+
+message Bolt12Settings {
+    bool amountless = 2;
+}
+
+message Bolt11Settings {
+    bool mpp = 1;
+    bool amountless = 2;
+    bool invoice_description = 5;
 }
 
+
 message Bolt11IncomingPaymentOptions {
   optional string description = 1;
   uint64 amount = 2;
   optional uint64 unix_expiry = 3;
 }
-
+message CustomIncomingPaymentOptions {
+  optional string description = 1;
+  optional uint64 amount = 2;
+  optional uint64 unix_expiry = 3;
+  // Extra payment-method-specific fields as JSON string
+  // These fields are flattened into the JSON representation on the client side
+  optional string extra_json = 4;
+}
 message Bolt12IncomingPaymentOptions {
   optional string description = 1;
   optional uint64 amount = 2;
@@ -31,26 +52,32 @@ message Bolt12IncomingPaymentOptions {
 }
 
 enum PaymentMethodType {
-  BOLT11 = 0;
-  BOLT12 = 1;
+  PAYMENT_METHOD_TYPE_UNSPECIFIED = 0;
+  PAYMENT_METHOD_TYPE_BOLT11 = 1;
+  PAYMENT_METHOD_TYPE_BOLT12 = 2;
+  PAYMENT_METHOD_TYPE_CUSTOM = 3;
 }
 
 enum OutgoingPaymentRequestType {
-  BOLT11_INVOICE = 0;
-  BOLT12_OFFER = 1;
+  OUTGOING_PAYMENT_REQUEST_TYPE_UNSPECIFIED = 0;
+  OUTGOING_PAYMENT_REQUEST_TYPE_BOLT11_INVOICE = 1;
+  OUTGOING_PAYMENT_REQUEST_TYPE_BOLT12_OFFER = 2;
+  OUTGOING_PAYMENT_REQUEST_TYPE_CUSTOM = 3;
 }
 
 enum PaymentIdentifierType {
-  PAYMENT_HASH = 0;
-  OFFER_ID = 1;
-  LABEL = 2;
-  BOLT12_PAYMENT_HASH = 3;
-  CUSTOM_ID = 4;
-  PAYMENT_ID = 5;
+  PAYMENT_IDENTIFIER_TYPE_UNSPECIFIED = 0;
+  PAYMENT_IDENTIFIER_TYPE_PAYMENT_HASH = 1;
+  PAYMENT_IDENTIFIER_TYPE_OFFER_ID = 2;
+  PAYMENT_IDENTIFIER_TYPE_LABEL = 3;
+  PAYMENT_IDENTIFIER_TYPE_BOLT12_PAYMENT_HASH = 4;
+  PAYMENT_IDENTIFIER_TYPE_CUSTOM_ID = 5;
+  PAYMENT_IDENTIFIER_TYPE_PAYMENT_ID = 6;
 }
 
 message PaymentIdentifier {
   PaymentIdentifierType type = 1;
+
   oneof value {
     string hash = 2; // Used for PAYMENT_HASH and BOLT12_PAYMENT_HASH
     string id = 3;   // Used for OFFER_ID, LABEL, and CUSTOM_ID
@@ -61,6 +88,7 @@ message IncomingPaymentOptions {
   oneof options {
     Bolt11IncomingPaymentOptions bolt11 = 1;
     Bolt12IncomingPaymentOptions bolt12 = 2;
+    CustomIncomingPaymentOptions custom = 3;
   }
 }
 
@@ -73,6 +101,9 @@ message CreatePaymentResponse {
   PaymentIdentifier request_identifier = 1;
   string request = 2;
   optional uint64 expiry = 3;
+  // Extra payment-method-specific fields as JSON string
+  // For custom payment methods, these fields are flattened into the response
+  optional string extra_json = 4;
 }
 
 message Mpp {
@@ -95,15 +126,19 @@ message PaymentQuoteRequest {
   string unit = 2;
   optional MeltOptions options = 3;
   OutgoingPaymentRequestType request_type = 4;
+  // Extra payment-method-specific fields as JSON string
+  // For custom payment methods, these fields are passed through for validation
+  optional string extra_json = 5;
 }
 
 enum QuoteState {
-    UNPAID = 0;
-    PAID = 1;
-    PENDING = 2;
-    UNKNOWN = 3;
-    FAILED = 4;
-    ISSUED = 5;
+    QUOTE_STATE_UNSPECIFIED = 0;
+    QUOTE_STATE_UNPAID = 1;
+    QUOTE_STATE_PAID = 2;
+    QUOTE_STATE_PENDING = 3;
+    QUOTE_STATE_UNKNOWN = 4;
+    QUOTE_STATE_FAILED = 5;
+    QUOTE_STATE_ISSUED = 6;
 }
 
 
@@ -113,6 +148,9 @@ message PaymentQuoteResponse {
   uint64 fee = 3;
   QuoteState state = 4;
   string unit = 5;
+  // Extra payment-method-specific fields as JSON string
+  // For custom payment methods, these fields are flattened into the response
+  optional string extra_json = 6;
 }
 
 message Bolt11OutgoingPaymentOptions {
@@ -128,16 +166,28 @@ message Bolt12OutgoingPaymentOptions {
   optional uint64 timeout_secs = 3;
   optional MeltOptions melt_options = 5;
 }
+message CustomOutgoingPaymentOptions {
+  string offer = 1;
+  optional uint64 max_fee_amount = 2;
+  optional uint64 timeout_secs = 3;
+  optional MeltOptions melt_options = 5;
+  // Extra payment-method-specific fields as JSON string
+  // These fields are flattened into the JSON representation on the client side
+  optional string extra_json = 6;
+}
 
 enum OutgoingPaymentOptionsType {
-  OUTGOING_BOLT11 = 0;
-  OUTGOING_BOLT12 = 1;
+  OUTGOING_PAYMENT_OPTIONS_TYPE_UNSPECIFIED = 0;
+  OUTGOING_PAYMENT_OPTIONS_TYPE_BOLT11 = 1;
+  OUTGOING_PAYMENT_OPTIONS_TYPE_BOLT12 = 2;
+  OUTGOING_PAYMENT_OPTIONS_TYPE_CUSTOM = 3;
 }
 
 message OutgoingPaymentVariant {
   oneof options {
     Bolt11OutgoingPaymentOptions bolt11 = 1;
     Bolt12OutgoingPaymentOptions bolt12 = 2;
+    CustomOutgoingPaymentOptions custom = 3;
   }
 }
 
@@ -153,6 +203,9 @@ message MakePaymentResponse {
   QuoteState status = 3;
   uint64 total_spent = 4;
   string unit = 5;
+  // Extra payment-method-specific fields as JSON string
+  // For custom payment methods, these fields are flattened into the response
+  optional string extra_json = 6;
 }
 
 message CheckIncomingPaymentRequest {

+ 50 - 3
crates/cdk-payment-processor/src/proto/server.rs

@@ -9,7 +9,6 @@ use cdk_common::payment::{IncomingPaymentOptions, MintPayment};
 use cdk_common::CurrencyUnit;
 use futures::{Stream, StreamExt};
 use lightning::offers::offer::Offer;
-use serde_json::Value;
 use tokio::sync::{mpsc, Notify};
 use tokio::task::JoinHandle;
 use tokio::time::{sleep, Instant};
@@ -167,14 +166,23 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
         &self,
         _request: Request<EmptyRequest>,
     ) -> Result<Response<SettingsResponse>, Status> {
-        let settings: Value = self
+        let settings = self
             .inner
             .get_settings()
             .await
             .map_err(|_| Status::internal("Could not get settings"))?;
 
         Ok(Response::new(SettingsResponse {
-            inner: settings.to_string(),
+            unit: settings.unit,
+            bolt11: settings.bolt11.map(|b| super::Bolt11Settings {
+                mpp: b.mpp,
+                amountless: b.amountless,
+                invoice_description: b.invoice_description,
+            }),
+            bolt12: settings.bolt12.map(|b| super::Bolt12Settings {
+                amountless: b.amountless,
+            }),
+            custom: settings.custom,
         }))
     }
 
@@ -193,6 +201,15 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
             .options
             .ok_or_else(|| Status::invalid_argument("Missing options"))?
         {
+            incoming_payment_options::Options::Custom(opts) => IncomingPaymentOptions::Custom(
+                Box::new(cdk_common::payment::CustomIncomingPaymentOptions {
+                    method: "".to_string(),
+                    description: opts.description,
+                    amount: opts.amount.unwrap_or(0).into(),
+                    unix_expiry: opts.unix_expiry,
+                    extra_json: opts.extra_json,
+                }),
+            ),
             incoming_payment_options::Options::Bolt11(opts) => {
                 IncomingPaymentOptions::Bolt11(cdk_common::payment::Bolt11IncomingPaymentOptions {
                     description: opts.description,
@@ -255,6 +272,22 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
                     },
                 ))
             }
+            OutgoingPaymentRequestType::Custom => {
+                // Custom payment method - pass request as-is with no validation
+                cdk_common::payment::OutgoingPaymentOptions::Custom(Box::new(
+                    cdk_common::payment::CustomOutgoingPaymentOptions {
+                        method: String::new(), // Will be set from variant
+                        request: request.request.clone(),
+                        max_fee_amount: None,
+                        timeout_secs: None,
+                        melt_options: request.options.map(Into::into),
+                        extra_json: request.extra_json.clone(),
+                    },
+                ))
+            }
+            OutgoingPaymentRequestType::Unspecified => {
+                return Err(Status::invalid_argument("Unspecified payment request type"));
+            }
         };
 
         let payment_quote = self
@@ -312,6 +345,20 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
 
                 (CurrencyUnit::Msat, payment_options)
             }
+            outgoing_payment_variant::Options::Custom(opts) => {
+                let payment_options = cdk_common::payment::OutgoingPaymentOptions::Custom(
+                    Box::new(cdk_common::payment::CustomOutgoingPaymentOptions {
+                        method: String::new(), // Method will be determined from context
+                        request: opts.offer,   // Reusing offer field for custom request string
+                        max_fee_amount: opts.max_fee_amount.map(Into::into),
+                        timeout_secs: opts.timeout_secs,
+                        melt_options: opts.melt_options.map(Into::into),
+                        extra_json: opts.extra_json,
+                    }),
+                );
+
+                (CurrencyUnit::Msat, payment_options)
+            }
         };
 
         let pay_response = self

+ 3 - 1
crates/cdk-redb/src/wallet/mod.rs

@@ -13,6 +13,7 @@ use cdk_common::database::{
     WalletDatabase, WalletDatabaseTransaction,
 };
 use cdk_common::mint_url::MintUrl;
+use cdk_common::nut00::KnownMethod;
 use cdk_common::util::unix_time;
 use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
 use cdk_common::{
@@ -422,7 +423,8 @@ impl WalletDatabase<database::Error> for WalletRedbDatabase {
             .flatten()
             .flat_map(|(_id, quote)| serde_json::from_str::<MintQuote>(quote.value()).ok())
             .filter(|quote| {
-                quote.amount_issued == Amount::ZERO || quote.payment_method == PaymentMethod::Bolt12
+                quote.amount_issued == Amount::ZERO
+                    || quote.payment_method == PaymentMethod::Known(KnownMethod::Bolt12)
             })
             .collect())
     }

+ 1 - 0
crates/cdk-sql-common/src/mint/quotes.rs

@@ -436,6 +436,7 @@ fn sql_row_to_mint_quote(
         column_as_number!(created_time),
         payments,
         issueances,
+        None,
     ))
 }
 

+ 7 - 6
crates/cdk-sqlite/src/wallet/mod.rs

@@ -23,6 +23,7 @@ mod tests {
     use std::str::FromStr;
 
     use cdk_common::database::WalletDatabase;
+    use cdk_common::nut00::KnownMethod;
     use cdk_common::nuts::{ProofDleq, State};
     use cdk_common::secret::Secret;
 
@@ -166,8 +167,8 @@ mod tests {
         // Test PaymentMethod variants
         let mint_url = MintUrl::from_str("https://example.com").unwrap();
         let payment_methods = [
-            PaymentMethod::Bolt11,
-            PaymentMethod::Bolt12,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            PaymentMethod::Known(KnownMethod::Bolt11),
             PaymentMethod::Custom("custom".to_string()),
         ];
 
@@ -324,7 +325,7 @@ mod tests {
             state: MintQuoteState::Paid,
             expiry: 1000000000,
             secret_key: None,
-            payment_method: PaymentMethod::Bolt11,
+            payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
             amount_issued: Amount::from(100),
             amount_paid: Amount::from(100),
         };
@@ -339,7 +340,7 @@ mod tests {
             state: MintQuoteState::Paid,
             expiry: 1000000000,
             secret_key: None,
-            payment_method: PaymentMethod::Bolt11,
+            payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
             amount_issued: Amount::from(0),
             amount_paid: Amount::from(100),
         };
@@ -354,7 +355,7 @@ mod tests {
             state: MintQuoteState::Unpaid,
             expiry: 1000000000,
             secret_key: None,
-            payment_method: PaymentMethod::Bolt12,
+            payment_method: PaymentMethod::Known(KnownMethod::Bolt12),
             amount_issued: Amount::from(0),
             amount_paid: Amount::from(0),
         };
@@ -369,7 +370,7 @@ mod tests {
             state: MintQuoteState::Unpaid,
             expiry: 1000000000,
             secret_key: None,
-            payment_method: PaymentMethod::Bolt11,
+            payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
             amount_issued: Amount::from(0),
             amount_paid: Amount::from(0),
         };

+ 1 - 1
crates/cdk/src/mint/auth/mod.rs

@@ -14,7 +14,7 @@ impl Mint {
         method: &ProtectedEndpoint,
     ) -> Result<Option<AuthRequired>, Error> {
         if let Some(auth_db) = self.auth_localstore.as_ref() {
-            Ok(auth_db.get_auth_for_endpoint(*method).await?)
+            Ok(auth_db.get_auth_for_endpoint(method.clone()).await?)
         } else {
             Ok(None)
         }

+ 459 - 40
crates/cdk/src/mint/builder.rs

@@ -6,9 +6,10 @@ use std::sync::Arc;
 use bitcoin::bip32::DerivationPath;
 use cdk_common::database::{DynMintDatabase, MintKeysDatabase};
 use cdk_common::error::Error;
+use cdk_common::nut00::KnownMethod;
 use cdk_common::nut04::MintMethodOptions;
 use cdk_common::nut05::MeltMethodOptions;
-use cdk_common::payment::{Bolt11Settings, DynMintPayment};
+use cdk_common::payment::DynMintPayment;
 #[cfg(feature = "auth")]
 use cdk_common::{database::DynMintAuthDatabase, nut21, nut22};
 use cdk_signatory::signatory::Signatory;
@@ -250,48 +251,105 @@ impl MintBuilder {
 
         let settings = payment_processor.get_settings().await?;
 
-        let settings: Bolt11Settings = settings.try_into()?;
-
-        if settings.mpp {
-            let mpp_settings = MppMethodSettings {
-                method: method.clone(),
-                unit: unit.clone(),
-            };
-
-            let mut mpp = self.mint_info.nuts.nut15.clone();
-
-            mpp.methods.push(mpp_settings);
-
-            self.mint_info.nuts.nut15 = mpp;
+        match method {
+            // Handle bolt11 methods
+            PaymentMethod::Known(KnownMethod::Bolt11) => {
+                if let Some(ref bolt11_settings) = settings.bolt11 {
+                    // Add MPP support if available
+                    if bolt11_settings.mpp {
+                        let mpp_settings = MppMethodSettings {
+                            method: method.clone(),
+                            unit: unit.clone(),
+                        };
+
+                        let mut mpp = self.mint_info.nuts.nut15.clone();
+                        mpp.methods.push(mpp_settings);
+                        self.mint_info.nuts.nut15 = mpp;
+                    }
+
+                    // Add to NUT04 (mint)
+                    let mint_method_settings = MintMethodSettings {
+                        method: method.clone(),
+                        unit: unit.clone(),
+                        min_amount: Some(limits.mint_min),
+                        max_amount: Some(limits.mint_max),
+                        options: Some(MintMethodOptions::Bolt11 {
+                            description: bolt11_settings.invoice_description,
+                        }),
+                    };
+                    self.mint_info.nuts.nut04.methods.push(mint_method_settings);
+                    self.mint_info.nuts.nut04.disabled = false;
+
+                    // Add to NUT05 (melt)
+                    let melt_method_settings = MeltMethodSettings {
+                        method: method.clone(),
+                        unit: unit.clone(),
+                        min_amount: Some(limits.melt_min),
+                        max_amount: Some(limits.melt_max),
+                        options: Some(MeltMethodOptions::Bolt11 {
+                            amountless: bolt11_settings.amountless,
+                        }),
+                    };
+                    self.mint_info.nuts.nut05.methods.push(melt_method_settings);
+                    self.mint_info.nuts.nut05.disabled = false;
+                }
+            }
+            // Handle bolt12 methods
+            PaymentMethod::Known(KnownMethod::Bolt12) => {
+                if settings.bolt12.is_some() {
+                    // Add to NUT04 (mint) - bolt12 doesn't have specific options yet
+                    let mint_method_settings = MintMethodSettings {
+                        method: method.clone(),
+                        unit: unit.clone(),
+                        min_amount: Some(limits.mint_min),
+                        max_amount: Some(limits.mint_max),
+                        options: None, // No bolt12-specific options in NUT04 yet
+                    };
+                    self.mint_info.nuts.nut04.methods.push(mint_method_settings);
+                    self.mint_info.nuts.nut04.disabled = false;
+
+                    // Add to NUT05 (melt) - bolt12 doesn't have specific options in MeltMethodOptions yet
+                    let melt_method_settings = MeltMethodSettings {
+                        method: method.clone(),
+                        unit: unit.clone(),
+                        min_amount: Some(limits.melt_min),
+                        max_amount: Some(limits.melt_max),
+                        options: None, // No bolt12-specific options in NUT05 yet
+                    };
+                    self.mint_info.nuts.nut05.methods.push(melt_method_settings);
+                    self.mint_info.nuts.nut05.disabled = false;
+                }
+            }
+            // Handle custom methods
+            PaymentMethod::Custom(_) => {
+                // Check if this custom method is supported by the payment processor
+                if settings.custom.contains_key(method.as_str()) {
+                    // Add to NUT04 (mint)
+                    let mint_method_settings = MintMethodSettings {
+                        method: method.clone(),
+                        unit: unit.clone(),
+                        min_amount: Some(limits.mint_min),
+                        max_amount: Some(limits.mint_max),
+                        options: Some(MintMethodOptions::Custom {}),
+                    };
+                    self.mint_info.nuts.nut04.methods.push(mint_method_settings);
+                    self.mint_info.nuts.nut04.disabled = false;
+
+                    // Add to NUT05 (melt)
+                    let melt_method_settings = MeltMethodSettings {
+                        method: method.clone(),
+                        unit: unit.clone(),
+                        min_amount: Some(limits.melt_min),
+                        max_amount: Some(limits.melt_max),
+                        options: None, // No custom-specific options in NUT05 yet
+                    };
+                    self.mint_info.nuts.nut05.methods.push(melt_method_settings);
+                    self.mint_info.nuts.nut05.disabled = false;
+                }
+            }
         }
 
-        let mint_method_settings = MintMethodSettings {
-            method: method.clone(),
-            unit: unit.clone(),
-            min_amount: Some(limits.mint_min),
-            max_amount: Some(limits.mint_max),
-            options: Some(MintMethodOptions::Bolt11 {
-                description: settings.invoice_description,
-            }),
-        };
-
-        self.mint_info.nuts.nut04.methods.push(mint_method_settings);
-        self.mint_info.nuts.nut04.disabled = false;
-
-        let melt_method_settings = MeltMethodSettings {
-            method,
-            unit,
-            min_amount: Some(limits.melt_min),
-            max_amount: Some(limits.melt_max),
-            options: Some(MeltMethodOptions::Bolt11 {
-                amountless: settings.amountless,
-            }),
-        };
-        self.mint_info.nuts.nut05.methods.push(melt_method_settings);
-        self.mint_info.nuts.nut05.disabled = false;
-
         let mut supported_units = self.supported_units.clone();
-
         supported_units.insert(key.unit.clone(), (0, 32));
         self.supported_units = supported_units;
 
@@ -386,12 +444,86 @@ impl MintMeltLimits {
 
 #[cfg(test)]
 mod tests {
+    use std::collections::HashMap;
+    use std::pin::Pin;
     use std::sync::Arc;
 
+    use async_trait::async_trait;
+    use cdk_common::payment::{
+        Bolt11Settings, Bolt12Settings, CreateIncomingPaymentResponse, Event,
+        IncomingPaymentOptions, MakePaymentResponse, OutgoingPaymentOptions, PaymentIdentifier,
+        PaymentQuoteResponse, SettingsResponse,
+    };
     use cdk_sqlite::mint::memory;
+    use futures::Stream;
+    use KnownMethod;
 
     use super::*;
 
+    // Mock payment processor for testing
+    struct MockPaymentProcessor {
+        settings: SettingsResponse,
+    }
+
+    #[async_trait]
+    impl cdk_common::payment::MintPayment for MockPaymentProcessor {
+        type Err = cdk_common::payment::Error;
+
+        async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
+            Ok(self.settings.clone())
+        }
+
+        async fn create_incoming_payment_request(
+            &self,
+            _unit: &CurrencyUnit,
+            _options: IncomingPaymentOptions,
+        ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
+            unimplemented!()
+        }
+
+        async fn get_payment_quote(
+            &self,
+            _unit: &CurrencyUnit,
+            _options: OutgoingPaymentOptions,
+        ) -> Result<PaymentQuoteResponse, Self::Err> {
+            unimplemented!()
+        }
+
+        async fn make_payment(
+            &self,
+            _unit: &CurrencyUnit,
+            _options: OutgoingPaymentOptions,
+        ) -> Result<MakePaymentResponse, Self::Err> {
+            unimplemented!()
+        }
+
+        async fn wait_payment_event(
+            &self,
+        ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
+            unimplemented!()
+        }
+
+        fn is_wait_invoice_active(&self) -> bool {
+            false
+        }
+
+        fn cancel_wait_invoice(&self) {}
+
+        async fn check_incoming_payment_status(
+            &self,
+            _payment_identifier: &PaymentIdentifier,
+        ) -> Result<Vec<cdk_common::payment::WaitPaymentResponse>, Self::Err> {
+            unimplemented!()
+        }
+
+        async fn check_outgoing_payment(
+            &self,
+            _payment_identifier: &PaymentIdentifier,
+        ) -> Result<MakePaymentResponse, Self::Err> {
+            unimplemented!()
+        }
+    }
+
     #[tokio::test]
     async fn test_mint_builder_default_nuts_support() {
         let localstore = Arc::new(memory::empty().await.unwrap());
@@ -431,4 +563,291 @@ mod tests {
             "NUT-20 should be supported by default"
         );
     }
+
+    #[tokio::test]
+    async fn test_add_payment_processor_bolt11() {
+        let localstore = Arc::new(memory::empty().await.unwrap());
+        let mut builder = MintBuilder::new(localstore);
+
+        let bolt11_settings = Bolt11Settings {
+            mpp: true,
+            amountless: true,
+            invoice_description: true,
+        };
+
+        let settings = SettingsResponse {
+            unit: "sat".to_string(),
+            bolt11: Some(bolt11_settings),
+            bolt12: None,
+            custom: HashMap::new(),
+        };
+
+        let payment_processor = Arc::new(MockPaymentProcessor { settings });
+        let unit = CurrencyUnit::Sat;
+        let method = PaymentMethod::Known(KnownMethod::Bolt11);
+        let limits = MintMeltLimits::new(100, 10000);
+
+        builder
+            .add_payment_processor(unit.clone(), method.clone(), limits, payment_processor)
+            .await
+            .unwrap();
+
+        let mint_info = builder.current_mint_info();
+
+        // Check NUT04 (mint) settings
+        assert!(!mint_info.nuts.nut04.disabled);
+        assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
+        let mint_method = &mint_info.nuts.nut04.methods[0];
+        assert_eq!(mint_method.method, method);
+        assert_eq!(mint_method.unit, unit);
+        assert_eq!(mint_method.min_amount, Some(limits.mint_min));
+        assert_eq!(mint_method.max_amount, Some(limits.mint_max));
+        assert!(matches!(
+            mint_method.options,
+            Some(MintMethodOptions::Bolt11 { description: true })
+        ));
+
+        // Check NUT05 (melt) settings
+        assert!(!mint_info.nuts.nut05.disabled);
+        assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
+        let melt_method = &mint_info.nuts.nut05.methods[0];
+        assert_eq!(melt_method.method, method);
+        assert_eq!(melt_method.unit, unit);
+        assert_eq!(melt_method.min_amount, Some(limits.melt_min));
+        assert_eq!(melt_method.max_amount, Some(limits.melt_max));
+        assert!(matches!(
+            melt_method.options,
+            Some(MeltMethodOptions::Bolt11 { amountless: true })
+        ));
+
+        // Check NUT15 (MPP) settings
+        assert_eq!(mint_info.nuts.nut15.methods.len(), 1);
+        let mpp_method = &mint_info.nuts.nut15.methods[0];
+        assert_eq!(mpp_method.method, method);
+        assert_eq!(mpp_method.unit, unit);
+    }
+
+    #[tokio::test]
+    async fn test_add_payment_processor_bolt11_without_mpp() {
+        let localstore = Arc::new(memory::empty().await.unwrap());
+        let mut builder = MintBuilder::new(localstore);
+
+        let bolt11_settings = Bolt11Settings {
+            mpp: false, // MPP disabled
+            amountless: false,
+            invoice_description: false,
+        };
+
+        let settings = SettingsResponse {
+            unit: "sat".to_string(),
+            bolt11: Some(bolt11_settings),
+            bolt12: None,
+            custom: HashMap::new(),
+        };
+
+        let payment_processor = Arc::new(MockPaymentProcessor { settings });
+        let unit = CurrencyUnit::Sat;
+        let method = PaymentMethod::Known(KnownMethod::Bolt11);
+        let limits = MintMeltLimits::new(100, 10000);
+
+        builder
+            .add_payment_processor(unit, method, limits, payment_processor)
+            .await
+            .unwrap();
+
+        let mint_info = builder.current_mint_info();
+
+        // NUT15 should be empty when MPP is disabled
+        assert_eq!(mint_info.nuts.nut15.methods.len(), 0);
+
+        // But NUT04 and NUT05 should still be populated
+        assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
+        assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
+    }
+
+    #[tokio::test]
+    async fn test_add_payment_processor_bolt12() {
+        let localstore = Arc::new(memory::empty().await.unwrap());
+        let mut builder = MintBuilder::new(localstore);
+
+        let bolt12_settings = Bolt12Settings { amountless: true };
+
+        let settings = SettingsResponse {
+            unit: "sat".to_string(),
+            bolt11: None,
+            bolt12: Some(bolt12_settings),
+            custom: HashMap::new(),
+        };
+
+        let payment_processor = Arc::new(MockPaymentProcessor { settings });
+        let unit = CurrencyUnit::Sat;
+        let method = PaymentMethod::Known(KnownMethod::Bolt12);
+        let limits = MintMeltLimits::new(100, 10000);
+
+        builder
+            .add_payment_processor(unit.clone(), method.clone(), limits, payment_processor)
+            .await
+            .unwrap();
+
+        let mint_info = builder.current_mint_info();
+
+        // Check NUT04 (mint) settings
+        assert!(!mint_info.nuts.nut04.disabled);
+        assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
+        let mint_method = &mint_info.nuts.nut04.methods[0];
+        assert_eq!(mint_method.method, method);
+        assert_eq!(mint_method.unit, unit);
+        assert_eq!(mint_method.min_amount, Some(limits.mint_min));
+        assert_eq!(mint_method.max_amount, Some(limits.mint_max));
+        assert!(mint_method.options.is_none());
+
+        // Check NUT05 (melt) settings
+        assert!(!mint_info.nuts.nut05.disabled);
+        assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
+        let melt_method = &mint_info.nuts.nut05.methods[0];
+        assert_eq!(melt_method.method, method);
+        assert_eq!(melt_method.unit, unit);
+        assert_eq!(melt_method.min_amount, Some(limits.melt_min));
+        assert_eq!(melt_method.max_amount, Some(limits.melt_max));
+        assert!(melt_method.options.is_none());
+    }
+
+    #[tokio::test]
+    async fn test_add_payment_processor_custom() {
+        let localstore = Arc::new(memory::empty().await.unwrap());
+        let mut builder = MintBuilder::new(localstore);
+
+        let mut custom_methods = HashMap::new();
+        custom_methods.insert("paypal".to_string(), "{}".to_string());
+
+        let settings = SettingsResponse {
+            unit: "usd".to_string(),
+            bolt11: None,
+            bolt12: None,
+            custom: custom_methods,
+        };
+
+        let payment_processor = Arc::new(MockPaymentProcessor { settings });
+        let unit = CurrencyUnit::Usd;
+        let method = PaymentMethod::Custom("paypal".to_string());
+        let limits = MintMeltLimits::new(100, 10000);
+
+        builder
+            .add_payment_processor(unit.clone(), method.clone(), limits, payment_processor)
+            .await
+            .unwrap();
+
+        let mint_info = builder.current_mint_info();
+
+        // Check NUT04 (mint) settings
+        assert!(!mint_info.nuts.nut04.disabled);
+        assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
+        let mint_method = &mint_info.nuts.nut04.methods[0];
+        assert_eq!(mint_method.method, method);
+        assert_eq!(mint_method.unit, unit);
+        assert_eq!(mint_method.min_amount, Some(limits.mint_min));
+        assert_eq!(mint_method.max_amount, Some(limits.mint_max));
+        assert!(matches!(
+            mint_method.options,
+            Some(MintMethodOptions::Custom {})
+        ));
+
+        // Check NUT05 (melt) settings
+        assert!(!mint_info.nuts.nut05.disabled);
+        assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
+        let melt_method = &mint_info.nuts.nut05.methods[0];
+        assert_eq!(melt_method.method, method);
+        assert_eq!(melt_method.unit, unit);
+        assert_eq!(melt_method.min_amount, Some(limits.melt_min));
+        assert_eq!(melt_method.max_amount, Some(limits.melt_max));
+        assert!(melt_method.options.is_none());
+    }
+
+    #[tokio::test]
+    async fn test_add_payment_processor_custom_not_supported() {
+        let localstore = Arc::new(memory::empty().await.unwrap());
+        let mut builder = MintBuilder::new(localstore);
+
+        // Settings with no custom methods
+        let settings = SettingsResponse {
+            unit: "usd".to_string(),
+            bolt11: None,
+            bolt12: None,
+            custom: HashMap::new(), // Empty - no custom methods supported
+        };
+
+        let payment_processor = Arc::new(MockPaymentProcessor { settings });
+        let unit = CurrencyUnit::Usd;
+        let method = PaymentMethod::Custom("paypal".to_string());
+        let limits = MintMeltLimits::new(1, 1000);
+
+        builder
+            .add_payment_processor(unit, method, limits, payment_processor)
+            .await
+            .unwrap();
+
+        let mint_info = builder.current_mint_info();
+
+        // NUT04 and NUT05 should remain empty since the custom method is not in settings
+        assert_eq!(mint_info.nuts.nut04.methods.len(), 0);
+        assert_eq!(mint_info.nuts.nut05.methods.len(), 0);
+    }
+
+    #[tokio::test]
+    async fn test_add_multiple_payment_processors() {
+        let localstore = Arc::new(memory::empty().await.unwrap());
+        let mut builder = MintBuilder::new(localstore);
+
+        // Add Bolt11
+        let bolt11_settings = Bolt11Settings {
+            mpp: false,
+            amountless: true,
+            invoice_description: false,
+        };
+        let settings1 = SettingsResponse {
+            unit: "sat".to_string(),
+            bolt11: Some(bolt11_settings),
+            bolt12: None,
+            custom: HashMap::new(),
+        };
+        let processor1 = Arc::new(MockPaymentProcessor {
+            settings: settings1,
+        });
+        builder
+            .add_payment_processor(
+                CurrencyUnit::Sat,
+                PaymentMethod::Known(KnownMethod::Bolt11),
+                MintMeltLimits::new(100, 10000),
+                processor1,
+            )
+            .await
+            .unwrap();
+
+        // Add Bolt12
+        let bolt12_settings = Bolt12Settings { amountless: false };
+        let settings2 = SettingsResponse {
+            unit: "sat".to_string(),
+            bolt11: None,
+            bolt12: Some(bolt12_settings),
+            custom: HashMap::new(),
+        };
+        let processor2 = Arc::new(MockPaymentProcessor {
+            settings: settings2,
+        });
+        builder
+            .add_payment_processor(
+                CurrencyUnit::Sat,
+                PaymentMethod::Known(KnownMethod::Bolt12),
+                MintMeltLimits::new(200, 20000),
+                processor2,
+            )
+            .await
+            .unwrap();
+
+        let mint_info = builder.current_mint_info();
+
+        // Should have both methods in NUT04 and NUT05
+        assert_eq!(mint_info.nuts.nut04.methods.len(), 2);
+        assert_eq!(mint_info.nuts.nut05.methods.len(), 2);
+    }
 }

+ 100 - 61
crates/cdk/src/mint/issue/mod.rs

@@ -1,15 +1,17 @@
 use cdk_common::database::Acquired;
 use cdk_common::mint::{MintQuote, Operation};
+use cdk_common::nut00::KnownMethod;
 use cdk_common::payment::{
-    Bolt11IncomingPaymentOptions, Bolt11Settings, Bolt12IncomingPaymentOptions,
+    Bolt11IncomingPaymentOptions, Bolt12IncomingPaymentOptions, CustomIncomingPaymentOptions,
     IncomingPaymentOptions, WaitPaymentResponse,
 };
 use cdk_common::quote_id::QuoteId;
 use cdk_common::util::unix_time;
 use cdk_common::{
     database, ensure_cdk, Amount, CurrencyUnit, Error, MintQuoteBolt11Request,
-    MintQuoteBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response, MintQuoteState,
-    MintRequest, MintResponse, NotificationPayload, PaymentMethod, PublicKey,
+    MintQuoteBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response,
+    MintQuoteCustomRequest, MintQuoteCustomResponse, MintQuoteState, MintRequest, MintResponse,
+    NotificationPayload, PaymentMethod, PublicKey,
 };
 #[cfg(feature = "prometheus")]
 use cdk_prometheus::METRICS;
@@ -31,6 +33,13 @@ pub enum MintQuoteRequest {
     Bolt11(MintQuoteBolt11Request),
     /// Lightning Network BOLT12 offer request
     Bolt12(MintQuoteBolt12Request),
+    /// Custom payment method request
+    Custom {
+        /// Payment method name (e.g., "paypal", "venmo")
+        method: String,
+        /// Generic request data
+        request: MintQuoteCustomRequest,
+    },
 }
 
 impl From<MintQuoteBolt11Request> for MintQuoteRequest {
@@ -50,10 +59,12 @@ impl MintQuoteRequest {
     ///
     /// For Bolt11 requests, this returns `Some(amount)` as the amount is required.
     /// For Bolt12 requests, this returns the optional amount.
+    /// For Custom requests, this returns `Some(amount)` as the amount is required.
     pub fn amount(&self) -> Option<Amount> {
         match self {
             MintQuoteRequest::Bolt11(request) => Some(request.amount),
             MintQuoteRequest::Bolt12(request) => request.amount,
+            MintQuoteRequest::Custom { request, .. } => Some(request.amount),
         }
     }
 
@@ -62,14 +73,16 @@ impl MintQuoteRequest {
         match self {
             MintQuoteRequest::Bolt11(request) => request.unit.clone(),
             MintQuoteRequest::Bolt12(request) => request.unit.clone(),
+            MintQuoteRequest::Custom { request, .. } => request.unit.clone(),
         }
     }
 
     /// Get the payment method for the mint quote request
     pub fn payment_method(&self) -> PaymentMethod {
         match self {
-            MintQuoteRequest::Bolt11(_) => PaymentMethod::Bolt11,
-            MintQuoteRequest::Bolt12(_) => PaymentMethod::Bolt12,
+            MintQuoteRequest::Bolt11(_) => PaymentMethod::Known(KnownMethod::Bolt11),
+            MintQuoteRequest::Bolt12(_) => PaymentMethod::Known(KnownMethod::Bolt12),
+            MintQuoteRequest::Custom { method, .. } => PaymentMethod::from(method.clone()),
         }
     }
 
@@ -77,10 +90,12 @@ impl MintQuoteRequest {
     ///
     /// For Bolt11 requests, this returns the optional pubkey.
     /// For Bolt12 requests, this returns `Some(pubkey)` as the pubkey is required.
+    /// For Custom requests, this returns the optional pubkey.
     pub fn pubkey(&self) -> Option<PublicKey> {
         match self {
             MintQuoteRequest::Bolt11(request) => request.pubkey,
             MintQuoteRequest::Bolt12(request) => Some(request.pubkey),
+            MintQuoteRequest::Custom { request, .. } => request.pubkey,
         }
     }
 }
@@ -95,6 +110,13 @@ pub enum MintQuoteResponse {
     Bolt11(MintQuoteBolt11Response<QuoteId>),
     /// Lightning Network BOLT12 offer response
     Bolt12(MintQuoteBolt12Response<QuoteId>),
+    /// Custom payment method response
+    Custom {
+        /// Payment method name
+        method: String,
+        /// Generic response data
+        response: MintQuoteCustomResponse<QuoteId>,
+    },
 }
 
 impl TryFrom<MintQuoteResponse> for MintQuoteBolt11Response<QuoteId> {
@@ -123,16 +145,19 @@ impl TryFrom<MintQuote> for MintQuoteResponse {
     type Error = Error;
 
     fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
-        match quote.payment_method {
-            PaymentMethod::Bolt11 => {
-                let bolt11_response: MintQuoteBolt11Response<QuoteId> = quote.into();
-                Ok(MintQuoteResponse::Bolt11(bolt11_response))
-            }
-            PaymentMethod::Bolt12 => {
-                let bolt12_response = MintQuoteBolt12Response::try_from(quote)?;
-                Ok(MintQuoteResponse::Bolt12(bolt12_response))
-            }
-            PaymentMethod::Custom(_) => Err(Error::InvalidPaymentMethod),
+        if quote.payment_method.is_bolt11() {
+            let bolt11_response: MintQuoteBolt11Response<QuoteId> = quote.into();
+            Ok(MintQuoteResponse::Bolt11(bolt11_response))
+        } else if quote.payment_method.is_bolt12() {
+            let bolt12_response = MintQuoteBolt12Response::try_from(quote)?;
+            Ok(MintQuoteResponse::Bolt12(bolt12_response))
+        } else {
+            let method = quote.payment_method.to_string();
+            let custom_response = MintQuoteCustomResponse::try_from(quote)?;
+            Ok(MintQuoteResponse::Custom {
+                method,
+                response: custom_response,
+            })
         }
     }
 }
@@ -252,13 +277,14 @@ impl Mint {
                     let quote_expiry = unix_time() + mint_ttl;
 
                     let settings = ln.get_settings().await?;
-                    let settings: Bolt11Settings = serde_json::from_value(settings)?;
 
                     let description = bolt11_request.description;
 
-                    if description.is_some() && !settings.invoice_description {
-                        tracing::error!("Backend does not support invoice description");
-                        return Err(Error::InvoiceDescriptionUnsupported);
+                    if let Some(ref bolt11_settings) = settings.bolt11 {
+                        if description.is_some() && !bolt11_settings.invoice_description {
+                            tracing::error!("Backend does not support invoice description");
+                            return Err(Error::InvoiceDescriptionUnsupported);
+                        }
                     }
 
                     let bolt11_options = Bolt11IncomingPaymentOptions {
@@ -280,6 +306,27 @@ impl Mint {
 
                     IncomingPaymentOptions::Bolt12(Box::new(bolt12_options))
                 }
+                MintQuoteRequest::Custom { method, request } => {
+                    let mint_ttl = self.quote_ttl().await?.mint_ttl;
+                    let quote_expiry = unix_time() + mint_ttl;
+
+                    // Convert extra serde_json::Value to JSON string if not null
+                    let extra_json = if request.extra.is_null() {
+                        None
+                    } else {
+                        Some(request.extra.to_string())
+                    };
+
+                    let custom_options = CustomIncomingPaymentOptions {
+                        method,
+                        description: request.description,
+                        amount: request.amount,
+                        unix_expiry: Some(quote_expiry),
+                        extra_json,
+                    };
+
+                    IncomingPaymentOptions::Custom(Box::new(custom_options))
+                }
             };
 
             let create_invoice_response = ln
@@ -304,6 +351,7 @@ impl Mint {
                 unix_time(),
                 vec![],
                 vec![],
+                Some(create_invoice_response.extra_json.unwrap_or_default()),
             );
 
             tracing::debug!(
@@ -319,18 +367,14 @@ impl Mint {
             tx.add_mint_quote(quote.clone()).await?;
             tx.commit().await?;
 
-            match payment_method {
-                PaymentMethod::Bolt11 => {
-                    let res: MintQuoteBolt11Response<QuoteId> = quote.clone().into();
-                    self.pubsub_manager
-                        .publish(NotificationPayload::MintQuoteBolt11Response(res));
-                }
-                PaymentMethod::Bolt12 => {
-                    let res: MintQuoteBolt12Response<QuoteId> = quote.clone().try_into()?;
-                    self.pubsub_manager
-                        .publish(NotificationPayload::MintQuoteBolt12Response(res));
-                }
-                PaymentMethod::Custom(_) => {}
+            if payment_method.is_bolt11() {
+                let res: MintQuoteBolt11Response<QuoteId> = quote.clone().into();
+                self.pubsub_manager
+                    .publish(NotificationPayload::MintQuoteBolt11Response(res));
+            } else if payment_method.is_bolt12() {
+                let res: MintQuoteBolt12Response<QuoteId> = quote.clone().try_into()?;
+                self.pubsub_manager
+                    .publish(NotificationPayload::MintQuoteBolt12Response(res));
             }
 
             quote.try_into()
@@ -504,9 +548,7 @@ impl Mint {
                 .await?
                 .ok_or(Error::UnknownQuote)?;
 
-            if quote.payment_method == PaymentMethod::Bolt11 {
-                self.check_mint_quote_paid(&mut quote).await?;
-            }
+            self.check_mint_quote_paid(&mut quote).await?;
 
             quote.try_into()
         }
@@ -553,10 +595,9 @@ impl Mint {
                 .get_mint_quote(&mint_request.quote)
                 .await?
                 .ok_or(Error::UnknownQuote)?;
+            self.check_mint_quote_paid(&mut mint_quote).await?;
+
 
-            if mint_quote.payment_method == PaymentMethod::Bolt11 {
-                self.check_mint_quote_paid(&mut mint_quote).await?;
-            }
         // get the blind signatures before having starting the db transaction, if there are any
         // rollbacks this blind_signatures will be lost, and the signature is stateless. It is not a
         // good idea to call an external service (which is really a trait, it could be anything
@@ -575,7 +616,7 @@ impl Mint {
                 return Err(Error::UnpaidQuote);
             }
             MintQuoteState::Issued => {
-                if mint_quote.payment_method == PaymentMethod::Bolt12
+                if mint_quote.payment_method.is_bolt12()
                     && mint_quote.amount_paid() > mint_quote.amount_issued()
                 {
                     tracing::warn!("Mint quote should state should have been set to issued upon new payment. Something isn't right. Stopping mint");
@@ -586,35 +627,33 @@ impl Mint {
             MintQuoteState::Paid => (),
         }
 
-        if mint_quote.payment_method == PaymentMethod::Bolt12 && mint_quote.pubkey.is_none() {
+        if mint_quote.payment_method.is_bolt12() && mint_quote.pubkey.is_none() {
             tracing::warn!("Bolt12 mint quote created without pubkey");
             return Err(Error::SignatureMissingOrInvalid);
         }
 
-        let mint_amount = match mint_quote.payment_method {
-            PaymentMethod::Bolt11 => {
-                let quote_amount = mint_quote.amount.ok_or(Error::AmountUndefined)?;
-
-                if quote_amount != mint_quote.amount_mintable() {
-                    tracing::error!("The quote amount {} does not equal the amount paid {}.", quote_amount, mint_quote.amount_mintable());
-                    return Err(Error::IncorrectQuoteAmount);
-                }
+        let mint_amount = if mint_quote.payment_method.is_bolt11() {
+            let quote_amount = mint_quote.amount.ok_or(Error::AmountUndefined)?;
 
-                quote_amount
-            },
-            PaymentMethod::Bolt12 => {
-                if mint_quote.amount_mintable() == Amount::ZERO{
-                    tracing::error!(
-                            "Quote state should not be issued if issued {} is => paid {}.",
-                            mint_quote.amount_issued(),
-                            mint_quote.amount_paid()
-                        );
-                    return Err(Error::UnpaidQuote);
-                }
+            if quote_amount != mint_quote.amount_mintable() {
+                tracing::error!("The quote amount {} does not equal the amount paid {}.", quote_amount, mint_quote.amount_mintable());
+                return Err(Error::IncorrectQuoteAmount);
+            }
 
-                mint_quote.amount_mintable()
+            quote_amount
+        } else if mint_quote.payment_method.is_bolt12() {
+            if mint_quote.amount_mintable() == Amount::ZERO{
+                tracing::error!(
+                        "Quote state should not be issued if issued {} is => paid {}.",
+                        mint_quote.amount_issued(),
+                        mint_quote.amount_paid()
+                    );
+                return Err(Error::UnpaidQuote);
             }
-            _ => return Err(Error::UnsupportedPaymentMethod),
+
+            mint_quote.amount_mintable()
+        } else {
+            mint_quote.amount_mintable()
         };
 
         // If the there is a public key provoided in mint quote request
@@ -635,7 +674,7 @@ impl Mint {
             }
         };
 
-        if mint_quote.payment_method == PaymentMethod::Bolt11 {
+        if mint_quote.payment_method.is_bolt11() {
             // For bolt11 we enforce that mint amount == quote amount
             if outputs_amount != mint_amount {
                 return Err(Error::TransactionUnbalanced(

+ 3 - 3
crates/cdk/src/mint/ln.rs

@@ -6,7 +6,7 @@ use cdk_common::common::PaymentProcessorKey;
 use cdk_common::database::DynMintDatabase;
 use cdk_common::mint::MintQuote;
 use cdk_common::payment::DynMintPayment;
-use cdk_common::{Amount, MintQuoteState, PaymentMethod};
+use cdk_common::{Amount, MintQuoteState};
 use tracing::instrument;
 
 use super::subscription::PubSubManager;
@@ -27,7 +27,7 @@ impl Mint {
         // We can just return here and do not need to check with ln node.
         // If quote is issued it is already in a final state,
         // If it is paid ln node will only tell us what we already know
-        if quote.payment_method == PaymentMethod::Bolt11
+        if quote.payment_method.is_bolt11()
             && (state == MintQuoteState::Issued || state == MintQuoteState::Paid)
         {
             return Ok(());
@@ -63,7 +63,7 @@ impl Mint {
 
         let current_state = new_quote.state();
 
-        if new_quote.payment_method == PaymentMethod::Bolt11
+        if new_quote.payment_method.is_bolt11()
             && (current_state == MintQuoteState::Issued || current_state == MintQuoteState::Paid)
         {
             return Ok(());

+ 2 - 1
crates/cdk/src/mint/melt/melt_saga/mod.rs

@@ -5,6 +5,7 @@ use cdk_common::amount::to_unit;
 use cdk_common::database::mint::MeltRequestInfo;
 use cdk_common::database::DynMintDatabase;
 use cdk_common::mint::{MeltSagaState, Operation, Saga, SagaStateEnum};
+use cdk_common::nut00::KnownMethod;
 use cdk_common::nuts::MeltQuoteState;
 use cdk_common::state::check_state_transition;
 use cdk_common::{Amount, Error, ProofsMethods, PublicKey, QuoteId, State};
@@ -508,7 +509,7 @@ impl MeltSaga<SetupComplete> {
         // Mint quote has already been settled
         if (mint_quote.state() == cdk_common::nuts::MintQuoteState::Issued
             || mint_quote.state() == cdk_common::nuts::MintQuoteState::Paid)
-            && mint_quote.payment_method == crate::mint::PaymentMethod::Bolt11
+            && mint_quote.payment_method == crate::mint::PaymentMethod::Known(KnownMethod::Bolt11)
         {
             tx.rollback().await?;
             self.compensate_all().await?;

+ 221 - 44
crates/cdk/src/mint/melt/melt_saga/tests.rs

@@ -11,6 +11,7 @@
 use std::str::FromStr;
 
 use cdk_common::mint::{MeltSagaState, OperationKind, Saga};
+use cdk_common::nut00::KnownMethod;
 use cdk_common::nuts::MeltQuoteState;
 use cdk_common::{Amount, PaymentMethod, ProofsMethods, State};
 
@@ -53,7 +54,11 @@ async fn test_saga_state_persistence_after_setup() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -164,7 +169,11 @@ async fn test_saga_deletion_on_success() {
 
     // Setup
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
     let operation_id = *setup_saga.state_data.operation.id();
@@ -240,7 +249,11 @@ async fn test_crash_recovery_setup_complete() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .expect("Setup should succeed");
 
@@ -311,7 +324,11 @@ async fn test_crash_recovery_multiple_sagas() {
             mint.pubsub_manager(),
         );
         let setup_saga = saga
-            .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+            .setup_melt(
+                &melt_request,
+                verification,
+                PaymentMethod::Known(KnownMethod::Bolt11),
+            )
             .await
             .unwrap();
 
@@ -417,7 +434,11 @@ async fn test_crash_recovery_orphaned_saga() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -541,7 +562,11 @@ async fn test_crash_recovery_internal_settlement() {
     );
 
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
     let operation_id = *setup_saga.state_data.operation.id();
@@ -662,7 +687,11 @@ async fn test_startup_recovery_integration() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -701,7 +730,11 @@ async fn test_startup_recovery_integration() {
         mint.pubsub_manager(),
     );
     let _new_setup = new_saga
-        .setup_melt(&new_request, new_verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &new_request,
+            new_verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -743,7 +776,11 @@ async fn test_compensation_removes_proofs() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -777,7 +814,11 @@ async fn test_compensation_removes_proofs() {
         mint.pubsub_manager(),
     );
     let new_setup = new_saga
-        .setup_melt(&new_request, new_verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &new_request,
+            new_verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .expect("Should be able to reuse proofs after compensation");
 
@@ -826,7 +867,11 @@ async fn test_compensation_removes_change_outputs() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -907,7 +952,11 @@ async fn test_compensation_resets_quote_state() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -962,7 +1011,11 @@ async fn test_compensation_resets_quote_state() {
         mint.pubsub_manager(),
     );
     let _new_setup = new_saga
-        .setup_melt(&new_request, new_verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &new_request,
+            new_verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .expect("Should be able to reuse quote after compensation");
 
@@ -991,7 +1044,11 @@ async fn test_compensation_idempotent() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -1109,7 +1166,11 @@ async fn test_saga_deleted_after_payment_failure() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
     let operation_id = setup_saga.operation_id;
@@ -1189,7 +1250,11 @@ async fn test_saga_content_validation() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -1312,7 +1377,11 @@ async fn test_saga_state_updates_timestamp() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -1378,7 +1447,11 @@ async fn test_get_incomplete_sagas_filters_by_kind() {
         mint.pubsub_manager(),
     );
     let melt_setup = melt_saga
-        .setup_melt(&melt_request, melt_verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            melt_verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -1504,7 +1577,11 @@ async fn test_concurrent_melt_operations() {
                 mint_clone.pubsub_manager(),
             );
             let setup_saga = saga
-                .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+                .setup_melt(
+                    &melt_request,
+                    verification,
+                    PaymentMethod::Known(KnownMethod::Bolt11),
+                )
                 .await
                 .unwrap();
             let operation_id = *setup_saga.state_data.operation.id();
@@ -1566,7 +1643,11 @@ async fn test_concurrent_recovery_and_operations() {
         mint.pubsub_manager(),
     );
     let setup_saga1 = saga1
-        .setup_melt(&melt_request1, verification1, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request1,
+            verification1,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
     let incomplete_operation_id = *setup_saga1.state_data.operation.id();
@@ -1604,7 +1685,11 @@ async fn test_concurrent_recovery_and_operations() {
             mint_for_new_op.pubsub_manager(),
         );
         let setup_saga2 = saga2
-            .setup_melt(&melt_request2, verification2, PaymentMethod::Bolt11)
+            .setup_melt(
+                &melt_request2,
+                verification2,
+                PaymentMethod::Known(KnownMethod::Bolt11),
+            )
             .await
             .unwrap();
         *setup_saga2.state_data.operation.id()
@@ -1647,7 +1732,11 @@ async fn test_double_spend_detection() {
         mint.pubsub_manager(),
     );
     let _setup_saga1 = saga1
-        .setup_melt(&melt_request1, verification1, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request1,
+            verification1,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -1668,7 +1757,11 @@ async fn test_double_spend_detection() {
         mint.pubsub_manager(),
     );
     let setup_result2 = saga2
-        .setup_melt(&melt_request2, verification2, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request2,
+            verification2,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await;
 
     // STEP 5: Verify second setup fails with appropriate error
@@ -1720,7 +1813,11 @@ async fn test_insufficient_funds() {
         mint.pubsub_manager(),
     );
     let setup_result = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await;
 
     // With 10000 msats input and 9000 msats quote, this should succeed
@@ -1765,7 +1862,11 @@ async fn test_invalid_quote_id() {
             mint.pubsub_manager(),
         );
         let setup_result = saga
-            .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+            .setup_melt(
+                &melt_request,
+                verification,
+                PaymentMethod::Known(KnownMethod::Bolt11),
+            )
             .await;
 
         // STEP 4: Verify setup fails with unknown quote error
@@ -1815,7 +1916,11 @@ async fn test_quote_already_paid() {
         mint.pubsub_manager(),
     );
     let setup_saga1 = saga1
-        .setup_melt(&melt_request1, verification1, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request1,
+            verification1,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -1851,7 +1956,11 @@ async fn test_quote_already_paid() {
         mint.pubsub_manager(),
     );
     let setup_result2 = saga2
-        .setup_melt(&melt_request2, verification2, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request2,
+            verification2,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await;
 
     // STEP 4: Verify setup fails
@@ -1892,7 +2001,11 @@ async fn test_quote_already_pending() {
         mint.pubsub_manager(),
     );
     let _setup_saga1 = saga1
-        .setup_melt(&melt_request1, verification1, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request1,
+            verification1,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -1920,7 +2033,11 @@ async fn test_quote_already_pending() {
         mint.pubsub_manager(),
     );
     let setup_result2 = saga2
-        .setup_melt(&melt_request2, verification2, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request2,
+            verification2,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await;
 
     // STEP 4: Verify second setup fails
@@ -2028,7 +2145,11 @@ async fn test_recovery_no_melt_request() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -2080,7 +2201,11 @@ async fn test_recovery_order_on_startup() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -2143,7 +2268,11 @@ async fn test_recovery_order_on_startup() {
         mint.pubsub_manager(),
     );
     let _new_setup = new_saga
-        .setup_melt(&new_request, new_verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &new_request,
+            new_verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -2171,7 +2300,11 @@ async fn test_no_duplicate_recovery() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -2249,7 +2382,11 @@ async fn test_operation_id_uniqueness_and_tracking() {
             mint.pubsub_manager(),
         );
         let setup_saga = saga
-            .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+            .setup_melt(
+                &melt_request,
+                verification,
+                PaymentMethod::Known(KnownMethod::Bolt11),
+            )
             .await
             .unwrap();
 
@@ -2304,7 +2441,11 @@ async fn test_saga_drop_without_finalize() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
     let operation_id = *setup_saga.state_data.operation.id();
@@ -2342,7 +2483,11 @@ async fn test_saga_drop_after_payment() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
     let operation_id = *setup_saga.state_data.operation.id();
@@ -2426,7 +2571,11 @@ async fn test_payment_attempted_state_triggers_ln_check() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
     let operation_id = *setup_saga.state_data.operation.id();
@@ -2516,7 +2665,11 @@ async fn test_setup_complete_state_compensates() {
         mint.pubsub_manager(),
     );
     let setup_saga = saga
-        .setup_melt(&melt_request, verification, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request,
+            verification,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
     let operation_id = *setup_saga.state_data.operation.id();
@@ -2780,7 +2933,11 @@ async fn test_duplicate_lookup_id_prevents_second_pending() {
         mint.pubsub_manager(),
     );
     let setup_saga1 = saga1
-        .setup_melt(&melt_request1, verification1, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request1,
+            verification1,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -2822,7 +2979,11 @@ async fn test_duplicate_lookup_id_prevents_second_pending() {
         mint.pubsub_manager(),
     );
     let setup_result2 = saga2
-        .setup_melt(&melt_request2, verification2, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request2,
+            verification2,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await;
 
     // STEP 5: Verify second setup fails due to duplicate pending lookup_id
@@ -2932,7 +3093,11 @@ async fn test_paid_lookup_id_prevents_pending() {
         mint.pubsub_manager(),
     );
     let setup_saga1 = saga1
-        .setup_melt(&melt_request1, verification1, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request1,
+            verification1,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -2968,7 +3133,11 @@ async fn test_paid_lookup_id_prevents_pending() {
         mint.pubsub_manager(),
     );
     let setup_result2 = saga2
-        .setup_melt(&melt_request2, verification2, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request2,
+            verification2,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await;
 
     // STEP 5: Verify second setup fails due to already paid lookup_id
@@ -3021,7 +3190,11 @@ async fn test_different_lookup_ids_allow_concurrent_pending() {
         mint.pubsub_manager(),
     );
     let _setup_saga1 = saga1
-        .setup_melt(&melt_request1, verification1, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request1,
+            verification1,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 
@@ -3036,7 +3209,11 @@ async fn test_different_lookup_ids_allow_concurrent_pending() {
         mint.pubsub_manager(),
     );
     let _setup_saga2 = saga2
-        .setup_melt(&melt_request2, verification2, PaymentMethod::Bolt11)
+        .setup_melt(
+            &melt_request2,
+            verification2,
+            PaymentMethod::Known(KnownMethod::Bolt11),
+        )
         .await
         .unwrap();
 

+ 132 - 10
crates/cdk/src/mint/melt/mod.rs

@@ -3,12 +3,16 @@ use std::str::FromStr;
 use cdk_common::amount::amount_for_offer;
 use cdk_common::melt::MeltQuoteRequest;
 use cdk_common::mint::MeltPaymentRequest;
+use cdk_common::nut00::KnownMethod;
 use cdk_common::nut05::MeltMethodOptions;
 use cdk_common::payment::{
-    Bolt11OutgoingPaymentOptions, Bolt12OutgoingPaymentOptions, OutgoingPaymentOptions,
+    Bolt11OutgoingPaymentOptions, Bolt12OutgoingPaymentOptions, CustomOutgoingPaymentOptions,
+    OutgoingPaymentOptions,
 };
 use cdk_common::quote_id::QuoteId;
-use cdk_common::{MeltOptions, MeltQuoteBolt12Request, SpendingConditionVerification};
+use cdk_common::{
+    MeltOptions, MeltQuoteBolt12Request, MeltQuoteCustomRequest, SpendingConditionVerification,
+};
 #[cfg(feature = "prometheus")]
 use cdk_prometheus::METRICS;
 use lightning::offers::offer::Offer;
@@ -73,7 +77,7 @@ impl Mint {
                 amount
             }
             Some(MeltOptions::Amountless { amountless: _ }) => {
-                if method == PaymentMethod::Bolt11
+                if method.is_bolt11()
                     && !matches!(
                         settings.options,
                         Some(MeltMethodOptions::Bolt11 { amountless: true })
@@ -107,7 +111,7 @@ impl Mint {
         }
     }
 
-    /// Get melt quote for either BOLT11 or BOLT12
+    /// Get melt quote for BOLT11, BOLT12, or Custom payment methods
     ///
     /// This function accepts a `MeltQuoteRequest` enum and delegates to the
     /// appropriate handler based on the request type.
@@ -123,6 +127,7 @@ impl Mint {
             MeltQuoteRequest::Bolt12(bolt12_request) => {
                 self.get_melt_bolt12_quote_impl(&bolt12_request).await
             }
+            MeltQuoteRequest::Custom(request) => self.get_melt_custom_quote_impl(&request).await,
         }
     }
 
@@ -145,7 +150,7 @@ impl Mint {
             .payment_processors
             .get(&PaymentProcessorKey::new(
                 unit.clone(),
-                PaymentMethod::Bolt11,
+                PaymentMethod::Known(KnownMethod::Bolt11),
             ))
             .ok_or_else(|| {
                 tracing::info!("Could not get ln backend for {}, bolt11 ", unit);
@@ -190,7 +195,7 @@ impl Mint {
         self.check_melt_request_acceptable(
             payment_quote.amount,
             unit.clone(),
-            PaymentMethod::Bolt11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             request.to_string(),
             *options,
         )
@@ -208,7 +213,7 @@ impl Mint {
             unix_time() + melt_ttl,
             payment_quote.request_lookup_id.clone(),
             *options,
-            PaymentMethod::Bolt11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
         );
 
         tracing::debug!(
@@ -255,7 +260,7 @@ impl Mint {
             .payment_processors
             .get(&PaymentProcessorKey::new(
                 unit.clone(),
-                PaymentMethod::Bolt12,
+                PaymentMethod::Known(KnownMethod::Bolt12),
             ))
             .ok_or_else(|| {
                 tracing::info!("Could not get ln backend for {}, bolt12 ", unit);
@@ -296,7 +301,7 @@ impl Mint {
         self.check_melt_request_acceptable(
             payment_quote.amount,
             unit.clone(),
-            PaymentMethod::Bolt12,
+            PaymentMethod::Known(KnownMethod::Bolt12),
             request.clone(),
             *options,
         )
@@ -314,7 +319,7 @@ impl Mint {
             unix_time() + self.quote_ttl().await?.melt_ttl,
             payment_quote.request_lookup_id.clone(),
             *options,
-            PaymentMethod::Bolt12,
+            PaymentMethod::Known(KnownMethod::Bolt12),
         );
 
         tracing::debug!(
@@ -339,6 +344,123 @@ impl Mint {
         Ok(quote.into())
     }
 
+    /// Implementation of get_melt_custom_quote
+    #[instrument(skip_all)]
+    async fn get_melt_custom_quote_impl(
+        &self,
+        melt_request: &MeltQuoteCustomRequest,
+    ) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
+        #[cfg(feature = "prometheus")]
+        METRICS.inc_in_flight_requests("get_melt_custom_quote");
+
+        let MeltQuoteCustomRequest {
+            request,
+            unit,
+            method,
+            extra,
+        } = melt_request;
+
+        let ln = self
+            .payment_processors
+            .get(&PaymentProcessorKey::new(
+                unit.clone(),
+                PaymentMethod::from(method.as_str()),
+            ))
+            .ok_or_else(|| {
+                tracing::info!("Could not get payment processor for {}, {} ", unit, method);
+                Error::UnsupportedUnit
+            })?;
+
+        // Convert extra serde_json::Value to JSON string if not null
+        let extra_json = if extra.is_null() {
+            None
+        } else {
+            Some(extra.to_string())
+        };
+
+        let custom_options =
+            OutgoingPaymentOptions::Custom(Box::new(CustomOutgoingPaymentOptions {
+                method: method.to_string(),
+                request: request.clone(),
+                max_fee_amount: None,
+                timeout_secs: None,
+                melt_options: None,
+                extra_json,
+            }));
+
+        let payment_quote = ln
+            .get_payment_quote(&melt_request.unit, custom_options)
+            .await
+            .map_err(|err| {
+                tracing::error!(
+                    "Could not get payment quote for melt quote, {} {}, {}",
+                    unit,
+                    method,
+                    err
+                );
+
+                #[cfg(feature = "prometheus")]
+                {
+                    METRICS.dec_in_flight_requests("get_melt_custom_quote");
+                    METRICS.record_mint_operation("get_melt_custom_quote", false);
+                    METRICS.record_error();
+                }
+                Error::UnsupportedUnit
+            })?;
+
+        if &payment_quote.unit != unit {
+            return Err(Error::UnitMismatch);
+        }
+
+        // For custom methods, we don't validate amount limits upfront since
+        // the payment processor handles method-specific validation
+        self.check_melt_request_acceptable(
+            payment_quote.amount,
+            unit.clone(),
+            PaymentMethod::from(method.as_str()),
+            request.clone(),
+            None, // Custom methods don't use options
+        )
+        .await?;
+
+        let melt_ttl = self.quote_ttl().await?.melt_ttl;
+
+        let quote = MeltQuote::new(
+            MeltPaymentRequest::Custom {
+                method: method.to_string(),
+                request: request.clone(),
+            },
+            unit.clone(),
+            payment_quote.amount,
+            payment_quote.fee,
+            unix_time() + melt_ttl,
+            payment_quote.request_lookup_id.clone(),
+            None, // Custom methods don't use options
+            PaymentMethod::from(method.as_str()),
+        );
+
+        tracing::debug!(
+            "New {} melt quote {} for {} {} with request id {:?}",
+            method,
+            quote.id,
+            payment_quote.amount,
+            unit,
+            payment_quote.request_lookup_id
+        );
+
+        let mut tx = self.localstore.begin_transaction().await?;
+        tx.add_melt_quote(quote.clone()).await?;
+        tx.commit().await?;
+
+        #[cfg(feature = "prometheus")]
+        {
+            METRICS.dec_in_flight_requests("get_melt_custom_quote");
+            METRICS.record_mint_operation("get_melt_custom_quote", true);
+        }
+
+        Ok(quote.into())
+    }
+
     /// Check melt quote status
     #[instrument(skip(self))]
     pub async fn check_melt_quote(

+ 33 - 1
crates/cdk/src/mint/mod.rs

@@ -46,6 +46,7 @@ mod verification;
 
 pub use builder::{MintBuilder, MintMeltLimits};
 pub use cdk_common::mint::{MeltQuote, MintKeySetInfo, MintQuote};
+pub use issue::{MintQuoteRequest, MintQuoteResponse};
 pub use verification::Verification;
 
 const CDK_MINT_PRIMARY_NAMESPACE: &str = "cdk_mint";
@@ -398,6 +399,37 @@ impl Mint {
         Ok(())
     }
 
+    /// Get all custom payment methods supported by registered payment processors
+    ///
+    /// This queries all payment processors for their supported custom methods
+    /// and returns a deduplicated list.
+    pub async fn get_custom_payment_methods(&self) -> Result<Vec<String>, Error> {
+        use std::collections::HashSet;
+        let mut custom_methods = HashSet::new();
+        let mut seen_processors = Vec::new();
+
+        for processor in self.payment_processors.values() {
+            // Skip if we've already queried this processor instance
+            if seen_processors.iter().any(|p| Arc::ptr_eq(p, processor)) {
+                continue;
+            }
+            seen_processors.push(Arc::clone(processor));
+
+            match processor.get_settings().await {
+                Ok(settings) => {
+                    for (method, _) in settings.custom {
+                        custom_methods.insert(method);
+                    }
+                }
+                Err(e) => {
+                    tracing::warn!("Failed to get settings from payment processor: {}", e);
+                }
+            }
+        }
+
+        Ok(custom_methods.into_iter().collect())
+    }
+
     /// Get the payment processor for the given unit and payment method
     pub fn get_payment_processor(
         &self,
@@ -727,7 +759,7 @@ impl Mint {
             .payment_ids()
             .contains(&&wait_payment_response.payment_id)
         {
-            if mint_quote.payment_method == PaymentMethod::Bolt11
+            if mint_quote.payment_method.is_bolt11()
                 && (quote_state == MintQuoteState::Issued || quote_state == MintQuoteState::Paid)
             {
                 tracing::info!("Received payment notification for already issued quote.");

+ 20 - 18
crates/cdk/src/mint/subscription.rs

@@ -13,7 +13,7 @@ use cdk_common::pub_sub::{Pubsub, Spec, Subscriber};
 use cdk_common::subscription::SubId;
 use cdk_common::{
     Amount, BlindSignature, MeltQuoteBolt11Response, MeltQuoteState, MintQuoteBolt11Response,
-    MintQuoteBolt12Response, MintQuoteState, PaymentMethod, ProofState, PublicKey, QuoteId,
+    MintQuoteBolt12Response, MintQuoteState, ProofState, PublicKey, QuoteId,
 };
 
 use super::Mint;
@@ -99,21 +99,23 @@ impl MintPubSubSpec {
                         quotes
                             .into_iter()
                             .filter_map(|quote| {
-                                quote.and_then(|mint_quotes| match mint_quotes.payment_method {
-                                    PaymentMethod::Bolt11 => {
-                                        let response: MintQuoteBolt11Response<QuoteId> =
-                                            mint_quotes.into();
-                                        Some(response.into())
-                                    }
-                                    PaymentMethod::Bolt12 => match mint_quotes.try_into() {
-                                        Ok(response) => {
-                                            let response: MintQuoteBolt12Response<QuoteId> =
-                                                response;
+                                quote.and_then(|mint_quotes| {
+                                    match mint_quotes.payment_method.as_str() {
+                                        "bolt11" => {
+                                            let response: MintQuoteBolt11Response<QuoteId> =
+                                                mint_quotes.into();
                                             Some(response.into())
                                         }
-                                        Err(_) => None,
-                                    },
-                                    PaymentMethod::Custom(_) => None,
+                                        "bolt12" => match mint_quotes.try_into() {
+                                            Ok(response) => {
+                                                let response: MintQuoteBolt12Response<QuoteId> =
+                                                    response;
+                                                Some(response.into())
+                                            }
+                                            Err(_) => None,
+                                        },
+                                        _ => None,
+                                    }
                                 })
                             })
                             .collect::<Vec<_>>()
@@ -193,10 +195,10 @@ impl PubSubManager {
     /// Helper function to publish even of a mint quote being paid
     pub fn mint_quote_issue(&self, mint_quote: &MintQuote, total_issued: Amount) {
         match mint_quote.payment_method {
-            PaymentMethod::Bolt11 => {
+            cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => {
                 self.mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Issued);
             }
-            PaymentMethod::Bolt12 => {
+            cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt12) => {
                 self.mint_quote_bolt12_status(
                     mint_quote.clone(),
                     mint_quote.amount_paid(),
@@ -212,10 +214,10 @@ impl PubSubManager {
     /// Helper function to publish even of a mint quote being paid
     pub fn mint_quote_payment(&self, mint_quote: &MintQuote, total_paid: Amount) {
         match mint_quote.payment_method {
-            PaymentMethod::Bolt11 => {
+            cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => {
                 self.mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Paid);
             }
-            PaymentMethod::Bolt12 => {
+            cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt12) => {
                 self.mint_quote_bolt12_status(
                     mint_quote.clone(),
                     total_paid,

+ 2 - 1
crates/cdk/src/test_helpers/mint.rs

@@ -10,6 +10,7 @@ use std::time::Duration;
 use bip39::Mnemonic;
 use cdk_common::amount::SplitTarget;
 use cdk_common::dhke::construct_proofs;
+use cdk_common::nut00::KnownMethod;
 use cdk_common::nuts::{BlindedMessage, CurrencyUnit, Id, PaymentMethod, PreMintSecrets, Proofs};
 use cdk_common::{
     Amount, MintQuoteBolt11Request, MintQuoteBolt11Response, MintQuoteState, MintRequest,
@@ -93,7 +94,7 @@ pub async fn create_test_mint() -> Result<Mint, Error> {
     mint_builder
         .add_payment_processor(
             CurrencyUnit::Sat,
-            PaymentMethod::Bolt11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             MintMeltLimits::new(1, 10_000),
             Arc::new(ln_fake_backend),
         )

+ 9 - 5
crates/cdk/src/wallet/issue/issue_bolt11.rs → crates/cdk/src/wallet/issue/bolt11.rs

@@ -1,5 +1,6 @@
 use std::collections::HashMap;
 
+use cdk_common::nut00::KnownMethod;
 use cdk_common::nut04::MintMethodOptions;
 use cdk_common::wallet::{MintQuote, Transaction, TransactionDirection};
 use cdk_common::PaymentMethod;
@@ -59,7 +60,10 @@ impl Wallet {
             let settings = mint_info
                 .nuts
                 .nut04
-                .get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11)
+                .get_settings(
+                    &unit,
+                    &crate::nuts::PaymentMethod::Known(KnownMethod::Bolt11),
+                )
                 .ok_or(Error::UnsupportedUnit)?;
 
             match settings.options {
@@ -82,7 +86,7 @@ impl Wallet {
         let quote = MintQuote::new(
             quote_res.quote,
             mint_url,
-            PaymentMethod::Bolt11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             Some(amount),
             unit,
             quote_res.request,
@@ -132,7 +136,7 @@ impl Wallet {
 
         for mint_quote in mint_quotes {
             match mint_quote.payment_method {
-                PaymentMethod::Bolt11 => {
+                PaymentMethod::Known(KnownMethod::Bolt11) => {
                     let mint_quote_response = self.mint_quote_state(&mint_quote.id).await?;
 
                     if mint_quote_response.state == MintQuoteState::Paid {
@@ -142,7 +146,7 @@ impl Wallet {
                         total_amount += proofs.total_amount()?;
                     }
                 }
-                PaymentMethod::Bolt12 => {
+                PaymentMethod::Known(KnownMethod::Bolt12) => {
                     let mint_quote_response = self.mint_bolt12_quote_state(&mint_quote.id).await?;
                     if mint_quote_response.amount_paid > mint_quote_response.amount_issued {
                         let proofs = self
@@ -235,7 +239,7 @@ impl Wallet {
             .await?
             .ok_or(Error::UnknownQuote)?;
 
-        if quote_info.payment_method != PaymentMethod::Bolt11 {
+        if quote_info.payment_method != PaymentMethod::Known(KnownMethod::Bolt11) {
             return Err(Error::UnsupportedPaymentMethod);
         }
 

+ 6 - 2
crates/cdk/src/wallet/issue/issue_bolt12.rs → crates/cdk/src/wallet/issue/bolt12.rs

@@ -1,5 +1,6 @@
 use std::collections::HashMap;
 
+use cdk_common::nut00::KnownMethod;
 use cdk_common::nut04::MintMethodOptions;
 use cdk_common::nut25::MintQuoteBolt12Request;
 use cdk_common::wallet::{Transaction, TransactionDirection};
@@ -36,7 +37,10 @@ impl Wallet {
             let mint_method_settings = mint_info
                 .nuts
                 .nut04
-                .get_settings(unit, &crate::nuts::PaymentMethod::Bolt12)
+                .get_settings(
+                    unit,
+                    &crate::nuts::PaymentMethod::Known(KnownMethod::Bolt12),
+                )
                 .ok_or(Error::UnsupportedUnit)?;
 
             match mint_method_settings.options {
@@ -59,7 +63,7 @@ impl Wallet {
         let quote = MintQuote::new(
             quote_res.quote,
             mint_url,
-            PaymentMethod::Bolt12,
+            PaymentMethod::Known(KnownMethod::Bolt12),
             amount,
             unit.clone(),
             quote_res.request,

+ 237 - 0
crates/cdk/src/wallet/issue/custom.rs

@@ -0,0 +1,237 @@
+use std::collections::HashMap;
+
+use cdk_common::nut04::MintMethodOptions;
+use cdk_common::wallet::{MintQuote, Transaction, TransactionDirection};
+use cdk_common::{Proofs, SecretKey};
+use tracing::instrument;
+
+use crate::amount::SplitTarget;
+use crate::dhke::construct_proofs;
+use crate::nuts::nut00::ProofsMethods;
+use crate::nuts::{
+    nut12, MintQuoteCustomRequest, MintRequest, PaymentMethod, PreMintSecrets, SpendingConditions,
+    State,
+};
+use crate::types::ProofInfo;
+use crate::util::unix_time;
+use crate::{Amount, Error, Wallet};
+
+impl Wallet {
+    /// Mint Quote for Custom Payment Method
+    #[instrument(skip(self))]
+    pub(super) async fn mint_quote_custom(
+        &self,
+        amount: Option<Amount>,
+        method: &str,
+        description: Option<String>,
+        extra: Option<String>,
+    ) -> Result<MintQuote, Error> {
+        let mint_url = self.mint_url.clone();
+        let unit = &self.unit;
+
+        self.refresh_keysets().await?;
+
+        // If we have a description, we check that the mint supports it.
+        if description.is_some() {
+            let payment_method = PaymentMethod::Custom(method.to_string());
+            let mint_method_settings = self
+                .localstore
+                .get_mint(mint_url.clone())
+                .await?
+                .ok_or(Error::IncorrectMint)?
+                .nuts
+                .nut04
+                .get_settings(unit, &payment_method)
+                .ok_or(Error::UnsupportedUnit)?;
+
+            match mint_method_settings.options {
+                Some(MintMethodOptions::Bolt11 { description }) if description => (),
+                _ => return Err(Error::InvoiceDescriptionUnsupported),
+            }
+        }
+
+        let secret_key = SecretKey::generate();
+
+        let amount = amount.ok_or(Error::AmountUndefined)?;
+
+        let mint_request = MintQuoteCustomRequest {
+            amount,
+            unit: self.unit.clone(),
+            description,
+            pubkey: Some(secret_key.public_key()),
+            extra: serde_json::from_str(&extra.unwrap_or_default())?,
+        };
+
+        let quote_res = self
+            .client
+            .post_mint_custom_quote(method, mint_request)
+            .await?;
+
+        let quote = MintQuote::new(
+            quote_res.quote,
+            mint_url,
+            PaymentMethod::Custom(method.to_string()),
+            Some(amount),
+            unit.clone(),
+            quote_res.request,
+            quote_res.expiry.unwrap_or(0),
+            Some(secret_key),
+        );
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        tx.add_mint_quote(quote.clone()).await?;
+        tx.commit().await?;
+        Ok(quote)
+    }
+
+    /// Mint with custom payment method
+    /// This is used for all custom payment methods - delegates to existing mint logic
+    #[instrument(skip(self))]
+    pub(super) async fn mint_custom(
+        &self,
+        quote_id: &str,
+        amount_split_target: SplitTarget,
+        spending_conditions: Option<SpendingConditions>,
+    ) -> Result<Proofs, Error> {
+        self.refresh_keysets().await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        let quote_info = self
+            .localstore
+            .get_mint_quote(quote_id)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        // Verify it's a custom payment method
+        if !quote_info.payment_method.is_custom() {
+            return Err(Error::UnsupportedPaymentMethod);
+        }
+
+        let amount_mintable = quote_info.amount_mintable();
+
+        if amount_mintable == Amount::ZERO {
+            tracing::debug!("Amount mintable 0.");
+            return Err(Error::AmountUndefined);
+        }
+
+        let unix_time = unix_time();
+
+        if quote_info.expiry > unix_time {
+            tracing::warn!("Attempting to mint with expired quote.");
+        }
+
+        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        let fee_and_amounts = self
+            .get_keyset_fees_and_amounts_by_id(active_keyset_id)
+            .await?;
+
+        let premint_secrets = match &spending_conditions {
+            Some(spending_conditions) => PreMintSecrets::with_conditions(
+                active_keyset_id,
+                amount_mintable,
+                &amount_split_target,
+                spending_conditions,
+                &fee_and_amounts,
+            )?,
+            None => {
+                // Calculate how many secrets we'll need
+                let amount_split =
+                    amount_mintable.split_targeted(&amount_split_target, &fee_and_amounts)?;
+                let num_secrets = amount_split.len() as u32;
+
+                tracing::debug!(
+                    "Incrementing keyset {} counter by {}",
+                    active_keyset_id,
+                    num_secrets
+                );
+
+                // Atomically get the counter range we need
+                let new_counter = tx
+                    .increment_keyset_counter(&active_keyset_id, num_secrets)
+                    .await?;
+
+                let count = new_counter - num_secrets;
+
+                PreMintSecrets::from_seed(
+                    active_keyset_id,
+                    count,
+                    &self.seed,
+                    amount_mintable,
+                    &amount_split_target,
+                    &fee_and_amounts,
+                )?
+            }
+        };
+
+        let mut request = MintRequest {
+            quote: quote_id.to_string(),
+            outputs: premint_secrets.blinded_messages(),
+            signature: None,
+        };
+
+        if let Some(secret_key) = quote_info.secret_key {
+            request.sign(secret_key)?;
+        }
+
+        let mint_res = self.client.post_mint(request).await?;
+
+        let keys = self.load_keyset_keys(active_keyset_id).await?;
+
+        // Verify the signature DLEQ is valid
+        {
+            for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
+                let keys = self.load_keyset_keys(sig.keyset_id).await?;
+                let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
+                match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
+                    Ok(_) | Err(nut12::Error::MissingDleqProof) => (),
+                    Err(_) => return Err(Error::CouldNotVerifyDleq),
+                }
+            }
+        }
+
+        let proofs = construct_proofs(
+            mint_res.signatures,
+            premint_secrets.rs(),
+            premint_secrets.secrets(),
+            &keys,
+        )?;
+
+        // Remove filled quote from store
+        tx.remove_mint_quote(&quote_info.id).await?;
+
+        let proof_infos = proofs
+            .iter()
+            .map(|proof| {
+                ProofInfo::new(
+                    proof.clone(),
+                    self.mint_url.clone(),
+                    State::Unspent,
+                    quote_info.unit.clone(),
+                )
+            })
+            .collect::<Result<Vec<ProofInfo>, _>>()?;
+
+        // Add new proofs to store
+        tx.update_proofs(proof_infos, vec![]).await?;
+
+        // Add transaction to store
+        tx.add_transaction(Transaction {
+            mint_url: self.mint_url.clone(),
+            direction: TransactionDirection::Incoming,
+            amount: proofs.total_amount()?,
+            fee: Amount::ZERO,
+            unit: self.unit.clone(),
+            ys: proofs.ys()?,
+            timestamp: unix_time,
+            memo: None,
+            metadata: HashMap::new(),
+            quote_id: Some(quote_id.to_string()),
+            payment_request: Some(quote_info.request),
+            payment_proof: None,
+            payment_method: Some(quote_info.payment_method),
+        })
+        .await?;
+        tx.commit().await?;
+        Ok(proofs)
+    }
+}

+ 72 - 2
crates/cdk/src/wallet/issue/mod.rs

@@ -1,2 +1,72 @@
-mod issue_bolt11;
-mod issue_bolt12;
+mod bolt11;
+mod bolt12;
+mod custom;
+
+use cdk_common::PaymentMethod;
+
+use crate::amount::SplitTarget;
+use crate::nuts::nut00::KnownMethod;
+use crate::nuts::{Proofs, SpendingConditions};
+use crate::wallet::MintQuote;
+use crate::{Amount, Error, Wallet};
+
+impl Wallet {
+    /// Unified mint quote method for all payment methods
+    /// Routes to the appropriate handler based on the payment method
+    pub async fn mint_quote_unified(
+        &self,
+        amount: Option<Amount>,
+        method: PaymentMethod,
+        description: Option<String>,
+        extra: Option<String>,
+    ) -> Result<MintQuote, Error> {
+        match method {
+            PaymentMethod::Known(KnownMethod::Bolt11) => {
+                // For bolt11, request should be empty or ignored, amount is required
+                let amount = amount.ok_or(Error::AmountUndefined)?;
+                self.mint_quote(amount, description).await
+            }
+            PaymentMethod::Known(KnownMethod::Bolt12) => {
+                // For bolt12, request is the offer string
+                self.mint_bolt12_quote(amount, description).await
+            }
+            PaymentMethod::Custom(custom_method) => {
+                self.mint_quote_custom(amount, &custom_method, description, extra)
+                    .await
+            }
+        }
+    }
+
+    /// Unified mint method for all payment methods
+    /// Routes to the appropriate handler based on the payment method stored in the quote
+    pub async fn mint_unified(
+        &self,
+        quote_id: &str,
+        amount: Option<Amount>,
+        amount_split_target: SplitTarget,
+        spending_conditions: Option<SpendingConditions>,
+    ) -> Result<Proofs, Error> {
+        // Fetch the quote to determine the payment method
+        let quote_info = self
+            .localstore
+            .get_mint_quote(quote_id)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        match quote_info.payment_method {
+            PaymentMethod::Known(KnownMethod::Bolt11) => {
+                // Bolt11 doesn't need amount parameter
+                self.mint(quote_id, amount_split_target, spending_conditions)
+                    .await
+            }
+            PaymentMethod::Known(KnownMethod::Bolt12) => {
+                self.mint_bolt12(quote_id, amount, amount_split_target, spending_conditions)
+                    .await
+            }
+            PaymentMethod::Custom(_) => {
+                self.mint_custom(quote_id, amount_split_target, spending_conditions)
+                    .await
+            }
+        }
+    }
+}

+ 15 - 7
crates/cdk/src/wallet/melt/melt_bolt11.rs → crates/cdk/src/wallet/melt/bolt11.rs

@@ -2,6 +2,7 @@ use std::collections::HashMap;
 use std::str::FromStr;
 
 use cdk_common::amount::SplitTarget;
+use cdk_common::nut00::KnownMethod;
 use cdk_common::wallet::{Transaction, TransactionDirection};
 use cdk_common::PaymentMethod;
 use lightning_invoice::Bolt11Invoice;
@@ -88,7 +89,7 @@ impl Wallet {
             state: quote_res.state,
             expiry: quote_res.expiry,
             payment_preimage: quote_res.payment_preimage,
-            payment_method: PaymentMethod::Bolt11,
+            payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
         };
 
         let mut tx = self.localstore.begin_db_transaction().await?;
@@ -211,22 +212,28 @@ impl Wallet {
         tx.commit().await?;
 
         let melt_response = match quote_info.payment_method {
-            cdk_common::PaymentMethod::Bolt11 => {
+            cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => {
                 self.try_proof_operation_or_reclaim(
                     request.inputs().clone(),
                     self.client.post_melt(request),
                 )
                 .await?
             }
-            cdk_common::PaymentMethod::Bolt12 => {
+            cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt12) => {
                 self.try_proof_operation_or_reclaim(
                     request.inputs().clone(),
                     self.client.post_melt_bolt12(request),
                 )
                 .await?
             }
-            cdk_common::PaymentMethod::Custom(_) => {
-                return Err(Error::UnsupportedPaymentMethod);
+            cdk_common::PaymentMethod::Custom(ref _method) => {
+                // For now, custom methods will use the same post_melt endpoint
+                // This will be enhanced when custom HTTP client methods are added
+                self.try_proof_operation_or_reclaim(
+                    request.inputs().clone(),
+                    self.client.post_melt(request),
+                )
+                .await?
             }
         };
 
@@ -258,10 +265,11 @@ impl Wallet {
         };
 
         let payment_preimage = melt_response.payment_preimage.clone();
+        let state = melt_response.state;
 
         let melted = Melted::from_proofs(
-            melt_response.state,
-            melt_response.payment_preimage,
+            state,
+            payment_preimage.clone(),
             quote_info.amount,
             proofs.clone(),
             change_proofs.clone(),

+ 2 - 1
crates/cdk/src/wallet/melt/melt_bolt12.rs → crates/cdk/src/wallet/melt/bolt12.rs

@@ -5,6 +5,7 @@
 use std::str::FromStr;
 
 use cdk_common::amount::amount_for_offer;
+use cdk_common::nut00::KnownMethod;
 use cdk_common::wallet::MeltQuote;
 use cdk_common::PaymentMethod;
 use lightning::offers::offer::Offer;
@@ -58,7 +59,7 @@ impl Wallet {
             state: quote_res.state,
             expiry: quote_res.expiry,
             payment_preimage: quote_res.payment_preimage,
-            payment_method: PaymentMethod::Bolt12,
+            payment_method: PaymentMethod::Known(KnownMethod::Bolt12),
         };
 
         let mut tx = self.localstore.begin_db_transaction().await?;

+ 51 - 0
crates/cdk/src/wallet/melt/custom.rs

@@ -0,0 +1,51 @@
+use cdk_common::wallet::MeltQuote;
+use cdk_common::PaymentMethod;
+use tracing::instrument;
+
+use crate::nuts::{MeltOptions, MeltQuoteCustomRequest};
+use crate::{Error, Wallet};
+
+impl Wallet {
+    /// Melt Quote for Custom Payment Method
+    ///
+    /// # Arguments
+    /// * `method` - Custom payment method name
+    /// * `request` - Payment request string (method-specific format)
+    /// * `_options` - Melt options (currently unused for custom methods)
+    /// * `extra` - Optional extra payment-method-specific data as JSON
+    #[instrument(skip(self, request, extra))]
+    pub(super) async fn melt_quote_custom(
+        &self,
+        method: &str,
+        request: String,
+        _options: Option<MeltOptions>,
+        extra: Option<serde_json::Value>,
+    ) -> Result<MeltQuote, Error> {
+        self.refresh_keysets().await?;
+
+        let quote_request = MeltQuoteCustomRequest {
+            method: method.to_string(),
+            request: request.clone(),
+            unit: self.unit.clone(),
+            extra: extra.unwrap_or(serde_json::Value::Null),
+        };
+        let quote_res = self.client.post_melt_custom_quote(quote_request).await?;
+
+        let quote = MeltQuote {
+            id: quote_res.quote,
+            amount: quote_res.amount,
+            request,
+            unit: self.unit.clone(),
+            fee_reserve: quote_res.fee_reserve,
+            state: quote_res.state,
+            expiry: quote_res.expiry,
+            payment_preimage: quote_res.payment_preimage,
+            payment_method: PaymentMethod::Custom(method.to_string()),
+        };
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.add_melt_quote(quote.clone()).await?;
+        tx.commit().await?;
+
+        Ok(quote)
+    }
+}

+ 38 - 4
crates/cdk/src/wallet/melt/mod.rs

@@ -3,15 +3,20 @@ use std::collections::HashMap;
 use cdk_common::database::DynWalletDatabaseTransaction;
 use cdk_common::util::unix_time;
 use cdk_common::wallet::{MeltQuote, Transaction, TransactionDirection};
-use cdk_common::{Error, MeltQuoteBolt11Response, MeltQuoteState, ProofsMethods, State};
+use cdk_common::{
+    Error, MeltQuoteBolt11Response, MeltQuoteState, PaymentMethod, ProofsMethods, State,
+};
 use tracing::instrument;
 
+use crate::nuts::nut00::KnownMethod;
+use crate::nuts::MeltOptions;
 use crate::Wallet;
 
+mod bolt11;
+mod bolt12;
+mod custom;
 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
 mod melt_bip353;
-mod melt_bolt11;
-mod melt_bolt12;
 #[cfg(feature = "wallet")]
 mod melt_lightning_address;
 
@@ -126,7 +131,7 @@ impl Wallet {
             .nut05
             .methods
             .iter()
-            .any(|m| m.method == PaymentMethod::Bolt12);
+            .any(|m| m.method == PaymentMethod::Known(KnownMethod::Bolt12));
 
         if supports_bolt12 {
             // Mint supports bolt12, try BIP353 first
@@ -151,4 +156,33 @@ impl Wallet {
             self.melt_lightning_address_quote(address, amount).await
         }
     }
+    /// Unified melt quote method for all payment methods
+    ///
+    /// Routes to the appropriate handler based on the payment method.
+    /// For custom payment methods, you can pass extra JSON data that will be
+    /// forwarded to the payment processor.
+    ///
+    /// # Arguments
+    /// * `method` - Payment method to use (bolt11, bolt12, or custom)
+    /// * `request` - Payment request string (invoice, offer, or custom format)
+    /// * `options` - Optional melt options (MPP, amountless, etc.)
+    /// * `extra` - Optional extra payment-method-specific data as JSON (for custom methods)
+    pub async fn melt_quote_unified(
+        &self,
+        method: PaymentMethod,
+        request: String,
+        options: Option<MeltOptions>,
+        extra: Option<serde_json::Value>,
+    ) -> Result<MeltQuote, Error> {
+        match method {
+            PaymentMethod::Known(KnownMethod::Bolt11) => self.melt_quote(request, options).await,
+            PaymentMethod::Known(KnownMethod::Bolt12) => {
+                self.melt_bolt12_quote(request, options).await
+            }
+            PaymentMethod::Custom(custom_method) => {
+                self.melt_quote_custom(&custom_method, request, options, extra)
+                    .await
+            }
+        }
+    }
 }

+ 104 - 25
crates/cdk/src/wallet/mint_connector/http_client.rs

@@ -18,11 +18,14 @@ use super::transport::Transport;
 use super::{Error, MintConnector};
 use crate::mint_url::MintUrl;
 #[cfg(feature = "auth")]
+use crate::nuts::nut00::{KnownMethod, PaymentMethod};
+#[cfg(feature = "auth")]
 use crate::nuts::nut22::MintAuthRequest;
 use crate::nuts::{
     AuthToken, CheckStateRequest, CheckStateResponse, Id, KeySet, KeysResponse, KeysetResponse,
-    MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
-    MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
+    MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteCustomRequest, MeltRequest, MintInfo,
+    MintQuoteBolt11Request, MintQuoteBolt11Response, MintQuoteCustomRequest,
+    MintQuoteCustomResponse, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
     SwapRequest, SwapResponse,
 };
 #[cfg(feature = "auth")]
@@ -155,7 +158,7 @@ where
             .map(|cache_support| {
                 cache_support
                     .1
-                    .get(&(method, path))
+                    .get(&(method, path.clone()))
                     .map(|_| cache_support.0)
             })
             .unwrap_or_default()
@@ -164,14 +167,16 @@ where
 
         let transport = self.transport.clone();
         loop {
-            let url = self.mint_url.join_paths(&match path {
-                nut19::Path::MintBolt11 => vec!["v1", "mint", "bolt11"],
-                nut19::Path::MeltBolt11 => vec!["v1", "melt", "bolt11"],
-                nut19::Path::MintBolt12 => vec!["v1", "mint", "bolt12"],
-
-                nut19::Path::MeltBolt12 => vec!["v1", "melt", "bolt12"],
-                nut19::Path::Swap => vec!["v1", "swap"],
-            })?;
+            let url = match &path {
+                nut19::Path::Swap => self.mint_url.join_paths(&["v1", "swap"])?,
+                nut19::Path::Custom(custom_path) => {
+                    // Custom paths should be in the format "/v1/mint/{method}" or "/v1/melt/{method}"
+                    // Remove leading slash if present
+                    let path_str = custom_path.trim_start_matches('/');
+                    let parts: Vec<&str> = path_str.split('/').collect();
+                    self.mint_url.join_paths(&parts)?
+                }
+            };
 
             let result = match method {
                 nut19::Method::Get => transport.http_get(url, auth_token.clone()).await,
@@ -284,7 +289,10 @@ where
 
         #[cfg(feature = "auth")]
         let auth_token = self
-            .get_auth_token(Method::Post, RoutePath::MintQuoteBolt11)
+            .get_auth_token(
+                Method::Post,
+                RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+            )
             .await?;
 
         #[cfg(not(feature = "auth"))]
@@ -305,7 +313,10 @@ where
 
         #[cfg(feature = "auth")]
         let auth_token = self
-            .get_auth_token(Method::Get, RoutePath::MintQuoteBolt11)
+            .get_auth_token(
+                Method::Get,
+                RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+            )
             .await?;
 
         #[cfg(not(feature = "auth"))]
@@ -318,14 +329,17 @@ where
     async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error> {
         #[cfg(feature = "auth")]
         let auth_token = self
-            .get_auth_token(Method::Post, RoutePath::MintBolt11)
+            .get_auth_token(
+                Method::Post,
+                RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+            )
             .await?;
 
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         self.retriable_http_request(
             nut19::Method::Post,
-            nut19::Path::MintBolt11,
+            nut19::Path::Custom("/v1/mint/bolt11".to_string()),
             auth_token,
             &request,
         )
@@ -343,7 +357,10 @@ where
             .join_paths(&["v1", "melt", "quote", "bolt11"])?;
         #[cfg(feature = "auth")]
         let auth_token = self
-            .get_auth_token(Method::Post, RoutePath::MeltQuoteBolt11)
+            .get_auth_token(
+                Method::Post,
+                RoutePath::MeltQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+            )
             .await?;
 
         #[cfg(not(feature = "auth"))]
@@ -363,7 +380,10 @@ where
 
         #[cfg(feature = "auth")]
         let auth_token = self
-            .get_auth_token(Method::Get, RoutePath::MeltQuoteBolt11)
+            .get_auth_token(
+                Method::Get,
+                RoutePath::MeltQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+            )
             .await?;
 
         #[cfg(not(feature = "auth"))]
@@ -380,7 +400,10 @@ where
     ) -> Result<MeltQuoteBolt11Response<String>, Error> {
         #[cfg(feature = "auth")]
         let auth_token = self
-            .get_auth_token(Method::Post, RoutePath::MeltBolt11)
+            .get_auth_token(
+                Method::Post,
+                RoutePath::Melt(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
+            )
             .await?;
 
         #[cfg(not(feature = "auth"))]
@@ -388,7 +411,7 @@ where
 
         self.retriable_http_request(
             nut19::Method::Post,
-            nut19::Path::MeltBolt11,
+            nut19::Path::Custom("/v1/melt/bolt11".to_string()),
             auth_token,
             &request,
         )
@@ -488,7 +511,10 @@ where
 
         #[cfg(feature = "auth")]
         let auth_token = self
-            .get_auth_token(Method::Post, RoutePath::MintQuoteBolt12)
+            .get_auth_token(
+                Method::Post,
+                RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
+            )
             .await?;
 
         #[cfg(not(feature = "auth"))]
@@ -509,7 +535,10 @@ where
 
         #[cfg(feature = "auth")]
         let auth_token = self
-            .get_auth_token(Method::Get, RoutePath::MintQuoteBolt12)
+            .get_auth_token(
+                Method::Get,
+                RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
+            )
             .await?;
 
         #[cfg(not(feature = "auth"))]
@@ -528,7 +557,10 @@ where
             .join_paths(&["v1", "melt", "quote", "bolt12"])?;
         #[cfg(feature = "auth")]
         let auth_token = self
-            .get_auth_token(Method::Post, RoutePath::MeltQuoteBolt12)
+            .get_auth_token(
+                Method::Post,
+                RoutePath::MeltQuote(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
+            )
             .await?;
 
         #[cfg(not(feature = "auth"))]
@@ -548,7 +580,10 @@ where
 
         #[cfg(feature = "auth")]
         let auth_token = self
-            .get_auth_token(Method::Get, RoutePath::MeltQuoteBolt12)
+            .get_auth_token(
+                Method::Get,
+                RoutePath::MeltQuote(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
+            )
             .await?;
 
         #[cfg(not(feature = "auth"))]
@@ -564,19 +599,63 @@ where
     ) -> Result<MeltQuoteBolt11Response<String>, Error> {
         #[cfg(feature = "auth")]
         let auth_token = self
-            .get_auth_token(Method::Post, RoutePath::MeltBolt12)
+            .get_auth_token(
+                Method::Post,
+                RoutePath::Melt(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
+            )
             .await?;
 
         #[cfg(not(feature = "auth"))]
         let auth_token = None;
         self.retriable_http_request(
             nut19::Method::Post,
-            nut19::Path::MeltBolt12,
+            nut19::Path::Custom("/v1/melt/bolt12".to_string()),
             auth_token,
             &request,
         )
         .await
     }
+
+    /// Mint Quote for Custom Payment Method
+    #[instrument(skip(self), fields(mint_url = %self.mint_url))]
+    async fn post_mint_custom_quote(
+        &self,
+        method: &str,
+        request: MintQuoteCustomRequest,
+    ) -> Result<MintQuoteCustomResponse<String>, Error> {
+        let url = self.mint_url.join_paths(&["v1", "mint", "quote", method])?;
+
+        #[cfg(feature = "auth")]
+        let auth_token = self
+            .get_auth_token(Method::Post, RoutePath::MintQuote(method.to_string()))
+            .await?;
+
+        #[cfg(not(feature = "auth"))]
+        let auth_token = None;
+
+        self.transport.http_post(url, auth_token, &request).await
+    }
+
+    /// Melt Quote for Custom Payment Method
+    #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
+    async fn post_melt_custom_quote(
+        &self,
+        request: MeltQuoteCustomRequest,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+        let url = self
+            .mint_url
+            .join_paths(&["v1", "melt", "quote", &request.method])?;
+
+        #[cfg(feature = "auth")]
+        let auth_token = self
+            .get_auth_token(Method::Post, RoutePath::MeltQuote(request.method.clone()))
+            .await?;
+
+        #[cfg(not(feature = "auth"))]
+        let auth_token = None;
+
+        self.transport.http_post(url, auth_token, &request).await
+    }
 }
 
 /// Http Client

+ 16 - 3
crates/cdk/src/wallet/mint_connector/mod.rs

@@ -10,9 +10,9 @@ use super::Error;
 pub use crate::lightning_address::{LnurlPayInvoiceResponse, LnurlPayResponse};
 use crate::nuts::{
     CheckStateRequest, CheckStateResponse, Id, KeySet, KeysetResponse, MeltQuoteBolt11Request,
-    MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
-    MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
-    SwapRequest, SwapResponse,
+    MeltQuoteBolt11Response, MeltQuoteCustomRequest, MeltRequest, MintInfo, MintQuoteBolt11Request,
+    MintQuoteBolt11Response, MintQuoteCustomRequest, MintQuoteCustomResponse, MintRequest,
+    MintResponse, RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
 };
 #[cfg(feature = "auth")]
 use crate::wallet::AuthWallet;
@@ -127,4 +127,17 @@ pub trait MintConnector: Debug {
         &self,
         request: MeltRequest<String>,
     ) -> Result<MeltQuoteBolt11Response<String>, Error>;
+
+    /// Mint Quote for Custom Payment Method
+    async fn post_mint_custom_quote(
+        &self,
+        method: &str,
+        request: MintQuoteCustomRequest,
+    ) -> Result<MintQuoteCustomResponse<String>, Error>;
+
+    /// Melt Quote for Custom Payment Method
+    async fn post_melt_custom_quote(
+        &self,
+        request: MeltQuoteCustomRequest,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error>;
 }

+ 1 - 1
crates/cdk/src/wallet/mint_connector/transport/tor_transport.rs

@@ -1,4 +1,4 @@
-///! Tor transport implementation (non-wasm32 only)
+//! Tor transport implementation (non-wasm32 only)
 use std::sync::Arc;
 
 use arti_client::{TorClient, TorClientConfig};

+ 0 - 1
crates/cdk/src/wallet/receive.rs

@@ -239,7 +239,6 @@ impl Wallet {
         opts: ReceiveOptions,
     ) -> Result<Amount, Error> {
         let token = Token::from_str(encoded_token)?;
-
         let unit = token.unit().unwrap_or_default();
 
         ensure_cdk!(unit == self.unit, Error::UnsupportedUnit);

+ 4 - 4
crates/cdk/src/wallet/streams/mod.rs

@@ -63,10 +63,10 @@ impl WaitableEvent {
                 let (bolt11, bolt12) = quotes.into_iter().fold(
                     (Vec::new(), Vec::new()),
                     |mut acc, (quote_id, payment_method)| {
-                        match payment_method {
-                            PaymentMethod::Bolt11 => acc.0.push(quote_id),
-                            PaymentMethod::Bolt12 => acc.1.push(quote_id),
-                            PaymentMethod::Custom(_) => acc.0.push(quote_id),
+                        match payment_method.as_str() {
+                            "bolt11" => acc.0.push(quote_id),
+                            "bolt12" => acc.1.push(quote_id),
+                            _ => acc.0.push(quote_id),
                         }
                         acc
                     },

+ 2 - 2
crates/cdk/src/wallet/streams/proof.rs

@@ -110,11 +110,11 @@ impl Stream for MultipleMintQuoteProofStream<'_> {
 
                     let mut minting_future = Box::pin(async move {
                         match mint_quote.payment_method {
-                            PaymentMethod::Bolt11 => wallet
+                            PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => wallet
                                 .mint(&mint_quote.id, amount_split_target, spending_conditions)
                                 .await
                                 .map(|proofs| (mint_quote, proofs)),
-                            PaymentMethod::Bolt12 => wallet
+                            PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt12) => wallet
                                 .mint_bolt12(
                                     &mint_quote.id,
                                     amount,

+ 23 - 0
misc/mintd_payment_processor.sh

@@ -123,6 +123,29 @@ export CDK_PAYMENT_PROCESSOR_LISTEN_PORT="8090";
 
 echo "$CDK_PAYMENT_PROCESSOR_CLN_RPC_PATH"
 
+# Wait for LND certificate and macaroon files to exist (only if using LND backend)
+if [ "$LN_BACKEND" = "LND" ]; then
+    echo "Waiting for LND certificate and macaroon files..."
+    CERT_TIMEOUT=60
+    CERT_START_TIME=$(date +%s)
+    
+    while [ ! -f "$CDK_PAYMENT_PROCESSOR_LND_CERT_FILE" ] || [ ! -f "$CDK_PAYMENT_PROCESSOR_LND_MACAROON_FILE" ]; do
+        CURRENT_TIME=$(date +%s)
+        ELAPSED_TIME=$((CURRENT_TIME - CERT_START_TIME))
+        
+        if [ $ELAPSED_TIME -ge $CERT_TIMEOUT ]; then
+            echo "Timeout waiting for LND files after $CERT_TIMEOUT seconds"
+            echo "Expected cert file: $CDK_PAYMENT_PROCESSOR_LND_CERT_FILE"
+            echo "Expected macaroon file: $CDK_PAYMENT_PROCESSOR_LND_MACAROON_FILE"
+            exit 1
+        fi
+        
+        sleep 0.5
+    done
+    
+    echo "LND certificate and macaroon files found"
+fi
+
 cargo b --bin cdk-payment-processor
 
 cargo run --bin cdk-payment-processor &