Files
blackroad/workers/tollbooth-src/worker.js
Alexa Amundson c7d0216824
Some checks failed
Lint & Format / detect (push) Has been cancelled
Lint & Format / js-lint (push) Has been cancelled
Lint & Format / py-lint (push) Has been cancelled
Lint & Format / sh-lint (push) Has been cancelled
Lint & Format / go-lint (push) Has been cancelled
Monorepo Lint / lint-shell (push) Has been cancelled
Monorepo Lint / lint-js (push) Has been cancelled
sync: 2026-03-16 14:30 — 21 files from Alexandria
RoadChain-SHA2048: 6d362d0de7cfcef6
RoadChain-Identity: alexa@sovereign
RoadChain-Full: 6d362d0de7cfcef69b3f1c30d3671d86cf0f52efaeae70456b8d127d9f4d1569f805014c9404e14915d44b3a113d0a5096b11eee481c9d5c89119327c4a018549f15221f31fe7fbd03420e6dce9a79964ddc1a89a0835c711c4a76cf9f8838d8121dcdcd5eee1f5bfe98c5c04609da27a17962a541b2d7a87a81e9499d0c128c32e662994f31022bfb187d57c0fd7e311727b4adb02bf64fab3a315bb18b1b7b0a28ea52a9b0f8d66229724e909042e6ab31d3c580a763f3cebd8d7d839422bbb6e89b586dcce6d638f246c7f96257eab7abf80d01a6ebb065faa9d33e1a56c1ae7cecd67204a6902e14cae9ed88eb343b327bc8877c649ef5b09b9ee817abc1
2026-03-16 14:30:01 -05:00

1502 lines
61 KiB
JavaScript

// 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 = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RoadPay — Pricing &amp; Plans | BlackRoad OS</title>
<link rel="icon" type="image/x-icon" href="https://images.blackroad.io/brand/favicon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="https://images.blackroad.io/brand/br-square-192.png" />
<link rel="apple-touch-icon" sizes="180x180" href="https://images.blackroad.io/brand/apple-touch-icon.png" />
<meta property="og:image" content="https://images.blackroad.io/brand/blackroad-icon-512.png" />
<meta property="og:title" content="RoadPay — Pricing &amp; Plans" />
<meta property="og:description" content="Choose your plan. AI agents, fleet tools, and full API access — starting free." />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="description" content="RoadPay pricing plans and add-ons. AI agents, fleet management, and full API access — starting free." />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--g: linear-gradient(90deg,#FF6B2B,#FF2255,#CC00AA,#8844FF,#4488FF,#00D4FF);
--g135: linear-gradient(135deg,#FF6B2B,#FF2255,#CC00AA,#8844FF,#4488FF,#00D4FF);
--bg: #000; --card: #0a0a0a; --elevated: #111; --hover: #181818;
--border: #1a1a1a; --muted: #444; --sub: #737373; --text: #f5f5f5; --white: #fff;
--sg: 'Space Grotesk', sans-serif; --jb: 'JetBrains Mono', monospace;
}
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
html { scroll-behavior: smooth; }
body { background: var(--bg); color: var(--text); font-family: var(--sg); overflow-x: hidden; -webkit-font-smoothing: antialiased; }
::-webkit-scrollbar { width:6px; } ::-webkit-scrollbar-track { background:#000; }
::-webkit-scrollbar-thumb { background: linear-gradient(180deg,#FF6B2B,#FF2255,#8844FF,#00D4FF); border-radius:3px; }
.gradient-text { background: var(--g); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.gradient-border { border:1px solid transparent; background-origin:border-box; background-clip:padding-box,border-box; background-image:linear-gradient(#0a0a0a,#0a0a0a), var(--g); }
.gradient-line { height:1px; background: linear-gradient(90deg,transparent,#FF2255,#8844FF,#00D4FF,transparent); }
/* Nav */
nav { position:fixed; top:0; left:0; right:0; z-index:100; padding:16px 40px; display:flex; align-items:center; justify-content:space-between; background:rgba(0,0,0,.85); backdrop-filter:blur(12px); border-bottom:1px solid var(--border); }
.nav-logo { font-family:var(--jb); font-size:14px; letter-spacing:2px; text-transform:uppercase; opacity:.6; }
.nav-links { display:flex; gap:24px; }
.nav-links a { color:rgba(255,255,255,.5); text-decoration:none; font-size:14px; transition:color .3s; }
.nav-links a:hover { color:#fff; }
/* Hero */
.hero { padding:160px 40px 80px; text-align:center; max-width:900px; margin:0 auto; }
.hero h1 { font-size:clamp(48px,8vw,88px); font-weight:700; letter-spacing:-2px; line-height:1; }
.hero p { font-size:clamp(16px,2vw,20px); opacity:.5; margin-top:20px; max-width:560px; margin-left:auto; margin-right:auto; line-height:1.6; }
.badge { display:inline-block; font-family:var(--jb); font-size:12px; letter-spacing:3px; text-transform:uppercase; opacity:.35; margin-bottom:16px; }
/* Pricing Section */
.section { padding:80px 40px; max-width:1200px; margin:0 auto; }
.section-label { font-family:var(--jb); font-size:12px; letter-spacing:4px; text-transform:uppercase; opacity:.35; margin-bottom:12px; }
.section-title { font-size:clamp(32px,4vw,52px); font-weight:700; line-height:1.1; margin-bottom:16px; }
.section-sub { font-size:16px; opacity:.45; max-width:560px; line-height:1.6; margin-bottom:48px; }
/* Plan Cards */
.plans-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:20px; }
@media(max-width:1024px) { .plans-grid { grid-template-columns:repeat(2,1fr); } }
@media(max-width:600px) { .plans-grid { grid-template-columns:1fr; } }
.plan-card { padding:32px 28px; border-radius:16px; position:relative; transition:transform .3s, box-shadow .3s; display:flex; flex-direction:column; }
.plan-card:hover { transform:translateY(-6px); }
.plan-card.featured { box-shadow:0 0 80px rgba(255,34,85,.08); }
.plan-card.featured::before { content:'Most Popular'; position:absolute; top:-12px; left:50%; transform:translateX(-50%); font-family:var(--jb); font-size:11px; letter-spacing:2px; text-transform:uppercase; background:var(--g); color:#000; padding:4px 16px; border-radius:20px; font-weight:700; }
.plan-name { font-size:20px; font-weight:700; margin-bottom:4px; }
.plan-desc { font-size:13px; opacity:.4; margin-bottom:20px; min-height:36px; }
.plan-price { font-size:48px; font-weight:700; letter-spacing:-2px; }
.plan-price span { font-size:16px; opacity:.4; font-weight:400; letter-spacing:0; }
.plan-period { font-family:var(--jb); font-size:12px; opacity:.3; margin-bottom:24px; }
.plan-features { list-style:none; flex:1; margin-bottom:24px; }
.plan-features li { padding:8px 0; font-size:14px; opacity:.55; border-bottom:1px solid rgba(255,255,255,.04); display:flex; align-items:center; gap:8px; }
.plan-features li::before { content:'\\2713'; color:#00D4FF; font-size:12px; flex-shrink:0; }
.plan-cta { display:block; text-align:center; padding:14px 24px; border-radius:12px; color:#fff; text-decoration:none; font-weight:600; font-size:14px; transition:all .3s; border:1px solid rgba(255,255,255,.15); }
.plan-cta:hover { border-color:#FF2255; background:rgba(255,34,85,.1); transform:translateY(-1px); }
.plan-cta.primary { background:var(--g); border:none; color:#000; font-weight:700; }
.plan-cta.primary:hover { opacity:.9; }
/* Add-ons */
.addons-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:16px; }
.addon-card { padding:24px; border-radius:14px; display:flex; flex-direction:column; gap:8px; transition:transform .3s; }
.addon-card:hover { transform:translateY(-3px); }
.addon-header { display:flex; justify-content:space-between; align-items:center; }
.addon-name { font-weight:700; font-size:16px; }
.addon-price { font-family:var(--jb); font-size:14px; opacity:.5; }
.addon-desc { font-size:13px; opacity:.4; line-height:1.5; }
/* FAQ */
.faq-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(340px,1fr)); gap:24px; }
.faq-item { padding:28px; border-radius:14px; }
.faq-q { font-weight:700; font-size:16px; margin-bottom:8px; }
.faq-a { font-size:14px; opacity:.45; line-height:1.7; }
/* Footer */
.footer { border-top:1px solid var(--border); padding:60px 40px; text-align:center; margin-top:80px; }
.footer-links { display:flex; flex-wrap:wrap; justify-content:center; gap:24px; margin-bottom:24px; }
.footer-links a { color:rgba(255,255,255,.4); text-decoration:none; font-size:14px; transition:color .3s; }
.footer-links a:hover { color:#fff; }
/* Animations */
.fade-up { opacity:0; transform:translateY(30px); transition:opacity .7s ease, transform .7s ease; }
.fade-up.visible { opacity:1; transform:translateY(0); }
@media(max-width:768px) { .section { padding:60px 20px; } nav { padding:14px 20px; } .hero { padding:120px 20px 60px; } }
</style>
</head>
<body>
<nav>
<div class="nav-logo">RoadPay</div>
<div class="nav-links">
<a href="https://blackroad.io">Home</a>
<a href="#plans">Plans</a>
<a href="#addons">Add-ons</a>
<a href="#faq">FAQ</a>
<a href="/health">API</a>
</div>
</nav>
<section class="hero">
<div class="badge">Billing by BlackRoad</div>
<h1 class="gradient-text">Choose your road.</h1>
<p>AI agents, fleet management, and full API access. Start free, scale when you need to. Your data stays yours.</p>
</section>
<div class="gradient-line"></div>
<section class="section" id="plans">
<div class="fade-up">
<p class="section-label">Plans</p>
<h2 class="section-title">Simple pricing. No surprises.</h2>
<p class="section-sub">Every plan includes access to BlackRoad's AI agent platform. Upgrade or downgrade anytime.</p>
</div>
<div class="plans-grid fade-up" id="plans-grid">
<div class="plan-card gradient-border">
<div class="plan-name">Starter</div>
<div class="plan-desc">Get started with AI agents at no cost.</div>
<div class="plan-price">$0</div>
<div class="plan-period">free forever</div>
<ul class="plan-features">
<li>1 AI agent</li>
<li>Community support</li>
<li>Basic RoadSearch</li>
<li>Public dashboard</li>
</ul>
<a href="/subscribe?plan=operator" class="plan-cta">Get Started</a>
</div>
<div class="plan-card gradient-border featured">
<div class="plan-name">Sovereign</div>
<div class="plan-desc">For builders who need more power.</div>
<div class="plan-price">$29<span>/mo</span></div>
<div class="plan-period">billed monthly</div>
<ul class="plan-features">
<li>5 AI agents</li>
<li>Priority support</li>
<li>Full API access</li>
<li>RoadSearch Pro</li>
<li>Custom dashboards</li>
<li>Webhook integrations</li>
</ul>
<a href="/subscribe?plan=rider" class="plan-cta primary">Subscribe</a>
</div>
<div class="plan-card gradient-border">
<div class="plan-name">Enterprise</div>
<div class="plan-desc">For teams that build together.</div>
<div class="plan-price">$99<span>/mo</span></div>
<div class="plan-period">billed monthly</div>
<ul class="plan-features">
<li>25 AI agents</li>
<li>Dedicated support</li>
<li>Fleet management</li>
<li>Team workspaces</li>
<li>Advanced analytics</li>
<li>Priority inference</li>
<li>Custom models</li>
</ul>
<a href="/subscribe?plan=paver" class="plan-cta">Subscribe</a>
</div>
<div class="plan-card gradient-border">
<div class="plan-name">Enterprise</div>
<div class="plan-desc">Tailored to your organization.</div>
<div class="plan-price">Custom</div>
<div class="plan-period">per agreement</div>
<ul class="plan-features">
<li>Unlimited agents</li>
<li>Dedicated infrastructure</li>
<li>Custom SLAs</li>
<li>White-label option</li>
<li>Private deployment</li>
<li>Direct engineering support</li>
<li>Custom integrations</li>
</ul>
<a href="mailto:alexa@blackroad.io?subject=Enterprise%20Plan" class="plan-cta">Contact Us</a>
</div>
</div>
</section>
<div class="gradient-line"></div>
<section class="section" id="addons">
<div class="fade-up">
<p class="section-label">Add-ons</p>
<h2 class="section-title">Extend your plan.</h2>
<p class="section-sub">Stack capabilities on top of any paid plan. Each add-on is billed monthly.</p>
</div>
<div class="addons-grid fade-up">
<div class="addon-card gradient-border">
<div class="addon-header"><span class="addon-name">Lucidia Enhanced</span><span class="addon-price">$9.99/mo</span></div>
<div class="addon-desc">Advanced AI companion with memory, personality, and continuous learning.</div>
</div>
<div class="addon-card gradient-border">
<div class="addon-header"><span class="addon-name">RoadAuth</span><span class="addon-price">$4.99/mo</span></div>
<div class="addon-desc">Identity and authentication for your apps. Your data stays yours.</div>
</div>
<div class="addon-card gradient-border">
<div class="addon-header"><span class="addon-name">Context Bridge</span><span class="addon-price">$7.99/mo</span></div>
<div class="addon-desc">Cross-agent memory sharing and context that persists between sessions.</div>
</div>
<div class="addon-card gradient-border">
<div class="addon-header"><span class="addon-name">Knowledge Hub</span><span class="addon-price">$14.99/mo</span></div>
<div class="addon-desc">RAG pipeline with vector search across all your data. Answers grounded in your content.</div>
</div>
</div>
</section>
<div class="gradient-line"></div>
<section class="section" id="faq">
<div class="fade-up">
<p class="section-label">FAQ</p>
<h2 class="section-title">Common questions.</h2>
</div>
<div class="faq-grid fade-up">
<div class="faq-item gradient-border">
<div class="faq-q">How does billing work?</div>
<div class="faq-a">All plans are billed monthly. You can upgrade, downgrade, or cancel anytime from your dashboard. Upgrades are prorated automatically.</div>
</div>
<div class="faq-item gradient-border">
<div class="faq-q">What payment methods do you accept?</div>
<div class="faq-a">We accept all major credit and debit cards through our secure payment processor. No cryptocurrency at this time.</div>
</div>
<div class="faq-item gradient-border">
<div class="faq-q">Can I try before I buy?</div>
<div class="faq-a">The Starter plan is free to get started. Use it as long as you like. When you need more agents or features, upgrade in one click.</div>
</div>
<div class="faq-item gradient-border">
<div class="faq-q">Where does my data live?</div>
<div class="faq-a">Your data stays yours, always. It runs on your device or on edge infrastructure close to you. We never sell or share your data.</div>
</div>
<div class="faq-item gradient-border">
<div class="faq-q">What happens if I cancel?</div>
<div class="faq-a">Your account downgrades to the Starter plan at the end of your billing period. No data is deleted. You can re-subscribe anytime.</div>
</div>
<div class="faq-item gradient-border">
<div class="faq-q">Do add-ons require a paid plan?</div>
<div class="faq-a">Add-ons are available on the Rider plan and above. They stack on top of your base plan and are billed separately each month.</div>
</div>
</div>
</section>
<footer class="footer">
<div class="footer-links">
<a href="https://blackroad.io">Home</a>
<a href="https://lucidia.earth">Lucidia</a>
<a href="https://blackroadai.com">AI</a>
<a href="https://blackroad.network">Network</a>
<a href="https://blackroad.systems">Cloud</a>
<a href="https://status.blackroad.io">Status</a>
<a href="https://blackroad.company">Company</a>
<a href="https://search.blackroad.io">Search</a>
<a href="https://github.com/blackboxprogramming">GitHub</a>
</div>
<p style="color:rgba(255,255,255,.3);font-size:14px">BlackRoad OS &mdash; Pave Tomorrow.</p>
<p style="color:rgba(255,255,255,.15);font-size:12px;margin-top:8px">&copy; 2025&ndash;2026 BlackRoad OS, Inc. All rights reserved.</p>
</footer>
<script>
const obs = new IntersectionObserver(entries => {
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
}, { threshold: 0.1 });
document.querySelectorAll('.fade-up').forEach(el => obs.observe(el));
</script>
</body>
</html>`;
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html;charset=utf-8' },
});
}
// ─── Health ──────────────────────────────────────────────────────────────
function handleHealth() {
return json({
status: 'ok',
service: 'roadpay',
version: VERSION,
time: new Date().toISOString(),
endpoints: [
'GET / (landing page or health)',
'GET /pricing',
'GET /health',
'GET /init',
'GET /plans',
'GET /addons',
'GET /stats',
'GET /lookup?email=',
'GET /customers?email=|id=',
'POST /customers',
'POST /subscribe',
'GET /subscriptions/:id',
'DELETE /subscriptions/:id',
'POST /upgrade',
'POST /portal',
'GET /invoices?customer_id=',
'GET /payments?customer_id=',
'POST /webhook',
'GET /keys?customer_id=',
'POST /keys',
'DELETE /keys?id=',
'GET /usage?customer_id=&days=30',
'POST /usage/record',
],
});
}
// ─── API Keys ───────────────────────────────────────────────────────────
async function handleApiKeys(request, db) {
const url = new URL(request.url);
if (request.method === 'GET') {
const customerId = url.searchParams.get('customer_id');
if (!customerId) return err('customer_id required');
const keys = await db.prepare(
"SELECT id, customer_id, key_prefix, name, scopes, rate_limit, last_used, usage_count, status, expires_at, created_at FROM api_keys WHERE customer_id = ? ORDER BY created_at DESC"
).bind(customerId).all();
return json({ keys: keys.results.map(k => ({ ...k, scopes: JSON.parse(k.scopes || '[]') })) });
}
if (request.method === 'POST') {
const body = await request.json();
const { customer_id, name, scopes, rate_limit, expires_in_days } = body;
if (!customer_id) return err('customer_id required');
// Generate API key: rp_live_ + 32 random hex chars
const rawKey = `rp_live_${randHex(16)}`;
const keyPrefix = rawKey.slice(0, 12);
// Hash the key for storage (don't store raw)
const enc = new TextEncoder();
const hashBuf = await crypto.subtle.digest('SHA-256', enc.encode(rawKey));
const keyHash = Array.from(new Uint8Array(hashBuf)).map(b => b.toString(16).padStart(2, '0')).join('');
const id = `key_${uid()}`;
const expiresAt = expires_in_days
? new Date(Date.now() + expires_in_days * 86400000).toISOString()
: null;
await db.prepare(
'INSERT INTO api_keys (id, customer_id, key_hash, key_prefix, name, scopes, rate_limit, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
).bind(id, customer_id, keyHash, keyPrefix, name || 'default', JSON.stringify(scopes || ['api:read']), rate_limit || 1000, expiresAt).run();
// Return the raw key ONLY on creation — can never be retrieved again
return json({ id, key: rawKey, prefix: keyPrefix, name: name || 'default', warning: 'Save this key now. It cannot be retrieved again.' }, 201);
}
if (request.method === 'DELETE') {
const keyId = url.searchParams.get('id');
if (!keyId) return err('id required');
await db.prepare("UPDATE api_keys SET status = 'revoked' WHERE id = ?").bind(keyId).run();
return json({ ok: true, revoked: keyId });
}
return err('Method not allowed', 405);
}
// ─── Usage / Metering ────────────────────────────────────────────────────
async function handleUsage(request, db) {
const url = new URL(request.url);
const customerId = url.searchParams.get('customer_id');
if (!customerId) return err('customer_id required');
const days = parseInt(url.searchParams.get('days') || '30');
const since = new Date(Date.now() - days * 86400000).toISOString();
// Daily breakdown
const daily = await db.prepare(
`SELECT date(created_at) as day, COUNT(*) as requests, SUM(tokens_used) as tokens,
AVG(latency_ms) as avg_latency, SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END) as errors
FROM usage WHERE customer_id = ? AND created_at >= ? GROUP BY day ORDER BY day DESC`
).bind(customerId, since).all();
// Totals
const totals = await db.prepare(
`SELECT COUNT(*) as total_requests, SUM(tokens_used) as total_tokens,
AVG(latency_ms) as avg_latency, COUNT(DISTINCT endpoint) as unique_endpoints
FROM usage WHERE customer_id = ? AND created_at >= ?`
).bind(customerId, since).first();
// Top endpoints
const endpoints = await db.prepare(
`SELECT endpoint, method, COUNT(*) as count, AVG(latency_ms) as avg_latency
FROM usage WHERE customer_id = ? AND created_at >= ?
GROUP BY endpoint, method ORDER BY count DESC LIMIT 10`
).bind(customerId, since).all();
return json({
customer_id: customerId,
period: { days, since },
totals,
daily: daily.results,
top_endpoints: endpoints.results,
});
}
// ─── Record Usage (internal — called by API gateway) ─────────────────────
async function handleRecordUsage(request, db) {
const body = await request.json();
const { customer_id, api_key_id, endpoint, method, status_code, latency_ms, tokens_used } = body;
if (!customer_id || !endpoint) return err('customer_id and endpoint required');
await db.prepare(
'INSERT INTO usage (customer_id, api_key_id, endpoint, method, status_code, latency_ms, tokens_used) VALUES (?, ?, ?, ?, ?, ?, ?)'
).bind(customer_id, api_key_id || null, endpoint, method || 'GET', status_code || 200, latency_ms || 0, tokens_used || 0).run();
// Update API key usage count
if (api_key_id) {
await db.prepare(
"UPDATE api_keys SET usage_count = usage_count + 1, last_used = datetime('now') WHERE id = ?"
).bind(api_key_id).run();
}
return json({ ok: true });
}
function randHex(bytes) {
const arr = new Uint8Array(bytes);
crypto.getRandomValues(arr);
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
}
// ─── Rate limiting ───────────────────────────────────────────────────────
const rl = new Map();
function rateLimit(ip, max = 30, windowSec = 60) {
const now = Date.now();
const entry = rl.get(ip);
if (!entry || now - entry.t > windowSec * 1000) { rl.set(ip, { t: now, c: 1 }); return true; }
return ++entry.c <= max;
}
// ─── Router ──────────────────────────────────────────────────────────────
export default {
async fetch(request, env) {
const url = new URL(request.url);
const origin = request.headers.get('Origin') || '*';
const cors = corsHeaders(origin);
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: { ...cors, ...SECURITY_HEADERS } });
}
// Rate limit all requests
const clientIp = request.headers.get('cf-connecting-ip') || 'unknown';
if (!rateLimit(clientIp)) {
return addHeaders(err('Too many requests — slow down', 429), cors);
}
const db = env.DB;
let response;
try {
const path = url.pathname;
// Landing page — serve HTML for browsers
if ((path === '/' || path === '/pricing') && request.method === 'GET') {
const accept = request.headers.get('Accept') || '';
if (accept.includes('text/html')) {
return addHeaders(handleLandingPage(), cors);
}
}
// Public endpoints (no auth)
if (path === '/' && request.method === 'GET') return addHeaders(handleHealth(), cors);
if (path === '/health') return addHeaders(handleHealth(), cors);
if (path === '/init') return addHeaders(await handleInit(db), cors);
if (path === '/plans') return addHeaders(await handlePlans(db), cors);
if (path === '/addons') return addHeaders(await handleAddons(db), cors);
if (path === '/webhook' && request.method === 'POST') return addHeaders(await handleWebhook(request, db, env), cors);
if (path === '/lookup') return addHeaders(await handleLookup(request, db), cors);
// Authenticated endpoints
const user = await authenticate(request, env);
if (!user) {
return addHeaders(err('Unauthorized — provide Bearer token or X-RoadPay-Key', 401), cors);
}
switch (true) {
case path === '/customers':
response = await handleCustomers(request, db, env);
break;
case path === '/subscribe' && request.method === 'POST':
response = await handleSubscribe(request, db, env);
break;
case path.startsWith('/subscriptions/'):
response = await handleSubscription(request, db, env);
break;
case path === '/upgrade' && request.method === 'POST':
response = await handleUpgrade(request, db, env);
break;
case path === '/portal' && request.method === 'POST':
response = await handlePortal(request, db);
break;
case path === '/invoices':
response = await handleInvoices(request, db);
break;
case path === '/payments':
response = await handlePayments(request, db);
break;
case path === '/stats':
response = await handleStats(db);
break;
case path === '/keys' || path === '/api-keys':
response = await handleApiKeys(request, db);
break;
case path === '/usage':
response = await handleUsage(request, db);
break;
case path === '/usage/record' && request.method === 'POST':
response = await handleRecordUsage(request, db);
break;
default:
response = err('Not found', 404);
}
} catch (e) {
console.error('RoadPay error:', e);
response = err(e.message, 500);
}
return addHeaders(response, cors);
},
};
function addHeaders(response, cors) {
const headers = new Headers(response.headers);
for (const [k, v] of Object.entries({ ...cors, ...SECURITY_HEADERS })) {
headers.set(k, v);
}
return new Response(response.body, { status: response.status, headers });
}