Skip to main content

paygress/
pod_provisioning.rs

1// Unified Pod Provisioning Service
2use anyhow::Result;
3use serde::{Deserialize, Serialize};
4use tracing::{info, error};
5
6use crate::sidecar_service::{SidecarState, SidecarConfig, PodInfo, extract_token_value};
7use crate::nostr::{EncryptedSpawnPodRequest, EncryptedTopUpPodRequest, PodSpec};
8
9/// Request for spawning a new pod
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct SpawnPodTool {
12    pub cashu_token: String,
13    pub pod_spec_id: Option<String>,
14    pub pod_image: String,
15    pub ssh_username: String,
16    pub ssh_password: String,
17    pub user_pubkey: Option<String>,
18}
19
20/// Request for topping up an existing pod
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct TopUpPodTool {
23    pub pod_npub: String,
24    pub cashu_token: String,
25}
26
27
28/// Request for getting available pod specifications/offers
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct GetOffersTool {}
31
32/// Request for getting pod status/details
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct GetPodStatusTool {
35    pub pod_npub: String,
36}
37
38/// Response for pod spawning
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct SpawnPodResponse {
41    pub success: bool,
42    pub message: String,
43    pub pod_npub: Option<String>,
44    pub ssh_host: Option<String>,
45    pub ssh_port: Option<u16>,
46    pub ssh_username: Option<String>,
47    pub ssh_password: Option<String>,
48    pub expires_at: Option<String>,
49    pub pod_spec_name: Option<String>,
50    pub cpu_millicores: Option<u64>,
51    pub memory_mb: Option<u64>,
52    pub instructions: Vec<String>,
53}
54
55/// Response for pod top-up
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct TopUpPodResponse {
58    pub success: bool,
59    pub message: String,
60    pub pod_npub: String,
61    pub extended_duration_seconds: Option<u64>,
62    pub new_expires_at: Option<String>,
63}
64
65
66/// Response for getting offers
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct GetOffersResponse {
69    pub minimum_duration_seconds: u64,
70    pub whitelisted_mints: Vec<String>,
71    pub pod_specs: Vec<PodSpec>,
72}
73
74/// Response for pod status
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct GetPodStatusResponse {
77    pub success: bool,
78    pub message: String,
79    pub pod_npub: String,
80    pub found: bool,
81    pub created_at: Option<String>,
82    pub expires_at: Option<String>,
83    pub time_remaining_seconds: Option<u64>,
84    pub pod_spec_name: Option<String>,
85    pub cpu_millicores: Option<u64>,
86    pub memory_mb: Option<u64>,
87    pub status: Option<String>,
88}
89
90/// Unified service handler for pod provisioning
91pub struct PodProvisioningService {
92    state: SidecarState,
93}
94
95impl PodProvisioningService {
96    pub async fn new(config: SidecarConfig) -> Result<Self> {
97        let state = SidecarState::new(config).await
98            .map_err(|e| anyhow::anyhow!("Failed to initialize sidecar state: {}", e))?;
99        
100        Ok(Self { state })
101    }
102
103    /// Get the service configuration
104    pub fn get_config(&self) -> &SidecarConfig {
105        &self.state.config
106    }
107
108    /// Handle spawn pod request
109    pub async fn spawn_pod(&self, request: SpawnPodTool) -> Result<SpawnPodResponse> {
110        info!("Pod spawn request received for image: {}", request.pod_image);
111
112        // Convert request to internal format
113        let spawn_request = EncryptedSpawnPodRequest {
114            cashu_token: request.cashu_token,
115            pod_spec_id: request.pod_spec_id,
116            pod_image: request.pod_image,
117            ssh_username: request.ssh_username,
118            ssh_password: request.ssh_password,
119        };
120
121        // Use the existing logic from main.rs handle_spawn_pod_request
122        match self.handle_spawn_pod_internal(spawn_request, &request.user_pubkey.unwrap_or_else(|| "mcp-client".to_string())).await {
123            Ok(response) => Ok(response),
124            Err(e) => {
125                error!("Failed to spawn pod: {}", e);
126                Ok(SpawnPodResponse {
127                    success: false,
128                    message: format!("Failed to spawn pod: {}", e),
129                    pod_npub: None,
130                    ssh_host: None,
131                    ssh_port: None,
132                    ssh_username: None,
133                    ssh_password: None,
134                    expires_at: None,
135                    pod_spec_name: None,
136                    cpu_millicores: None,
137                    memory_mb: None,
138                    instructions: vec![],
139                })
140            }
141        }
142    }
143
144    /// Handle top-up pod request
145    pub async fn topup_pod(&self, request: TopUpPodTool) -> Result<TopUpPodResponse> {
146        info!("Pod top-up request received for NPUB: {}", request.pod_npub);
147
148        // Convert request to internal format
149        let topup_request = EncryptedTopUpPodRequest {
150            pod_npub: request.pod_npub.clone(),
151            cashu_token: request.cashu_token,
152        };
153
154        // Use the existing logic from main.rs handle_top_up_request
155        match self.handle_topup_pod_internal(topup_request).await {
156            Ok(response) => Ok(response),
157            Err(e) => {
158                error!("Failed to top-up pod: {}", e);
159                Ok(TopUpPodResponse {
160                    success: false,
161                    message: format!("Failed to top-up pod: {}", e),
162                    pod_npub: request.pod_npub,
163                    extended_duration_seconds: None,
164                    new_expires_at: None,
165                })
166            }
167        }
168    }
169
170
171    /// Handle get offers request
172    pub async fn get_offers(&self, _request: GetOffersTool) -> Result<GetOffersResponse> {
173        info!("Get offers request received");
174
175        Ok(GetOffersResponse {
176            minimum_duration_seconds: self.state.config.minimum_pod_duration_seconds,
177            whitelisted_mints: self.state.config.whitelisted_mints.clone(),
178            pod_specs: self.state.config.pod_specs.clone(),
179        })
180    }
181
182    /// Handle get pod status request
183    pub async fn get_pod_status(&self, request: GetPodStatusTool) -> Result<GetPodStatusResponse> {
184        info!("Get pod status request received for NPUB: {}", request.pod_npub);
185
186        use kube::{Api, api::ListParams};
187        use k8s_openapi::api::core::v1::Pod;
188        use chrono::Utc;
189
190        // First check our internal tracking
191        let active_pods = self.state.active_pods.read().await;
192        let pod_info = active_pods.get(&request.pod_npub);
193
194        if let Some(info) = pod_info {
195            // Calculate time remaining
196            let now = Utc::now();
197            let time_remaining = if info.expires_at > now {
198                Some((info.expires_at - now).num_seconds() as u64)
199            } else {
200                Some(0) // Expired
201            };
202
203            // Get pod spec details
204            let pod_spec = self.state.config.pod_specs.first(); // Default spec
205
206            Ok(GetPodStatusResponse {
207                success: true,
208                message: "Pod found and active".to_string(),
209                pod_npub: request.pod_npub.clone(),
210                found: true,
211                created_at: Some(info.created_at.to_rfc3339()),
212                expires_at: Some(info.expires_at.to_rfc3339()),
213                time_remaining_seconds: time_remaining,
214                pod_spec_name: pod_spec.map(|s| s.name.clone()),
215                cpu_millicores: pod_spec.map(|s| s.cpu_millicores),
216                memory_mb: pod_spec.map(|s| s.memory_mb),
217                status: Some(if time_remaining.unwrap_or(0) > 0 { "running".to_string() } else { "expired".to_string() }),
218            })
219        } else {
220            // Pod not found in our tracking, check if it exists in Kubernetes but expired
221            let pods_api: Api<Pod> = Api::namespaced(self.state.k8s_client.client.clone(), &self.state.config.pod_namespace);
222            let pods = match pods_api.list(&ListParams::default()).await {
223                Ok(pods) => pods,
224                Err(_) => {
225                    // If we can't list pods, assume pod doesn't exist
226                    return Ok(GetPodStatusResponse {
227                        success: true,
228                        message: "Pod not found".to_string(),
229                        pod_npub: request.pod_npub.clone(),
230                        found: false,
231                        created_at: None,
232                        expires_at: None,
233                        time_remaining_seconds: None,
234                        pod_spec_name: None,
235                        cpu_millicores: None,
236                        memory_mb: None,
237                        status: None,
238                    });
239                }
240            };
241
242            // Check if pod exists in Kubernetes
243            let target_pod = pods.items.iter().find(|pod| {
244                pod.metadata.labels.as_ref()
245                    .and_then(|labels| labels.get("pod-npub"))
246                    .map(|stored_hex| {
247                        let requested_hex = if request.pod_npub.starts_with("npub1") {
248                            &request.pod_npub[5..]
249                        } else {
250                            &request.pod_npub
251                        };
252                        let stored_truncated = if stored_hex.len() > 63 {
253                            &stored_hex[..63]
254                        } else {
255                            stored_hex
256                        };
257                        let requested_truncated = if requested_hex.len() > 63 {
258                            &requested_hex[..63]
259                        } else {
260                            requested_hex
261                        };
262                        stored_truncated == requested_truncated
263                    })
264                    .unwrap_or(false)
265            });
266
267            if let Some(pod) = target_pod {
268                // Pod exists in Kubernetes but not in our tracking (likely expired)
269                let status = pod.status.as_ref()
270                    .and_then(|status| status.phase.as_ref())
271                    .cloned()
272                    .unwrap_or_else(|| "unknown".to_string());
273
274                Ok(GetPodStatusResponse {
275                    success: true,
276                    message: "Pod found but not tracked (likely expired)".to_string(),
277                    pod_npub: request.pod_npub.clone(),
278                    found: true,
279                    created_at: pod.metadata.creation_timestamp.as_ref()
280                        .map(|ts| ts.0.to_rfc3339()),
281                    expires_at: None,
282                    time_remaining_seconds: Some(0), // Assume expired
283                    pod_spec_name: None,
284                    cpu_millicores: None,
285                    memory_mb: None,
286                    status: Some(status),
287                })
288            } else {
289                // Pod not found anywhere
290                Ok(GetPodStatusResponse {
291                    success: true,
292                    message: "Pod not found".to_string(),
293                    pod_npub: request.pod_npub.clone(),
294                    found: false,
295                    created_at: None,
296                    expires_at: None,
297                    time_remaining_seconds: None,
298                    pod_spec_name: None,
299                    cpu_millicores: None,
300                    memory_mb: None,
301                    status: None,
302                })
303            }
304        }
305    }
306
307    /// Internal handler for spawning pods (adapted from main.rs)
308    async fn handle_spawn_pod_internal(&self, request: EncryptedSpawnPodRequest, user_pubkey: &str) -> Result<SpawnPodResponse> {
309        use chrono::Utc;
310        use nostr_sdk::{Keys, ToBech32};
311
312        // Select pod specification
313        let pod_spec = if let Some(spec_id) = &request.pod_spec_id {
314            self.state.config.pod_specs.iter().find(|s| s.id == *spec_id)
315        } else {
316            self.state.config.pod_specs.first()
317        };
318        
319        let pod_spec = match pod_spec {
320            Some(spec) => spec,
321            None => {
322                return Ok(SpawnPodResponse {
323                    success: false,
324                    message: format!("Pod specification '{}' not found", request.pod_spec_id.as_deref().unwrap_or("default")),
325                    pod_npub: None,
326                    ssh_host: None,
327                    ssh_port: None,
328                    ssh_username: None,
329                    ssh_password: None,
330                    expires_at: None,
331                    pod_spec_name: None,
332                    cpu_millicores: None,
333                    memory_mb: None,
334                    instructions: vec!["Please check available specifications in the offer".to_string()],
335                });
336            }
337        };
338
339        // Decode token to get amount and duration
340        let payment_amount_msats = match extract_token_value(&request.cashu_token).await {
341            Ok(msats) => msats,
342            Err(e) => {
343                return Ok(SpawnPodResponse {
344                    success: false,
345                    message: "Failed to decode Cashu token".to_string(),
346                    pod_npub: None,
347                    ssh_host: None,
348                    ssh_port: None,
349                    ssh_username: None,
350                    ssh_password: None,
351                    expires_at: None,
352                    pod_spec_name: None,
353                    cpu_millicores: None,
354                    memory_mb: None,
355                    instructions: vec![format!("Token decode error: {}", e)],
356                });
357            }
358        };
359        
360        // Check if payment is sufficient for minimum duration with selected spec
361        let minimum_payment = self.state.config.minimum_pod_duration_seconds * pod_spec.rate_msats_per_sec;
362        if payment_amount_msats < minimum_payment {
363            return Ok(SpawnPodResponse {
364                success: false,
365                message: format!("Insufficient payment: {} msats", payment_amount_msats),
366                pod_npub: None,
367                ssh_host: None,
368                ssh_port: None,
369                ssh_username: None,
370                ssh_password: None,
371                expires_at: None,
372                pod_spec_name: Some(pod_spec.name.clone()),
373                cpu_millicores: Some(pod_spec.cpu_millicores),
374                memory_mb: Some(pod_spec.memory_mb),
375                instructions: vec![
376                    format!("Minimum required: {} msats for {} seconds with {} spec (rate: {} msats/sec)", 
377                        minimum_payment,
378                        self.state.config.minimum_pod_duration_seconds,
379                        pod_spec.name,
380                        pod_spec.rate_msats_per_sec)
381                ],
382            });
383        }
384
385        // Calculate duration based on payment and selected spec rate
386        let duration_seconds = payment_amount_msats / pod_spec.rate_msats_per_sec;
387
388        // Token verification handled by ngx_l402 at nginx layer
389        info!("✅ Using payment: {} msats for {} seconds (verified by ngx_l402)", payment_amount_msats, duration_seconds);
390
391        // Generate NPUB first and use it as pod name
392        let pod_keys = Keys::generate();
393        let pod_npub = pod_keys.public_key().to_bech32().unwrap();
394        let pod_nsec = pod_keys.secret_key().unwrap().to_secret_hex();
395        
396        // Create Kubernetes-safe pod name from NPUB (take first 8 chars after npub1 prefix)
397        let pod_name = format!("pod-{}", pod_npub.replace("npub1", "").chars().take(8).collect::<String>());
398        let username = request.ssh_username;
399        let password = request.ssh_password;
400        let image = request.pod_image;
401        let ssh_port = match self.state.generate_ssh_port().await {
402            Ok(port) => port,
403            Err(e) => {
404                return Ok(SpawnPodResponse {
405                    success: false,
406                    message: "Failed to allocate SSH port".to_string(),
407                    pod_npub: None,
408                    ssh_host: None,
409                    ssh_port: None,
410                    ssh_username: None,
411                    ssh_password: None,
412                    expires_at: None,
413                    pod_spec_name: Some(pod_spec.name.clone()),
414                    cpu_millicores: Some(pod_spec.cpu_millicores),
415                    memory_mb: Some(pod_spec.memory_mb),
416                    instructions: vec![format!("Port allocation error: {}", e)],
417                });
418            }
419        };
420
421        let now = Utc::now();
422        let expires_at = now + chrono::Duration::seconds(duration_seconds as i64);
423
424        match self.state.k8s_client.create_ssh_pod(
425            &self.state.config,
426            &self.state.config.pod_namespace,
427            &pod_name,
428            &pod_npub,
429            &pod_nsec,
430            &image,
431            ssh_port,
432            &username,
433            &password,
434            duration_seconds,
435            pod_spec.memory_mb,
436            pod_spec.cpu_millicores,
437            user_pubkey,
438        ).await {
439            Ok(node_port) => {
440                let pod_info = PodInfo {
441                    pod_npub: pod_npub.clone(),
442                    namespace: self.state.config.pod_namespace.clone(),
443                    created_at: now,
444                    expires_at,
445                    allocated_port: ssh_port,
446                    ssh_username: username.clone(),
447                    ssh_password: password.clone(),
448                    payment_amount_msats,
449                    duration_seconds,
450                    node_port: Some(node_port),
451                    nostr_public_key: pod_npub.clone(),
452                    nostr_private_key: pod_nsec,
453                };
454                self.state.active_pods.write().await.insert(pod_npub.clone(), pod_info.clone());
455
456                let instructions = vec![
457                    "🚀 SSH access available:".to_string(),
458                    "".to_string(),
459                    "Direct access (no kubectl needed):".to_string(),
460                    format!("   ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no {}@{} -p {}", username, self.state.config.ssh_host, node_port),
461                    "".to_string(),
462                    "⚠️  Pod expires at:".to_string(),
463                    format!("   {}", expires_at.format("%Y-%m-%d %H:%M:%S UTC")),
464                    "".to_string(),
465                    "📋 Pod Details:".to_string(),
466                    format!("   Pod NPUB: {}", pod_npub),
467                    format!("   Specification: {} ({})", pod_spec.name, pod_spec.description),
468                    format!("   CPU: {} millicores", pod_spec.cpu_millicores),
469                    format!("   Memory: {} MB", pod_spec.memory_mb),
470                    format!("   Duration: {} seconds", duration_seconds),
471                ];
472
473                info!("Pod with NPUB {} created successfully", pod_npub);
474
475                Ok(SpawnPodResponse {
476                    success: true,
477                    message: format!("Pod created successfully. SSH access available for {} seconds", duration_seconds),
478                    pod_npub: Some(pod_npub),
479                    ssh_host: Some(self.state.config.ssh_host.clone()),
480                    ssh_port: Some(node_port),
481                    ssh_username: Some(username),
482                    ssh_password: Some(password),
483                    expires_at: Some(expires_at.to_rfc3339()),
484                    pod_spec_name: Some(pod_spec.name.clone()),
485                    cpu_millicores: Some(pod_spec.cpu_millicores),
486                    memory_mb: Some(pod_spec.memory_mb),
487                    instructions,
488                })
489            }
490            Err(e) => {
491                Ok(SpawnPodResponse {
492                    success: false,
493                    message: "Failed to create pod".to_string(),
494                    pod_npub: None,
495                    ssh_host: None,
496                    ssh_port: None,
497                    ssh_username: None,
498                    ssh_password: None,
499                    expires_at: None,
500                    pod_spec_name: Some(pod_spec.name.clone()),
501                    cpu_millicores: Some(pod_spec.cpu_millicores),
502                    memory_mb: Some(pod_spec.memory_mb),
503                    instructions: vec![format!("Pod creation error: {}", e)],
504                })
505            }
506        }
507    }
508
509    /// Internal handler for topping up pods (adapted from main.rs)
510    async fn handle_topup_pod_internal(&self, request: EncryptedTopUpPodRequest) -> Result<TopUpPodResponse> {
511        use kube::{Api, api::ListParams};
512        use k8s_openapi::api::core::v1::Pod;
513        use chrono::Utc;
514
515        info!("Pod top-up request received for NPUB: {}", request.pod_npub);
516
517        // Find pod by NPUB label in Kubernetes
518        let pods_api: Api<Pod> = Api::namespaced(self.state.k8s_client.client.clone(), &self.state.config.pod_namespace);
519        let pods = match pods_api.list(&ListParams::default()).await {
520            Ok(pods) => pods,
521            Err(e) => {
522                error!("Failed to list pods: {}", e);
523                return Ok(TopUpPodResponse {
524                    success: false,
525                    message: format!("Failed to list pods: {}", e),
526                    pod_npub: request.pod_npub,
527                    extended_duration_seconds: None,
528                    new_expires_at: None,
529                });
530            }
531        };
532
533        // Find pod by NPUB label (compare truncated hex parts)
534        let target_pod = match pods.items.iter().find(|pod| {
535            pod.metadata.labels.as_ref()
536                .and_then(|labels| labels.get("pod-npub"))
537                .map(|stored_hex| {
538                    // Extract hex from the requested NPUB
539                    let requested_hex = if request.pod_npub.starts_with("npub1") {
540                        &request.pod_npub[5..] // Remove "npub1" prefix
541                    } else {
542                        &request.pod_npub // Already hex or different format
543                    };
544                    // Truncate both to 63 chars for comparison
545                    let stored_truncated = if stored_hex.len() > 63 {
546                        &stored_hex[..63]
547                    } else {
548                        stored_hex
549                    };
550                    let requested_truncated = if requested_hex.len() > 63 {
551                        &requested_hex[..63]
552                    } else {
553                        requested_hex
554                    };
555                    stored_truncated == requested_truncated
556                })
557                .unwrap_or(false)
558        }) {
559            Some(pod) => pod,
560            None => {
561                return Ok(TopUpPodResponse {
562                    success: false,
563                    message: format!("Pod with NPUB '{}' not found or already expired", request.pod_npub),
564                    pod_npub: request.pod_npub,
565                    extended_duration_seconds: None,
566                    new_expires_at: None,
567                });
568            }
569        };
570
571        // Get pod name from metadata
572        let pod_name = match &target_pod.metadata.name {
573            Some(name) => name.clone(),
574            None => {
575                return Ok(TopUpPodResponse {
576                    success: false,
577                    message: "Pod has no name in metadata".to_string(),
578                    pod_npub: request.pod_npub,
579                    extended_duration_seconds: None,
580                    new_expires_at: None,
581                });
582            }
583        };
584
585        // Extract payment amount from token
586        let payment_amount_msats = match extract_token_value(&request.cashu_token).await {
587            Ok(msats) => msats,
588            Err(e) => {
589                return Ok(TopUpPodResponse {
590                    success: false,
591                    message: format!("Failed to decode Cashu token: {}", e),
592                    pod_npub: request.pod_npub,
593                    extended_duration_seconds: None,
594                    new_expires_at: None,
595                });
596            }
597        };
598
599        // Calculate additional duration from payment
600        let additional_duration_seconds = self.state.calculate_duration_from_payment(payment_amount_msats);
601        
602        if additional_duration_seconds == 0 {
603            return Ok(TopUpPodResponse {
604                success: false,
605                message: format!("Insufficient payment: {} msats. Minimum required: {} msats for 1 second extension", 
606                    payment_amount_msats, self.state.config.pod_specs.first().map(|s| s.rate_msats_per_sec).unwrap_or(100)),
607                pod_npub: request.pod_npub,
608                extended_duration_seconds: None,
609                new_expires_at: None,
610            });
611        }
612
613        // Token verification handled by ngx_l402 at nginx layer
614        info!("✅ Top-up payment: {} msats for {} additional seconds (verified by ngx_l402)", payment_amount_msats, additional_duration_seconds);
615
616        // Get current pod configuration before restarting
617        
618        let pods_api: Api<Pod> = Api::namespaced(self.state.k8s_client.client.clone(), &self.state.config.pod_namespace);
619        let current_pod = match pods_api.get(&pod_name).await {
620            Ok(pod) => pod,
621            Err(e) => {
622                error!("Failed to get current pod configuration: {}", e);
623                return Ok(TopUpPodResponse {
624                    success: false,
625                    message: format!("Failed to get pod configuration: {}", e),
626                    pod_npub: request.pod_npub,
627                    extended_duration_seconds: None,
628                    new_expires_at: None,
629                });
630            }
631        };
632
633        // Use the proper deadline extension method instead of recreating the pod
634        if let Err(e) = self.state.k8s_client.extend_pod_deadline(&self.state.config.pod_namespace, &pod_name, additional_duration_seconds).await {
635            error!("Failed to extend pod deadline: {}", e);
636            return Ok(TopUpPodResponse {
637                success: false,
638                message: format!("Failed to extend pod deadline: {}", e),
639                pod_npub: request.pod_npub,
640                extended_duration_seconds: None,
641                new_expires_at: None,
642            });
643        }
644
645        // Get current pod expiration time to calculate new expiration
646        let current_deadline_seconds = current_pod
647            .spec
648            .as_ref()
649            .and_then(|spec| spec.active_deadline_seconds)
650            .unwrap_or(0);
651        
652        // Calculate new expiration time from pod creation time + new deadline
653        let new_expires_at = match &current_pod.metadata.creation_timestamp {
654            Some(creation_time) => {
655                let creation_utc = creation_time.0;
656                let new_deadline_seconds = current_deadline_seconds + additional_duration_seconds as i64;
657                creation_utc + chrono::Duration::seconds(new_deadline_seconds)
658            }
659            None => Utc::now() + chrono::Duration::seconds(additional_duration_seconds as i64), // Fallback
660        };
661        
662        // Update the pod info in our tracking with the new deadline
663        let mut active_pods = self.state.active_pods.write().await;
664        if let Some(pod_info) = active_pods.get_mut(&request.pod_npub) {
665            pod_info.expires_at = new_expires_at;
666            pod_info.duration_seconds = current_deadline_seconds as u64 + additional_duration_seconds;
667        }
668        drop(active_pods);
669
670        info!(
671            "🔄 Pod '{}' (NPUB: {}) extended by {} seconds (new deadline: {} seconds)",
672            pod_name,
673            request.pod_npub,
674            additional_duration_seconds,
675            current_deadline_seconds + additional_duration_seconds as i64
676        );
677
678        Ok(TopUpPodResponse {
679            success: true,
680            message: format!(
681                "Pod '{}' (NPUB: {}) successfully extended by {} seconds. New expiration: {}",
682                pod_name,
683                request.pod_npub,
684                additional_duration_seconds,
685                new_expires_at.format("%Y-%m-%d %H:%M:%S UTC")
686            ),
687            pod_npub: request.pod_npub,
688            extended_duration_seconds: Some(additional_duration_seconds),
689            new_expires_at: Some(new_expires_at.to_rfc3339()),
690        })
691    }
692}