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}`); | |
| }); | |