From ea8a248cb8a829b9c96e7ed16653214df2383f52 Mon Sep 17 00:00:00 2001 From: drjones Date: Tue, 11 Aug 2026 04:34:00 +0000 Subject: [PATCH] Add BTCPay Bitcoin payment integration - Store ID: D98SMWWKGxgF5QxvQ8dTHc1YrG8Po4Jho5omDvbLd7rT - API endpoints: /api/btcpay/create-invoice, /api/webhook/btcpay, /api/btcpay/invoice/:id - Upgrade flow now creates real BTCPay invoices instead of mock payments - Webhook auto-activates PRO subscription on payment settlement - /month pricing via Bitcoin on-chain --- server.ts | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 198 insertions(+), 16 deletions(-) diff --git a/server.ts b/server.ts index 28f9e2f..7b676cf 100644 --- a/server.ts +++ b/server.ts @@ -16,6 +16,54 @@ import { import { HardwareProfiles, DEFAULT_ANDROID_APPS } from './src/data/deviceProfiles.js'; dotenv.config(); +// ========================================== +// BTCPAY SERVER INTEGRATION +// ========================================== +const BTCPAY_URL = 'https://btcpay.thetempleofdoom.com'; +const BTCPAY_STORE_ID = 'D98SMWWKGxgF5QxvQ8dTHc1YrG8Po4Jho5omDvbLd7rT'; +const BTCPAY_API_KEY = '9cd2f26f2d73f6f670df3a80138e889e08c89b33'; +const SUBSCRIPTION_PRICE_USD = 5.00; // $5/month for Pro tier + +// Track pending BTCPay invoices: invoiceId -> { userId, targetPlan } +const pendingInvoices: Record = {}; + +// BTCPay helper: create invoice +async function createBtcpayInvoice(amount: number, metadata: Record): Promise { + const https = await import('https'); + return new Promise((resolve, reject) => { + const agent = new https.Agent({ rejectUnauthorized: false }); + const payload = JSON.stringify({ + amount: amount.toString(), + currency: 'USD', + metadata, + paymentMethods: ['BTC-CHAIN'], + checkout: { redirectAutomatically: true }, + }); + const url = new URL(`${BTCPAY_URL}/api/v1/stores/${BTCPAY_STORE_ID}/invoices`); + const req = https.request({ + hostname: url.hostname, + port: url.port || 443, + path: url.pathname, + method: 'POST', + agent, + headers: { + 'Authorization': `token ${BTCPAY_API_KEY}`, + 'Content-Type': 'application/json', + }, + }, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + try { resolve(JSON.parse(data)); } + catch (e) { reject(new Error(`BTCPay parse error: ${data}`)); } + }); + }); + req.on('error', reject); + req.write(payload); + req.end(); + }); +} + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -147,33 +195,57 @@ app.get('/api/user/subscription', (req, res) => { }); // 3. Upgrade or Switch Subscription Tier ($5/mo for 5 VMs) -app.post('/api/user/subscription/upgrade', (req, res) => { - const { targetPlan, paymentMethod } = req.body; +app.post('/api/user/subscription/upgrade', async (req, res) => { + const { targetPlan } = req.body; if (targetPlan === 'pro') { - userSubscription = { - plan: 'pro', - maxVms: 5, // 5 VMs for $5/mo - pricePerMonth: 5, - activeVmCount: virtualMachines.length, - renewsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), - paymentMethod: paymentMethod || 'Visa ending in 4242', - }; + // Create BTCPay invoice instead of mock upgrade + try { + const metadata = { + orderId: `droidfleet-${Date.now()}`, + plan: 'pro', + itemDesc: 'DroidFleet Visor - PRO Tier Subscription', + buyerEmail: 'droidfleet-user', + }; + + const invoice = await createBtcpayInvoice(SUBSCRIPTION_PRICE_USD, metadata); + + pendingInvoices[invoice.id] = { + targetPlan: 'pro', + createdAt: new Date().toISOString(), + }; + + return res.json({ + message: 'Pay with Bitcoin to activate PRO tier.', + invoiceId: invoice.id, + checkoutUrl: invoice.checkoutLink, + amount: `$${SUBSCRIPTION_PRICE_USD}`, + plan: 'pro', + subscription: { + ...userSubscription, + status: 'awaiting_payment', + }, + }); + } catch (err: any) { + console.error('BTCPay error:', err.message); + return res.status(500).json({ error: 'Bitcoin payment unavailable', detail: err.message }); + } } else { + // Downgrading to free is instant userSubscription = { plan: 'free', - maxVms: 1, // 1 Free VM + maxVms: 1, pricePerMonth: 0, activeVmCount: virtualMachines.length, renewsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), paymentMethod: 'Free Tier Active', }; - } - res.json({ - message: `Successfully updated subscription to ${userSubscription.plan.toUpperCase()} tier!`, - subscription: userSubscription, - }); + return res.json({ + message: `Successfully downgraded to FREE tier.`, + subscription: userSubscription, + }); + } }); // 4. List All Virtual Machines @@ -783,6 +855,116 @@ app.get('/api/openapi.json', (req, res) => { // ========================================== // MODEL CONTEXT PROTOCOL (MCP) SERVER ENGINE // ========================================== + +// BTCPay Invoice Creation Endpoint +app.post('/api/btcpay/create-invoice', async (req, res) => { + try { + const { targetPlan } = req.body; + if (targetPlan !== 'pro') { + return res.status(400).json({ error: 'Invalid plan. Only "pro" is available.' }); + } + + const metadata = { + orderId: `droidfleet-${Date.now()}`, + plan: targetPlan, + itemDesc: `DroidFleet Visor - ${targetPlan.toUpperCase()} Tier Subscription`, + buyerEmail: 'droidfleet-user', + }; + + const invoice = await createBtcpayInvoice(SUBSCRIPTION_PRICE_USD, metadata); + + // Track this invoice + pendingInvoices[invoice.id] = { + targetPlan, + createdAt: new Date().toISOString(), + }; + + res.json({ + invoiceId: invoice.id, + checkoutUrl: invoice.checkoutLink, + amount: `$${SUBSCRIPTION_PRICE_USD}`, + plan: targetPlan, + message: 'Pay with Bitcoin. Your subscription activates on payment confirmation.', + }); + } catch (err: any) { + console.error('BTCPay invoice error:', err.message); + res.status(500).json({ error: 'Failed to create Bitcoin invoice', detail: err.message }); + } +}); + +// BTCPay Webhook Endpoint (payment confirmation) +app.post('/api/webhook/btcpay', (req, res) => { + try { + const body = req.body; + const eventType = body?.type || ''; + const invoiceId = body?.invoiceId || ''; + const metadata = body?.metadata || {}; + + console.log(`BTCPay webhook: ${eventType} for invoice ${invoiceId}`); + + if (eventType === 'InvoiceSettled' || eventType === 'InvoiceProcessing') { + const pending = pendingInvoices[invoiceId]; + if (pending) { + // Activate the Pro subscription + userSubscription = { + plan: pending.targetPlan, + maxVms: 5, + pricePerMonth: SUBSCRIPTION_PRICE_USD, + activeVmCount: virtualMachines.length, + renewsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + paymentMethod: `Bitcoin (BTCPay invoice ${invoiceId.slice(0, 12)}...)`, + }; + console.log(`Subscription activated: ${pending.targetPlan} via BTCPay invoice ${invoiceId}`); + delete pendingInvoices[invoiceId]; + } + } + + // Always return 200 to prevent BTCPay retry spam + res.json({ status: 'ok' }); + } catch (err: any) { + console.error('Webhook error:', err.message); + res.status(200).json({ status: 'error_received_but_acked' }); + } +}); + +// Check invoice status +app.get('/api/btcpay/invoice/:id', async (req, res) => { + try { + const https = await import('https'); + const agent = new https.Agent({ rejectUnauthorized: false }); + const url = new URL(`${BTCPAY_URL}/api/v1/stores/${BTCPAY_STORE_ID}/invoices/${req.params.id}`); + + await new Promise((resolve, reject) => { + https.get({ + hostname: url.hostname, + port: url.port || 443, + path: url.pathname, + agent, + headers: { 'Authorization': `token ${BTCPAY_API_KEY}` }, + }, (resp) => { + let data = ''; + resp.on('data', (chunk) => data += chunk); + resp.on('end', () => { + try { + const inv = JSON.parse(data); + res.json({ + invoiceId: inv.id, + status: inv.status, + amount: `${inv.amount} ${inv.currency}`, + }); + } catch (e) { + res.status(500).json({ error: 'Parse error' }); + } + resolve(); + }); + }).on('error', (e) => { reject(e); }); + }); + } catch (err: any) { + res.status(500).json({ error: err.message }); + } +}); + + const MCP_TOOLS_DEFINITIONS = [ { name: 'list_vms',