// RoadPay — BlackRoad's Own Payment & Billing System // pay.blackroad.io // v1.0.0 // // Own your customers, subscriptions, invoices, and payments. // Stripe is just a dumb card charger underneath. const VERSION = '2.0.0'; // ─── Schema ────────────────────────────────────────────────────────────── const SCHEMA = [ `CREATE TABLE IF NOT EXISTS customers ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, name TEXT, stripe_customer_id TEXT, metadata TEXT DEFAULT '{}', status TEXT DEFAULT 'active', created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) )`, `CREATE TABLE IF NOT EXISTS plans ( id TEXT PRIMARY KEY, name TEXT NOT NULL, slug TEXT UNIQUE NOT NULL, description TEXT, amount INTEGER NOT NULL, currency TEXT DEFAULT 'usd', interval TEXT DEFAULT 'month', interval_count INTEGER DEFAULT 1, features TEXT DEFAULT '[]', stripe_price_id TEXT, tier INTEGER DEFAULT 0, active INTEGER DEFAULT 1, metadata TEXT DEFAULT '{}', created_at TEXT DEFAULT (datetime('now')) )`, `CREATE TABLE IF NOT EXISTS addons ( id TEXT PRIMARY KEY, name TEXT NOT NULL, slug TEXT UNIQUE NOT NULL, description TEXT, amount INTEGER NOT NULL, currency TEXT DEFAULT 'usd', interval TEXT DEFAULT 'month', stripe_price_id TEXT, active INTEGER DEFAULT 1, metadata TEXT DEFAULT '{}', created_at TEXT DEFAULT (datetime('now')) )`, `CREATE TABLE IF NOT EXISTS subscriptions ( id TEXT PRIMARY KEY, customer_id TEXT NOT NULL REFERENCES customers(id), plan_id TEXT NOT NULL REFERENCES plans(id), stripe_subscription_id TEXT, status TEXT DEFAULT 'active', current_period_start TEXT, current_period_end TEXT, cancel_at TEXT, canceled_at TEXT, metadata TEXT DEFAULT '{}', created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) )`, `CREATE TABLE IF NOT EXISTS subscription_addons ( id TEXT PRIMARY KEY, subscription_id TEXT NOT NULL REFERENCES subscriptions(id), addon_id TEXT NOT NULL REFERENCES addons(id), stripe_subscription_item_id TEXT, status TEXT DEFAULT 'active', created_at TEXT DEFAULT (datetime('now')) )`, `CREATE TABLE IF NOT EXISTS invoices ( id TEXT PRIMARY KEY, customer_id TEXT NOT NULL REFERENCES customers(id), subscription_id TEXT REFERENCES subscriptions(id), stripe_invoice_id TEXT, amount INTEGER NOT NULL, currency TEXT DEFAULT 'usd', status TEXT DEFAULT 'draft', description TEXT, line_items TEXT DEFAULT '[]', due_date TEXT, paid_at TEXT, metadata TEXT DEFAULT '{}', created_at TEXT DEFAULT (datetime('now')) )`, `CREATE TABLE IF NOT EXISTS payments ( id TEXT PRIMARY KEY, customer_id TEXT NOT NULL REFERENCES customers(id), invoice_id TEXT REFERENCES invoices(id), stripe_payment_id TEXT, amount INTEGER NOT NULL, currency TEXT DEFAULT 'usd', method TEXT DEFAULT 'card', status TEXT DEFAULT 'pending', metadata TEXT DEFAULT '{}', created_at TEXT DEFAULT (datetime('now')) )`, `CREATE TABLE IF NOT EXISTS events ( id TEXT PRIMARY KEY, type TEXT NOT NULL, customer_id TEXT, entity_type TEXT, entity_id TEXT, data TEXT DEFAULT '{}', created_at TEXT DEFAULT (datetime('now')) )`, `CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email)`, `CREATE INDEX IF NOT EXISTS idx_customers_stripe ON customers(stripe_customer_id)`, `CREATE INDEX IF NOT EXISTS idx_subscriptions_customer ON subscriptions(customer_id)`, `CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status)`, `CREATE INDEX IF NOT EXISTS idx_invoices_customer ON invoices(customer_id)`, `CREATE INDEX IF NOT EXISTS idx_payments_customer ON payments(customer_id)`, `CREATE INDEX IF NOT EXISTS idx_events_type ON events(type)`, `CREATE INDEX IF NOT EXISTS idx_events_customer ON events(customer_id)`, // v2: API keys for customers `CREATE TABLE IF NOT EXISTS api_keys ( id TEXT PRIMARY KEY, customer_id TEXT NOT NULL REFERENCES customers(id), key_hash TEXT NOT NULL, key_prefix TEXT NOT NULL, name TEXT DEFAULT 'default', scopes TEXT DEFAULT '["api:read"]', rate_limit INTEGER DEFAULT 1000, last_used TEXT, usage_count INTEGER DEFAULT 0, status TEXT DEFAULT 'active', expires_at TEXT, created_at TEXT DEFAULT (datetime('now')) )`, // v2: Usage metering `CREATE TABLE IF NOT EXISTS usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id TEXT NOT NULL REFERENCES customers(id), api_key_id TEXT REFERENCES api_keys(id), endpoint TEXT NOT NULL, method TEXT DEFAULT 'GET', status_code INTEGER, latency_ms INTEGER, tokens_used INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')) )`, `CREATE INDEX IF NOT EXISTS idx_api_keys_customer ON api_keys(customer_id)`, `CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix)`, `CREATE INDEX IF NOT EXISTS idx_usage_customer ON usage(customer_id)`, `CREATE INDEX IF NOT EXISTS idx_usage_date ON usage(created_at)`, ]; // ─── Seed Plans ────────────────────────────────────────────────────────── const SEED_PLANS = [ { id: 'plan_starter', name: 'Starter', slug: 'starter', description: 'Get started with sovereign AI. Self-hosted cluster, 5 agents, 50 skills.', amount: 0, interval: 'month', tier: 0, features: JSON.stringify([ 'Self-hosted cluster', '5 core agents', '50 AI skills', '90 CLI tools', 'Sovereign fleet models', ]), stripe_price_id: 'price_1TAjzB3e5FMFdlFwJrJaGhtK', }, { id: 'plan_sovereign', name: 'Sovereign', slug: 'sovereign', description: 'Full sovereign infrastructure. All agents, priority support, SLA.', amount: 4900, interval: 'month', tier: 1, features: JSON.stringify([ 'Everything in Starter', 'Full agent fleet', 'Priority infrastructure', '99.9% SLA guarantee', 'Direct support line', 'Custom agent configs', 'Advanced analytics', ]), stripe_price_id: 'price_1TAjzC3e5FMFdlFwPFUMCzSI', }, { id: 'plan_enterprise', name: 'Enterprise', slug: 'enterprise', description: 'Custom deployment. White-label, on-prem, air-gapped.', amount: 0, interval: 'month', tier: 2, features: JSON.stringify([ 'Everything in Sovereign', 'White-label OS', 'On-prem or air-gapped', 'Dedicated success team', 'Custom integrations', 'Volume licensing', '24/7 phone support', ]), stripe_price_id: null, }, ]; const SEED_ADDONS = [ { id: 'addon_lucidia', name: 'Lucidia Enhanced', slug: 'lucidia-enhanced', description: 'Advanced AI companion with memory, personality, and learning.', amount: 999, interval: 'month', stripe_price_id: 'price_1TAk3e3e5FMFdlFwGi46sMNf', }, { id: 'addon_roadauth', name: 'RoadAuth', slug: 'roadauth', description: 'Decentralized identity and auth for your apps.', amount: 499, interval: 'month', stripe_price_id: 'price_1TAk3f3e5FMFdlFwfYwLHZVk', }, { id: 'addon_context_bridge', name: 'Context Bridge', slug: 'context-bridge', description: 'Cross-agent memory sharing and context persistence.', amount: 799, interval: 'month', stripe_price_id: 'price_1TAk3g3e5FMFdlFwNkgWv2tU', }, { id: 'addon_knowledge_hub', name: 'Knowledge Hub', slug: 'knowledge-hub', description: 'RAG pipeline with vector search across your data.', amount: 1499, interval: 'month', stripe_price_id: 'price_1TAk3i3e5FMFdlFwlGXxMWFJ', }, ]; // ─── Helpers ───────────────────────────────────────────────────────────── function uid() { return crypto.randomUUID().replace(/-/g, '').slice(0, 20); } function json(data, status = 200) { return Response.json(data, { status }); } function err(message, status = 400) { return Response.json({ error: message }, { status }); } const SECURITY_HEADERS = { 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'SAMEORIGIN', 'X-XSS-Protection': '1; mode=block', 'Referrer-Policy': 'strict-origin-when-cross-origin', 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload', }; function corsHeaders(origin) { return { 'Access-Control-Allow-Origin': origin || '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-RoadPay-Key', 'Access-Control-Max-Age': '86400', }; } async function logEvent(db, type, customerId, entityType, entityId, data = {}) { await db.prepare( 'INSERT INTO events (id, type, customer_id, entity_type, entity_id, data) VALUES (?, ?, ?, ?, ?, ?)' ).bind(`evt_${uid()}`, type, customerId, entityType, entityId, JSON.stringify(data)).run(); } async function stripeRequest(env, method, path, body = null) { if (!env.STRIPE_SECRET_KEY) return null; const url = `https://api.stripe.com/v1${path}`; const headers = { 'Authorization': `Bearer ${env.STRIPE_SECRET_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded', }; const options = { method, headers }; if (body) options.body = new URLSearchParams(body).toString(); const res = await fetch(url, options); return res.json(); } // ─── Auth middleware ───────────────────────────────────────────────────── async function authenticate(request, env) { // Admin key for server-to-server const apiKey = request.headers.get('X-RoadPay-Key'); if (apiKey && env.ROADPAY_ADMIN_KEY && apiKey === env.ROADPAY_ADMIN_KEY) { return { role: 'admin', email: 'admin' }; } // Bearer token — verify with auth.blackroad.io const auth = request.headers.get('Authorization'); if (auth?.startsWith('Bearer ')) { const token = auth.slice(7); try { // Standard response headers const requestId = crypto.randomUUID().slice(0, 8); const res = await fetch(`${env.AUTH_API || 'https://auth.blackroad.io'}/api/me`, { headers: { 'Authorization': `Bearer ${token}` }, }); if (res.ok) { const user = await res.json(); return { role: 'user', email: user.email, userId: user.id }; } } catch {} } return null; } // ─── Init ──────────────────────────────────────────────────────────────── async function handleInit(db) { for (const sql of SCHEMA) { await db.prepare(sql).run(); } // Seed plans for (const plan of SEED_PLANS) { await db.prepare( `INSERT OR IGNORE INTO plans (id, name, slug, description, amount, interval, tier, features, stripe_price_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` ).bind(plan.id, plan.name, plan.slug, plan.description, plan.amount, plan.interval, plan.tier, plan.features, plan.stripe_price_id).run(); } // Seed addons for (const addon of SEED_ADDONS) { await db.prepare( `INSERT OR IGNORE INTO addons (id, name, slug, description, amount, interval, stripe_price_id) VALUES (?, ?, ?, ?, ?, ?, ?)` ).bind(addon.id, addon.name, addon.slug, addon.description, addon.amount, addon.interval, addon.stripe_price_id).run(); } return json({ ok: true, tables: SCHEMA.length, plans: SEED_PLANS.length, addons: SEED_ADDONS.length }); } // ─── Customers ─────────────────────────────────────────────────────────── async function handleCustomers(request, db, env) { const url = new URL(request.url); if (request.method === 'GET') { const email = url.searchParams.get('email'); const id = url.searchParams.get('id'); if (id) { const customer = await db.prepare('SELECT * FROM customers WHERE id = ?').bind(id).first(); if (!customer) return err('Customer not found', 404); // Get their subscriptions const subs = await db.prepare( `SELECT s.*, p.name as plan_name, p.slug as plan_slug, p.amount as plan_amount FROM subscriptions s JOIN plans p ON s.plan_id = p.id WHERE s.customer_id = ? ORDER BY s.created_at DESC` ).bind(id).all(); return json({ customer, subscriptions: subs.results }); } if (email) { const customer = await db.prepare('SELECT * FROM customers WHERE email = ?').bind(email).first(); if (!customer) return err('Customer not found', 404); return json({ customer }); } const limit = parseInt(url.searchParams.get('limit') || '50'); const offset = parseInt(url.searchParams.get('offset') || '0'); const customers = await db.prepare( 'SELECT * FROM customers ORDER BY created_at DESC LIMIT ? OFFSET ?' ).bind(limit, offset).all(); const count = await db.prepare('SELECT COUNT(*) as total FROM customers').first(); return json({ customers: customers.results, total: count.total }); } if (request.method === 'POST') { const body = await request.json(); const { email, name, metadata } = body; if (!email) return err('email is required'); // Check existing const existing = await db.prepare('SELECT id FROM customers WHERE email = ?').bind(email).first(); if (existing) return err('Customer already exists', 409); const id = `cus_${uid()}`; // Create Stripe customer if key exists let stripeId = null; if (env.STRIPE_SECRET_KEY) { const stripeCustomer = await stripeRequest(env, 'POST', '/customers', { email, name: name || '', 'metadata[roadpay_id]': id, 'metadata[source]': 'roadpay', }); stripeId = stripeCustomer?.id; } await db.prepare( 'INSERT INTO customers (id, email, name, stripe_customer_id, metadata) VALUES (?, ?, ?, ?, ?)' ).bind(id, email, name || null, stripeId, JSON.stringify(metadata || {})).run(); await logEvent(db, 'customer.created', id, 'customer', id, { email }); return json({ id, email, name, stripe_customer_id: stripeId }, 201); } return err('Method not allowed', 405); } // ─── Plans ─────────────────────────────────────────────────────────────── async function handlePlans(db) { const plans = await db.prepare('SELECT * FROM plans WHERE active = 1 ORDER BY tier ASC').all(); return json({ plans: plans.results.map(p => ({ ...p, features: JSON.parse(p.features || '[]'), metadata: JSON.parse(p.metadata || '{}'), })), }); } // ─── Addons ────────────────────────────────────────────────────────────── async function handleAddons(db) { const addons = await db.prepare('SELECT * FROM addons WHERE active = 1 ORDER BY amount ASC').all(); return json({ addons: addons.results.map(a => ({ ...a, metadata: JSON.parse(a.metadata || '{}'), })), }); } // ─── Subscribe ─────────────────────────────────────────────────────────── async function handleSubscribe(request, db, env) { const body = await request.json(); const { customer_id, plan_slug, addon_slugs = [], success_url, cancel_url } = body; if (!customer_id || !plan_slug) return err('customer_id and plan_slug are required'); const customer = await db.prepare('SELECT * FROM customers WHERE id = ?').bind(customer_id).first(); if (!customer) return err('Customer not found', 404); const plan = await db.prepare('SELECT * FROM plans WHERE slug = ?').bind(plan_slug).first(); if (!plan) return err('Plan not found', 404); // Check existing active subscription const existing = await db.prepare( "SELECT id FROM subscriptions WHERE customer_id = ? AND status = 'active'" ).bind(customer_id).first(); if (existing) return err('Customer already has an active subscription. Cancel first or upgrade.', 409); // Free plan — no payment needed if (plan.amount === 0 && !plan.stripe_price_id) { const subId = `sub_${uid()}`; const now = new Date().toISOString(); const periodEnd = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(); await db.prepare( `INSERT INTO subscriptions (id, customer_id, plan_id, status, current_period_start, current_period_end) VALUES (?, ?, ?, 'active', ?, ?)` ).bind(subId, customer_id, plan.id, now, periodEnd).run(); // Create invoice at $0 await db.prepare( `INSERT INTO invoices (id, customer_id, subscription_id, amount, status, description, paid_at) VALUES (?, ?, ?, 0, 'paid', ?, ?)` ).bind(`inv_${uid()}`, customer_id, subId, `${plan.name} — Free`, now).run(); await logEvent(db, 'subscription.created', customer_id, 'subscription', subId, { plan: plan.slug }); return json({ subscription_id: subId, plan: plan.slug, status: 'active' }); } // Paid plan — create Stripe checkout if (!env.STRIPE_SECRET_KEY) { return err('Payment processing not configured', 503); } // Build line items const lineItems = {}; lineItems['line_items[0][price]'] = plan.stripe_price_id; lineItems['line_items[0][quantity]'] = '1'; // Add addon line items if (addon_slugs.length > 0) { const addons = await db.prepare( `SELECT * FROM addons WHERE slug IN (${addon_slugs.map(() => '?').join(',')}) AND active = 1` ).bind(...addon_slugs).all(); addons.results.forEach((addon, i) => { lineItems[`line_items[${i + 1}][price]`] = addon.stripe_price_id; lineItems[`line_items[${i + 1}][quantity]`] = '1'; }); } const origin = new URL(request.url).origin; const subId = `sub_${uid()}`; const session = await stripeRequest(env, 'POST', '/checkout/sessions', { mode: 'subscription', ...lineItems, customer: customer.stripe_customer_id || undefined, customer_email: !customer.stripe_customer_id ? customer.email : undefined, success_url: success_url || `${origin}/success?session_id={CHECKOUT_SESSION_ID}&sub=${subId}`, cancel_url: cancel_url || `${origin}/pricing`, 'subscription_data[metadata][roadpay_sub_id]': subId, 'subscription_data[metadata][roadpay_customer_id]': customer_id, 'subscription_data[metadata][plan]': plan.slug, 'automatic_tax[enabled]': 'true', }); if (session.error) { return err(session.error.message, 500); } // Create pending subscription await db.prepare( `INSERT INTO subscriptions (id, customer_id, plan_id, status, metadata) VALUES (?, ?, ?, 'pending', ?)` ).bind(subId, customer_id, plan.id, JSON.stringify({ stripe_checkout_session: session.id })).run(); await logEvent(db, 'checkout.started', customer_id, 'subscription', subId, { plan: plan.slug }); return json({ checkout_url: session.url, subscription_id: subId, session_id: session.id }); } // ─── Manage Subscription ───────────────────────────────────────────────── async function handleSubscription(request, db, env) { const url = new URL(request.url); const subId = url.pathname.split('/').pop(); if (request.method === 'GET') { const sub = await db.prepare( `SELECT s.*, p.name as plan_name, p.slug as plan_slug, p.amount as plan_amount, p.features FROM subscriptions s JOIN plans p ON s.plan_id = p.id WHERE s.id = ?` ).bind(subId).first(); if (!sub) return err('Subscription not found', 404); // Get addons const addons = await db.prepare( `SELECT sa.*, a.name as addon_name, a.slug as addon_slug, a.amount as addon_amount FROM subscription_addons sa JOIN addons a ON sa.addon_id = a.id WHERE sa.subscription_id = ? AND sa.status = 'active'` ).bind(subId).all(); return json({ subscription: { ...sub, features: JSON.parse(sub.features || '[]'), metadata: JSON.parse(sub.metadata || '{}'), }, addons: addons.results, }); } if (request.method === 'DELETE') { const sub = await db.prepare('SELECT * FROM subscriptions WHERE id = ?').bind(subId).first(); if (!sub) return err('Subscription not found', 404); // Cancel in Stripe if (sub.stripe_subscription_id && env.STRIPE_SECRET_KEY) { await stripeRequest(env, 'DELETE', `/subscriptions/${sub.stripe_subscription_id}`); } const now = new Date().toISOString(); await db.prepare( "UPDATE subscriptions SET status = 'canceled', canceled_at = ?, updated_at = ? WHERE id = ?" ).bind(now, now, subId).run(); await logEvent(db, 'subscription.canceled', sub.customer_id, 'subscription', subId); return json({ ok: true, status: 'canceled' }); } return err('Method not allowed', 405); } // ─── Invoices ──────────────────────────────────────────────────────────── async function handleInvoices(request, db) { const url = new URL(request.url); const customerId = url.searchParams.get('customer_id'); if (!customerId) return err('customer_id is required'); const invoices = await db.prepare( 'SELECT * FROM invoices WHERE customer_id = ? ORDER BY created_at DESC LIMIT 50' ).bind(customerId).all(); return json({ invoices: invoices.results.map(inv => ({ ...inv, line_items: JSON.parse(inv.line_items || '[]'), metadata: JSON.parse(inv.metadata || '{}'), })), }); } // ─── Payments ──────────────────────────────────────────────────────────── async function handlePayments(request, db) { const url = new URL(request.url); const customerId = url.searchParams.get('customer_id'); if (!customerId) return err('customer_id is required'); const payments = await db.prepare( 'SELECT * FROM payments WHERE customer_id = ? ORDER BY created_at DESC LIMIT 50' ).bind(customerId).all(); return json({ payments: payments.results }); } // ─── Billing Portal (own) ──────────────────────────────────────────────── async function handlePortal(request, db) { const { customer_id } = await request.json(); if (!customer_id) return err('customer_id is required'); const customer = await db.prepare('SELECT * FROM customers WHERE id = ?').bind(customer_id).first(); if (!customer) return err('Customer not found', 404); const subs = await db.prepare( `SELECT s.*, p.name as plan_name, p.slug as plan_slug, p.amount as plan_amount, p.features FROM subscriptions s JOIN plans p ON s.plan_id = p.id WHERE s.customer_id = ? ORDER BY s.created_at DESC` ).bind(customer_id).all(); const invoices = await db.prepare( 'SELECT * FROM invoices WHERE customer_id = ? ORDER BY created_at DESC LIMIT 10' ).bind(customer_id).all(); const payments = await db.prepare( 'SELECT * FROM payments WHERE customer_id = ? ORDER BY created_at DESC LIMIT 10' ).bind(customer_id).all(); return json({ customer: { ...customer, metadata: JSON.parse(customer.metadata || '{}') }, subscriptions: subs.results.map(s => ({ ...s, features: JSON.parse(s.features || '[]'), metadata: JSON.parse(s.metadata || '{}'), })), invoices: invoices.results.map(i => ({ ...i, line_items: JSON.parse(i.line_items || '[]'), })), payments: payments.results, }); } // ─── Webhook (Stripe → RoadPay sync) ──────────────────────────────────── async function handleWebhook(request, db, env) { const signature = request.headers.get('stripe-signature'); const body = await request.text(); if (env.STRIPE_WEBHOOK_SECRET && signature) { try { await verifySignature(body, signature, env.STRIPE_WEBHOOK_SECRET); } catch (e) { return err(`Webhook verification failed: ${e.message}`, 400); } } let event; try { event = JSON.parse(body); } catch { return err('Invalid JSON', 400); } const now = new Date().toISOString(); switch (event.type) { case 'checkout.session.completed': { const session = event.data.object; const subId = session.metadata?.roadpay_sub_id; const customerId = session.metadata?.roadpay_customer_id; if (subId) { await db.prepare( `UPDATE subscriptions SET status = 'active', stripe_subscription_id = ?, current_period_start = ?, updated_at = ? WHERE id = ?` ).bind(session.subscription, now, now, subId).run(); } // Update customer's Stripe ID if we didn't have it if (customerId && session.customer) { await db.prepare( 'UPDATE customers SET stripe_customer_id = ?, updated_at = ? WHERE id = ? AND stripe_customer_id IS NULL' ).bind(session.customer, now, customerId).run(); } await logEvent(db, 'checkout.completed', customerId, 'subscription', subId, { stripe_session: session.id, }); break; } case 'invoice.payment_succeeded': { const invoice = event.data.object; const stripeSubId = invoice.subscription; // Find our subscription const sub = await db.prepare( 'SELECT * FROM subscriptions WHERE stripe_subscription_id = ?' ).bind(stripeSubId).first(); if (sub) { // Create invoice record await db.prepare( `INSERT INTO invoices (id, customer_id, subscription_id, stripe_invoice_id, amount, status, description, paid_at) VALUES (?, ?, ?, ?, ?, 'paid', ?, ?)` ).bind( `inv_${uid()}`, sub.customer_id, sub.id, invoice.id, invoice.amount_paid, `Payment for ${invoice.lines?.data?.[0]?.description || 'subscription'}`, now ).run(); // Create payment record await db.prepare( `INSERT INTO payments (id, customer_id, invoice_id, stripe_payment_id, amount, status) VALUES (?, ?, ?, ?, ?, 'succeeded')` ).bind(`pay_${uid()}`, sub.customer_id, invoice.id, invoice.payment_intent, invoice.amount_paid).run(); // Update subscription period const periodEnd = invoice.lines?.data?.[0]?.period?.end; if (periodEnd) { await db.prepare( 'UPDATE subscriptions SET current_period_end = ?, updated_at = ? WHERE id = ?' ).bind(new Date(periodEnd * 1000).toISOString(), now, sub.id).run(); } await logEvent(db, 'payment.succeeded', sub.customer_id, 'payment', invoice.payment_intent, { amount: invoice.amount_paid, }); } break; } case 'invoice.payment_failed': { const invoice = event.data.object; const sub = await db.prepare( 'SELECT * FROM subscriptions WHERE stripe_subscription_id = ?' ).bind(invoice.subscription).first(); if (sub) { await db.prepare( "UPDATE subscriptions SET status = 'past_due', updated_at = ? WHERE id = ?" ).bind(now, sub.id).run(); await logEvent(db, 'payment.failed', sub.customer_id, 'subscription', sub.id, { amount: invoice.amount_due, }); } break; } case 'customer.subscription.deleted': { const stripeSub = event.data.object; const sub = await db.prepare( 'SELECT * FROM subscriptions WHERE stripe_subscription_id = ?' ).bind(stripeSub.id).first(); if (sub) { await db.prepare( "UPDATE subscriptions SET status = 'canceled', canceled_at = ?, updated_at = ? WHERE id = ?" ).bind(now, now, sub.id).run(); await logEvent(db, 'subscription.canceled', sub.customer_id, 'subscription', sub.id); } break; } case 'customer.subscription.updated': { const stripeSub = event.data.object; const sub = await db.prepare( 'SELECT * FROM subscriptions WHERE stripe_subscription_id = ?' ).bind(stripeSub.id).first(); if (sub) { await db.prepare( 'UPDATE subscriptions SET status = ?, updated_at = ? WHERE id = ?' ).bind(stripeSub.status, now, sub.id).run(); } break; } default: console.log(`Unhandled webhook: ${event.type}`); } return json({ received: true }); } async function verifySignature(payload, sigHeader, secret) { if (!sigHeader) throw new Error('Missing signature'); const parts = sigHeader.split(',').reduce((acc, part) => { const [k, v] = part.split('='); acc[k] = v; return acc; }, {}); const timestamp = parts.t; const sigs = Object.entries(parts).filter(([k]) => k === 'v1').map(([, v]) => v); if (!timestamp || !sigs.length) throw new Error('Invalid signature format'); const tolerance = 300; if (Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp)) > tolerance) { throw new Error('Timestamp outside tolerance'); } const enc = new TextEncoder(); const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); const sig = await crypto.subtle.sign('HMAC', key, enc.encode(`${timestamp}.${payload}`)); const computed = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join(''); if (!sigs.includes(computed)) throw new Error('Signature mismatch'); } // ─── Stats / Dashboard ────────────────────────────────────────────────── async function handleStats(db) { const customers = await db.prepare('SELECT COUNT(*) as total FROM customers').first(); const activeSubs = await db.prepare("SELECT COUNT(*) as total FROM subscriptions WHERE status = 'active'").first(); const totalRevenue = await db.prepare("SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE status = 'succeeded'").first(); const invoicesPaid = await db.prepare("SELECT COUNT(*) as total FROM invoices WHERE status = 'paid'").first(); const recentEvents = await db.prepare('SELECT * FROM events ORDER BY created_at DESC LIMIT 20').all(); // MRR calculation const mrr = await db.prepare( `SELECT COALESCE(SUM(p.amount), 0) as mrr FROM subscriptions s JOIN plans p ON s.plan_id = p.id WHERE s.status = 'active'` ).first(); // Plan breakdown const planBreakdown = await db.prepare( `SELECT p.name, p.slug, COUNT(s.id) as subscribers, p.amount FROM plans p LEFT JOIN subscriptions s ON s.plan_id = p.id AND s.status = 'active' GROUP BY p.id ORDER BY p.tier ASC` ).all(); return json({ stats: { customers: customers.total, active_subscriptions: activeSubs.total, total_revenue_cents: totalRevenue.total, total_revenue: `$${(totalRevenue.total / 100).toFixed(2)}`, mrr_cents: mrr.mrr, mrr: `$${(mrr.mrr / 100).toFixed(2)}`, invoices_paid: invoicesPaid.total, }, plan_breakdown: planBreakdown.results, recent_events: recentEvents.results.map(e => ({ ...e, data: JSON.parse(e.data || '{}'), })), }); } // ─── Lookup (by email — public-ish) ────────────────────────────────────── async function handleLookup(request, db) { const url = new URL(request.url); const email = url.searchParams.get('email'); if (!email) return err('email is required'); const customer = await db.prepare('SELECT id, email, name, status, created_at FROM customers WHERE email = ?').bind(email).first(); if (!customer) return json({ found: false }); const sub = await db.prepare( `SELECT s.id, s.status, s.current_period_end, p.name as plan_name, p.slug as plan_slug, p.tier FROM subscriptions s JOIN plans p ON s.plan_id = p.id WHERE s.customer_id = ? AND s.status = 'active' ORDER BY s.created_at DESC LIMIT 1` ).bind(customer.id).first(); return json({ found: true, customer_id: customer.id, email: customer.email, name: customer.name, plan: sub ? { name: sub.plan_name, slug: sub.plan_slug, tier: sub.tier, status: sub.status, period_end: sub.current_period_end } : null, }); } // ─── Upgrade / Downgrade ───────────────────────────────────────────────── async function handleUpgrade(request, db, env) { const { customer_id, new_plan_slug } = await request.json(); if (!customer_id || !new_plan_slug) return err('customer_id and new_plan_slug required'); const customer = await db.prepare('SELECT * FROM customers WHERE id = ?').bind(customer_id).first(); if (!customer) return err('Customer not found', 404); const newPlan = await db.prepare('SELECT * FROM plans WHERE slug = ?').bind(new_plan_slug).first(); if (!newPlan) return err('Plan not found', 404); const currentSub = await db.prepare( "SELECT * FROM subscriptions WHERE customer_id = ? AND status = 'active'" ).bind(customer_id).first(); if (!currentSub) return err('No active subscription to upgrade', 404); const now = new Date().toISOString(); // If upgrading to enterprise/free (contact only) or downgrading to free if (!newPlan.stripe_price_id || newPlan.amount === 0) { // Cancel stripe sub if (currentSub.stripe_subscription_id && env.STRIPE_SECRET_KEY) { await stripeRequest(env, 'DELETE', `/subscriptions/${currentSub.stripe_subscription_id}`); } await db.prepare( "UPDATE subscriptions SET status = 'canceled', canceled_at = ?, updated_at = ? WHERE id = ?" ).bind(now, now, currentSub.id).run(); // Create new free sub const newSubId = `sub_${uid()}`; await db.prepare( `INSERT INTO subscriptions (id, customer_id, plan_id, status, current_period_start, current_period_end) VALUES (?, ?, ?, 'active', ?, ?)` ).bind(newSubId, customer_id, newPlan.id, now, new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString()).run(); await logEvent(db, 'subscription.changed', customer_id, 'subscription', newSubId, { from: currentSub.plan_id, to: newPlan.id, }); return json({ subscription_id: newSubId, plan: newPlan.slug, status: 'active' }); } // Stripe subscription update (proration) if (currentSub.stripe_subscription_id && env.STRIPE_SECRET_KEY) { // Get current subscription items const stripeSub = await stripeRequest(env, 'GET', `/subscriptions/${currentSub.stripe_subscription_id}`); if (stripeSub?.items?.data?.[0]) { await stripeRequest(env, 'POST', `/subscriptions/${currentSub.stripe_subscription_id}`, { 'items[0][id]': stripeSub.items.data[0].id, 'items[0][price]': newPlan.stripe_price_id, proration_behavior: 'create_prorations', }); } await db.prepare( 'UPDATE subscriptions SET plan_id = ?, updated_at = ? WHERE id = ?' ).bind(newPlan.id, now, currentSub.id).run(); await logEvent(db, 'subscription.upgraded', customer_id, 'subscription', currentSub.id, { from: currentSub.plan_id, to: newPlan.id, }); return json({ subscription_id: currentSub.id, plan: newPlan.slug, status: 'active', prorated: true }); } return err('Cannot upgrade — no active Stripe subscription', 400); } // ─── Landing Page ──────────────────────────────────────────────────────── function handleLandingPage() { const html = `
AI agents, fleet management, and full API access. Start free, scale when you need to. Your data stays yours.
Plans
Every plan includes access to BlackRoad's AI agent platform. Upgrade or downgrade anytime.
Add-ons
Stack capabilities on top of any paid plan. Each add-on is billed monthly.
FAQ