scottzilla-payments / server.mjs
ScottzillaSystems's picture
add: payment server β€” Stripe + PayPal + Coinbase + Square + Gumroad with API key management
9c60318 verified
import express from "express";
import cors from "cors";
import crypto from "crypto";
import { readFileSync } from "fs";
import Stripe from "stripe";
import https from "https";
const app = express();
const PORT = 7860;
// ─── Config ──────────────────────────────────────────────────────────────────
const pricing = JSON.parse(readFileSync("pricing.json", "utf-8"));
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString("hex");
const GATEWAY_URL = "https://scottzillasystems-scottzilla-gateway.hf.space";
// Provider configs (from Space secrets)
const STRIPE_KEY = process.env.STRIPE_SECRET_KEY || "";
const STRIPE_WH = process.env.STRIPE_WEBHOOK_SECRET || "";
const PAYPAL_ID = process.env.PAYPAL_CLIENT_ID || "";
const PAYPAL_SECRET = process.env.PAYPAL_CLIENT_SECRET || "";
const COINBASE_KEY = process.env.COINBASE_API_KEY || "";
const COINBASE_WH = process.env.COINBASE_WEBHOOK_SECRET || "";
const SQUARE_TOKEN = process.env.SQUARE_ACCESS_TOKEN || "";
const GUMROAD_TOKEN = process.env.GUMROAD_ACCESS_TOKEN || "";
const stripe = STRIPE_KEY ? new Stripe(STRIPE_KEY) : null;
// ─── In-memory store (swap for DB in production) ─────────────────────────────
const apiKeys = new Map(); // key β†’ {tier, email, created, usage_today, usage_total, balance, active}
const sessions = new Map(); // session_id β†’ {key, provider, tier, status}
app.use(cors());
// Raw body for Stripe webhooks
app.use("/api/webhooks/stripe", express.raw({ type: "application/json" }));
app.use(express.json());
// ─── Helpers ─────────────────────────────────────────────────────────────────
function generateApiKey() {
return "sz_" + crypto.randomBytes(24).toString("hex");
}
function getKeyData(key) {
if (!key) return null;
const clean = key.replace(/^Bearer\s+/i, "").trim();
return apiKeys.get(clean) || null;
}
function checkRateLimit(keyData) {
const tier = pricing.tiers[keyData.tier];
if (!tier) return false;
if (tier.requests_per_day === -1) return true; // unlimited
// Reset daily counter
const today = new Date().toISOString().split("T")[0];
if (keyData.last_reset !== today) {
keyData.usage_today = 0;
keyData.last_reset = today;
}
return keyData.usage_today < tier.requests_per_day;
}
function canAccessModel(keyData, modelAlias) {
const tier = pricing.tiers[keyData.tier];
if (!tier) return false;
if (tier.models.includes("all")) return true;
return tier.models.includes(modelAlias);
}
// ─── Health / Dashboard ──────────────────────────────────────────────────────
app.get("/", (req, res) => {
const providers = {
stripe: !!STRIPE_KEY,
paypal: !!PAYPAL_ID,
coinbase: !!COINBASE_KEY,
square: !!SQUARE_TOKEN,
gumroad: !!GUMROAD_TOKEN,
};
res.send(`<!DOCTYPE html>
<html><head><title>Scottzilla Payments</title>
<style>
body{font-family:system-ui;background:#0f0f23;color:#f1f5f9;max-width:900px;margin:0 auto;padding:2rem}
h1{background:linear-gradient(135deg,#6366f1,#06b6d4);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-size:2.5rem}
.card{background:#1e293b;border:1px solid #334155;border-radius:16px;padding:1.5rem;margin:1rem 0}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:1rem}
.tier{background:#1e293b;border:1px solid #334155;border-radius:16px;padding:1.5rem;text-align:center}
.tier h3{color:#818cf8;margin:0 0 .5rem}.price{font-size:2rem;font-weight:800;color:#f1f5f9}
.badge{display:inline-block;padding:.25rem .75rem;border-radius:100px;font-size:.75rem;font-weight:600}
.badge.on{background:rgba(16,185,129,.15);color:#10b981}.badge.off{background:rgba(239,68,68,.15);color:#ef4444}
table{width:100%;border-collapse:collapse}th,td{padding:.75rem;text-align:left;border-bottom:1px solid #334155}
th{color:#94a3b8;font-size:.75rem;text-transform:uppercase}
a{color:#818cf8}code{background:#0f172a;padding:.25rem .5rem;border-radius:6px;font-size:.875rem}
.btn{display:inline-block;padding:.75rem 1.5rem;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;border-radius:12px;text-decoration:none;font-weight:600;margin:.25rem}
</style></head><body>
<h1>πŸ’° Scottzilla Payments</h1>
<p>Multi-provider payment server for the Scottzilla AI platform.</p>
<div class="card">
<h2>Payment Providers</h2>
<table>
<tr><th>Provider</th><th>Status</th><th>Supports</th></tr>
<tr><td>Stripe</td><td><span class="badge ${providers.stripe ? "on" : "off"}">${providers.stripe ? "βœ… Connected" : "⚠️ Add STRIPE_SECRET_KEY"}</span></td><td>Cards, Apple Pay, Google Pay</td></tr>
<tr><td>PayPal</td><td><span class="badge ${providers.paypal ? "on" : "off"}">${providers.paypal ? "βœ… Connected" : "⚠️ Add PAYPAL_CLIENT_ID"}</span></td><td>PayPal, cards</td></tr>
<tr><td>Coinbase Commerce</td><td><span class="badge ${providers.coinbase ? "on" : "off"}">${providers.coinbase ? "βœ… Connected" : "⚠️ Add COINBASE_API_KEY"}</span></td><td>BTC, ETH, USDC</td></tr>
<tr><td>Square (Cash App)</td><td><span class="badge ${providers.square ? "on" : "off"}">${providers.square ? "βœ… Connected" : "⚠️ Add SQUARE_ACCESS_TOKEN"}</span></td><td>Cash App Pay</td></tr>
<tr><td>Gumroad</td><td><span class="badge ${providers.gumroad ? "on" : "off"}">${providers.gumroad ? "βœ… Connected" : "⚠️ Add GUMROAD_ACCESS_TOKEN"}</span></td><td>Simple checkout + affiliates</td></tr>
</table>
</div>
<h2>Pricing Tiers</h2>
<div class="grid">
${Object.entries(pricing.tiers).map(([id, t]) => `
<div class="tier">
<h3>${t.name}</h3>
<div class="price">$${t.price_monthly}<small>/mo</small></div>
<p>${t.requests_per_day === -1 ? "Unlimited" : t.requests_per_day} requests/day</p>
<p style="color:#94a3b8;font-size:.875rem">${t.features.join(", ")}</p>
</div>`).join("")}
</div>
<div class="card">
<h2>Quick Start</h2>
<p>1. Get a free API key:</p>
<code>curl -X POST ${req.headers.host ? "https://" + req.headers.host : ""}/api/keys/create -H "Content-Type: application/json" -d '{"email":"you@example.com"}'</code>
<p style="margin-top:1rem">2. Use it with the Scottzilla Gateway:</p>
<code>curl ${GATEWAY_URL}/v1/chat/completions -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" -d '{"model":"auto","messages":[{"role":"user","content":"Hello"}]}'</code>
<p style="margin-top:1rem">3. Upgrade for more models + requests:</p>
<code>curl -X POST ${req.headers.host ? "https://" + req.headers.host : ""}/api/checkout/stripe -H "Content-Type: application/json" -d '{"api_key":"YOUR_KEY","tier":"pro"}'</code>
</div>
<div class="card">
<h2>API Reference</h2>
<table>
<tr><th>Endpoint</th><th>Description</th></tr>
<tr><td><code>POST /api/keys/create</code></td><td>Create free API key (body: {email})</td></tr>
<tr><td><code>GET /api/keys/verify/:key</code></td><td>Verify key, get tier + usage</td></tr>
<tr><td><code>POST /api/checkout/:provider</code></td><td>Create payment checkout (body: {api_key, tier})</td></tr>
<tr><td><code>POST /api/webhooks/:provider</code></td><td>Payment confirmation webhooks</td></tr>
<tr><td><code>GET /api/usage/:key</code></td><td>Detailed usage stats</td></tr>
<tr><td><code>GET /api/pricing</code></td><td>Current pricing tiers</td></tr>
<tr><td><code>POST /api/usage/record</code></td><td>Record API usage (called by gateway)</td></tr>
</table>
</div>
<p style="text-align:center;color:#64748b;margin-top:2rem">
<a href="${GATEWAY_URL}">Scottzilla Gateway</a> Β·
<a href="https://huggingface.co/ScottzillaSystems">All Models</a> Β·
Active keys: ${apiKeys.size}
</p>
</body></html>`);
});
// ─── API Key Management ──────────────────────────────────────────────────────
app.post("/api/keys/create", (req, res) => {
const { email } = req.body || {};
if (!email) return res.status(400).json({ error: "email required" });
const key = generateApiKey();
apiKeys.set(key, {
tier: "free",
email,
created: new Date().toISOString(),
usage_today: 0,
usage_total: 0,
balance: 0,
active: true,
last_reset: new Date().toISOString().split("T")[0],
});
res.json({
api_key: key,
tier: "free",
limits: pricing.tiers.free,
message: "API key created. Use it with the Scottzilla Gateway.",
gateway: GATEWAY_URL,
});
});
app.get("/api/keys/verify/:key", (req, res) => {
const data = apiKeys.get(req.params.key);
if (!data) return res.status(404).json({ error: "invalid API key" });
const tier = pricing.tiers[data.tier];
const withinLimit = checkRateLimit(data);
res.json({
valid: true,
active: data.active,
tier: data.tier,
tier_name: tier?.name,
usage_today: data.usage_today,
limit_today: tier?.requests_per_day,
usage_total: data.usage_total,
balance: data.balance,
within_limit: withinLimit,
models: tier?.models || [],
features: tier?.features || [],
});
});
// ─── Usage Recording (called by gateway) ─────────────────────────────────────
app.post("/api/usage/record", (req, res) => {
const { api_key, model, tokens } = req.body || {};
const data = apiKeys.get(api_key);
if (!data) return res.status(404).json({ error: "invalid key" });
data.usage_today++;
data.usage_total++;
// Pay-as-you-go: deduct from balance
if (data.tier === "free" && data.balance > 0) {
data.balance -= pricing.payg.price_per_request;
}
res.json({ recorded: true, usage_today: data.usage_today });
});
app.get("/api/usage/:key", (req, res) => {
const data = apiKeys.get(req.params.key);
if (!data) return res.status(404).json({ error: "invalid key" });
res.json({
usage_today: data.usage_today,
usage_total: data.usage_total,
balance: data.balance,
tier: data.tier,
created: data.created,
});
});
// ─── Pricing ─────────────────────────────────────────────────────────────────
app.get("/api/pricing", (req, res) => res.json(pricing));
// ─── Stripe Checkout ─────────────────────────────────────────────────────────
app.post("/api/checkout/stripe", async (req, res) => {
if (!stripe) return res.status(503).json({ error: "Stripe not configured. Add STRIPE_SECRET_KEY secret." });
const { api_key, tier, amount } = req.body;
const data = apiKeys.get(api_key);
if (!data) return res.status(404).json({ error: "invalid API key" });
try {
const tierConfig = pricing.tiers[tier];
if (tier && tierConfig) {
// Subscription checkout
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: tierConfig.stripe_price_id, quantity: 1 }],
success_url: `https://scottzillasystems-scottzilla-payments.hf.space/?success=true&key=${api_key}`,
cancel_url: `https://scottzillasystems-scottzilla-payments.hf.space/?cancelled=true`,
metadata: { api_key, tier },
});
return res.json({ checkout_url: session.url, session_id: session.id });
} else if (amount) {
// Pay-as-you-go top-up
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price_data: { currency: "usd", product_data: { name: "Scottzilla API Credits" }, unit_amount: Math.round(amount * 100) }, quantity: 1 }],
success_url: `https://scottzillasystems-scottzilla-payments.hf.space/?success=true&key=${api_key}`,
cancel_url: `https://scottzillasystems-scottzilla-payments.hf.space/?cancelled=true`,
metadata: { api_key, type: "topup", amount: String(amount) },
});
return res.json({ checkout_url: session.url, session_id: session.id });
}
res.status(400).json({ error: "Specify tier or amount" });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ─── PayPal Checkout ─────────────────────────────────────────────────────────
app.post("/api/checkout/paypal", async (req, res) => {
if (!PAYPAL_ID) return res.status(503).json({ error: "PayPal not configured. Add PAYPAL_CLIENT_ID + PAYPAL_CLIENT_SECRET secrets." });
const { api_key, tier, amount } = req.body;
const data = apiKeys.get(api_key);
if (!data) return res.status(404).json({ error: "invalid API key" });
const tierConfig = pricing.tiers[tier];
const payAmount = amount || tierConfig?.price_monthly || 0;
// Get PayPal access token
const authResp = await fetch("https://api-m.paypal.com/v1/oauth2/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from(`${PAYPAL_ID}:${PAYPAL_SECRET}`).toString("base64")}` },
body: "grant_type=client_credentials",
});
const { access_token } = await authResp.json();
const orderResp = await fetch("https://api-m.paypal.com/v2/checkout/orders", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${access_token}` },
body: JSON.stringify({
intent: "CAPTURE",
purchase_units: [{ amount: { currency_code: "USD", value: String(payAmount) }, description: `Scottzilla ${tier || "credits"} - ${api_key.slice(0, 8)}...` }],
application_context: {
return_url: `https://scottzillasystems-scottzilla-payments.hf.space/api/webhooks/paypal?key=${api_key}&tier=${tier || "topup"}&amount=${payAmount}`,
cancel_url: "https://scottzillasystems-scottzilla-payments.hf.space/?cancelled=true",
},
}),
});
const order = await orderResp.json();
const approveLink = order.links?.find((l) => l.rel === "approve");
res.json({ checkout_url: approveLink?.href, order_id: order.id });
});
// ─── Coinbase Commerce Checkout ──────────────────────────────────────────────
app.post("/api/checkout/coinbase", async (req, res) => {
if (!COINBASE_KEY) return res.status(503).json({ error: "Coinbase not configured. Add COINBASE_API_KEY secret." });
const { api_key, tier, amount } = req.body;
const data = apiKeys.get(api_key);
if (!data) return res.status(404).json({ error: "invalid API key" });
const tierConfig = pricing.tiers[tier];
const payAmount = amount || tierConfig?.price_monthly || 0;
const resp = await fetch("https://api.commerce.coinbase.com/charges", {
method: "POST",
headers: { "Content-Type": "application/json", "X-CC-Api-Key": COINBASE_KEY, "X-CC-Version": "2018-03-22" },
body: JSON.stringify({
name: `Scottzilla ${tier || "Credits"}`,
description: `API access - ${tier || "pay-as-you-go"}`,
pricing_type: "fixed_price",
local_price: { amount: String(payAmount), currency: "USD" },
metadata: { api_key, tier: tier || "topup" },
redirect_url: `https://scottzillasystems-scottzilla-payments.hf.space/?success=true&key=${api_key}`,
cancel_url: "https://scottzillasystems-scottzilla-payments.hf.space/?cancelled=true",
}),
});
const charge = await resp.json();
res.json({ checkout_url: charge.data?.hosted_url, charge_id: charge.data?.id });
});
// ─── Square / Cash App Checkout ──────────────────────────────────────────────
app.post("/api/checkout/square", async (req, res) => {
if (!SQUARE_TOKEN) return res.status(503).json({ error: "Square not configured. Add SQUARE_ACCESS_TOKEN secret." });
const { api_key, tier, amount } = req.body;
const data = apiKeys.get(api_key);
if (!data) return res.status(404).json({ error: "invalid API key" });
const tierConfig = pricing.tiers[tier];
const payAmount = Math.round((amount || tierConfig?.price_monthly || 0) * 100);
const resp = await fetch("https://connect.squareup.com/v2/online-checkout/payment-links", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${SQUARE_TOKEN}`, "Square-Version": "2024-01-18" },
body: JSON.stringify({
idempotency_key: crypto.randomUUID(),
quick_pay: { name: `Scottzilla ${tier || "Credits"}`, price_money: { amount: payAmount, currency: "USD" }, location_id: "main" },
checkout_options: { redirect_url: `https://scottzillasystems-scottzilla-payments.hf.space/?success=true&key=${api_key}` },
pre_populated_data: { buyer_email: data.email },
}),
});
const link = await resp.json();
res.json({ checkout_url: link.payment_link?.long_url || link.payment_link?.url, link_id: link.payment_link?.id });
});
// ─── Gumroad Checkout ────────────────────────────────────────────────────────
app.post("/api/checkout/gumroad", async (req, res) => {
const { api_key, tier } = req.body;
const data = apiKeys.get(api_key);
if (!data) return res.status(404).json({ error: "invalid API key" });
const tierConfig = pricing.tiers[tier];
const productId = tierConfig?.gumroad_product_id;
// Gumroad uses hosted checkout pages β€” return the URL directly
res.json({
checkout_url: productId
? `https://scottzillasystems.gumroad.com/l/${productId}?api_key=${api_key}`
: "https://scottzillasystems.gumroad.com",
note: "Complete purchase on Gumroad, then your key will be upgraded via webhook.",
});
});
// ─── Webhooks ────────────────────────────────────────────────────────────────
app.post("/api/webhooks/stripe", (req, res) => {
if (!stripe) return res.sendStatus(200);
try {
const sig = req.headers["stripe-signature"];
const event = STRIPE_WH ? stripe.webhooks.constructEvent(req.body, sig, STRIPE_WH) : JSON.parse(req.body);
if (event.type === "checkout.session.completed") {
const session = event.data.object;
const { api_key, tier, type, amount } = session.metadata || {};
const data = apiKeys.get(api_key);
if (data) {
if (type === "topup") {
data.balance += parseFloat(amount || 0);
} else if (tier) {
data.tier = tier;
}
console.log(`[stripe] ${api_key.slice(0, 10)}... β†’ ${tier || "topup $" + amount}`);
}
}
res.json({ received: true });
} catch (err) {
console.error("[stripe webhook]", err.message);
res.status(400).json({ error: err.message });
}
});
app.post("/api/webhooks/paypal", (req, res) => {
const { key, tier, amount } = req.query;
const data = apiKeys.get(key);
if (data && tier && tier !== "topup") {
data.tier = tier;
console.log(`[paypal] ${key?.slice(0, 10)}... β†’ ${tier}`);
} else if (data && amount) {
data.balance += parseFloat(amount);
console.log(`[paypal] ${key?.slice(0, 10)}... β†’ topup $${amount}`);
}
res.redirect(`https://scottzillasystems-scottzilla-payments.hf.space/?success=true`);
});
app.post("/api/webhooks/coinbase", (req, res) => {
const event = req.body?.event;
if (event?.type === "charge:confirmed") {
const { api_key, tier } = event.data?.metadata || {};
const data = apiKeys.get(api_key);
if (data && tier && tier !== "topup") {
data.tier = tier;
console.log(`[coinbase] ${api_key?.slice(0, 10)}... β†’ ${tier}`);
}
}
res.json({ received: true });
});
app.post("/api/webhooks/gumroad", (req, res) => {
const { email, custom_fields } = req.body || {};
// Gumroad sends custom_fields with the api_key
const api_key = custom_fields?.api_key || req.body?.api_key;
const data = apiKeys.get(api_key);
if (data) {
data.tier = "pro"; // upgrade to pro on any Gumroad purchase
console.log(`[gumroad] ${api_key?.slice(0, 10)}... β†’ pro`);
}
res.json({ received: true });
});
// ─── Start ───────────────────────────────────────────────────────────────────
app.listen(PORT, "0.0.0.0", () => {
console.log(`πŸ’° Scottzilla Payments listening on :${PORT}`);
console.log(` Providers: Stripe=${!!STRIPE_KEY} PayPal=${!!PAYPAL_ID} Coinbase=${!!COINBASE_KEY} Square=${!!SQUARE_TOKEN} Gumroad=${!!GUMROAD_TOKEN}`);
});