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
This commit is contained in:
2026-08-11 04:34:00 +00:00
parent dbaf5fa063
commit ea8a248cb8

214
server.ts
View File

@@ -16,6 +16,54 @@ import {
import { HardwareProfiles, DEFAULT_ANDROID_APPS } from './src/data/deviceProfiles.js'; import { HardwareProfiles, DEFAULT_ANDROID_APPS } from './src/data/deviceProfiles.js';
dotenv.config(); 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<string, { targetPlan: string; createdAt: string }> = {};
// BTCPay helper: create invoice
async function createBtcpayInvoice(amount: number, metadata: Record<string, any>): Promise<any> {
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 __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); 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) // 3. Upgrade or Switch Subscription Tier ($5/mo for 5 VMs)
app.post('/api/user/subscription/upgrade', (req, res) => { app.post('/api/user/subscription/upgrade', async (req, res) => {
const { targetPlan, paymentMethod } = req.body; const { targetPlan } = req.body;
if (targetPlan === 'pro') { if (targetPlan === 'pro') {
userSubscription = { // Create BTCPay invoice instead of mock upgrade
plan: 'pro', try {
maxVms: 5, // 5 VMs for $5/mo const metadata = {
pricePerMonth: 5, orderId: `droidfleet-${Date.now()}`,
activeVmCount: virtualMachines.length, plan: 'pro',
renewsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), itemDesc: 'DroidFleet Visor - PRO Tier Subscription',
paymentMethod: paymentMethod || 'Visa ending in 4242', 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 { } else {
// Downgrading to free is instant
userSubscription = { userSubscription = {
plan: 'free', plan: 'free',
maxVms: 1, // 1 Free VM maxVms: 1,
pricePerMonth: 0, pricePerMonth: 0,
activeVmCount: virtualMachines.length, activeVmCount: virtualMachines.length,
renewsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), renewsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
paymentMethod: 'Free Tier Active', paymentMethod: 'Free Tier Active',
}; };
}
res.json({ return res.json({
message: `Successfully updated subscription to ${userSubscription.plan.toUpperCase()} tier!`, message: `Successfully downgraded to FREE tier.`,
subscription: userSubscription, subscription: userSubscription,
}); });
}
}); });
// 4. List All Virtual Machines // 4. List All Virtual Machines
@@ -783,6 +855,116 @@ app.get('/api/openapi.json', (req, res) => {
// ========================================== // ==========================================
// MODEL CONTEXT PROTOCOL (MCP) SERVER ENGINE // 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<void>((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 = [ const MCP_TOOLS_DEFINITIONS = [
{ {
name: 'list_vms', name: 'list_vms',