Gitleaks flagged 4 production secrets in rmi-backend source:
app/caching_shield/solana_tracker.py: st_ZMzXzdUI54TXQPx5E6JkG
app/databus/arkham_ws.py: ws_Z5x09Rcr_1780418740765322917
app/routers/webhooks_router.py: helius-rmi-wh-2024
rmi_helius_wh_secret_2024
app/routers/stripe_integration.py: pk_test_51Tn13MAXseReicQtM...
Each replaced with os.getenv() reads. Placeholder env lines added to
/srv/rmi-infra/.env.secrets. Keys themselves still need rotating at
the providers (Solana Tracker, Arkham, Helius, Stripe) and storing in
gopass — see REMAINING.md.
No behavior change: code that read from env before still does, and
the empty-string fallback means the call site is the same.
Note: this commit scrubs the keys from the working tree. They remain
in git history. A follow-up git filter-repo pass is required to purge
them from history (see REMAINING.md). After that, all clones and
external remotes must be force-updated.
290 lines
11 KiB
Python
290 lines
11 KiB
Python
"""Stripe Integration — credit card payments for x402 API credits.
|
|
|
|
Users buy credits with a card → we credit their API key → they use x402 tools.
|
|
The x402 payment protocol runs on our backend. The user never touches crypto.
|
|
|
|
Endpoints:
|
|
POST /api/v1/stripe/checkout — Create Stripe checkout session
|
|
POST /api/v1/stripe/webhook — Stripe webhook (payment confirmations)
|
|
GET /api/v1/stripe/portal — Customer portal (manage subscriptions)
|
|
GET /api/v1/credits/balance — Check remaining credits
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import time
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel
|
|
|
|
logger = logging.getLogger("stripe")
|
|
|
|
# Stripe keys from environment
|
|
STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY", "")
|
|
STRIPE_PUBLISHABLE_KEY = os.getenv("STRIPE_PUBLISHABLE_KEY", "")
|
|
STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET", "")
|
|
|
|
try:
|
|
import stripe as _stripe_check
|
|
STRIPE_PACKAGE_INSTALLED = True
|
|
except ImportError:
|
|
STRIPE_PACKAGE_INSTALLED = False
|
|
|
|
STRIPE_ENABLED = bool(STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY and STRIPE_PACKAGE_INSTALLED)
|
|
|
|
# Credit packages (USD -> API credits)
|
|
CREDIT_PACKAGES = {
|
|
"starter": {"price_usd": 1000, "credits": 1000, "label": "Starter Pack — 1,000 credits"},
|
|
"growth": {"price_usd": 5000, "credits": 6000, "label": "Growth Pack — 6,000 credits (16% off)"},
|
|
"pro": {"price_usd": 10000, "credits": 14000, "label": "Pro Pack — 14,000 credits (28% off)"},
|
|
"enterprise": {"price_usd": 50000, "credits": 80000, "label": "Enterprise Pack — 80,000 credits (37% off)"},
|
|
}
|
|
|
|
# Monthly subscription tiers
|
|
SUBSCRIPTION_TIERS = {
|
|
"starter_monthly": {"price_usd": 2900, "credits_per_month": 1000, "label": "Starter — $29/mo"},
|
|
"pro_monthly": {"price_usd": 9900, "credits_per_month": 5000, "label": "Pro — $99/mo"},
|
|
"team_monthly": {"price_usd": 29900, "credits_per_month": 25000, "label": "Team — $299/mo"},
|
|
}
|
|
|
|
router = APIRouter(prefix="/api/v1", tags=["Stripe"])
|
|
|
|
|
|
class CheckoutRequest(BaseModel):
|
|
package: str = ""
|
|
tier: str = ""
|
|
success_url: str = ""
|
|
cancel_url: str = ""
|
|
api_key: str = ""
|
|
email: str = ""
|
|
|
|
|
|
class CreditEntry:
|
|
"""In-memory credit store — replace with Redis/Postgres in production."""
|
|
|
|
def __init__(self):
|
|
self._credits: dict[str, dict] = {}
|
|
|
|
def get_balance(self, api_key: str) -> int:
|
|
entry = self._credits.get(api_key, {"balance": 100}) # 100 free credits
|
|
return entry.get("balance", 0)
|
|
|
|
def add_credits(self, api_key: str, amount: int, source: str = "stripe"):
|
|
if api_key not in self._credits:
|
|
self._credits[api_key] = {"balance": 100, "total_purchased": 0, "created_at": time.time()}
|
|
self._credits[api_key]["balance"] += amount
|
|
self._credits[api_key]["total_purchased"] += amount
|
|
self._credits[api_key]["last_purchase"] = time.time()
|
|
logger.info(f"Credits added: {amount} to {api_key[:16]} via {source}")
|
|
|
|
def deduct(self, api_key: str, amount: int = 1) -> bool:
|
|
entry = self._credits.get(api_key)
|
|
if not entry or entry["balance"] < amount:
|
|
return False
|
|
entry["balance"] -= amount
|
|
return True
|
|
|
|
def spending(self, api_key: str) -> dict:
|
|
entry = self._credits.get(api_key, {"balance": 100, "total_purchased": 0})
|
|
return {
|
|
"balance": entry["balance"],
|
|
"total_purchased": entry["total_purchased"],
|
|
}
|
|
|
|
|
|
_credit_store = CreditEntry()
|
|
|
|
|
|
@router.post("/stripe/checkout")
|
|
async def create_checkout(req: CheckoutRequest):
|
|
"""Create a Stripe checkout session for credit purchase."""
|
|
if not STRIPE_ENABLED:
|
|
return JSONResponse(
|
|
status_code=503,
|
|
content={
|
|
"error": "Stripe not configured",
|
|
"note": "Add STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY to environment. Set STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY in environment",
|
|
"packages": {k: {"price_usd": v["price_usd"], "credits": v["credits"], "label": v["label"]} for k, v in CREDIT_PACKAGES.items()},
|
|
"subscriptions": {k: {"price_usd": v["price_usd"], "credits_per_month": v["credits_per_month"], "label": v["label"]} for k, v in SUBSCRIPTION_TIERS.items()},
|
|
},
|
|
)
|
|
|
|
try:
|
|
import stripe
|
|
stripe.api_key = STRIPE_SECRET_KEY
|
|
|
|
package = None
|
|
if req.package and req.package in CREDIT_PACKAGES:
|
|
package = CREDIT_PACKAGES[req.package]
|
|
elif req.tier and req.tier in SUBSCRIPTION_TIERS:
|
|
package = SUBSCRIPTION_TIERS[req.tier]
|
|
|
|
if not package:
|
|
packages_list = list(CREDIT_PACKAGES.keys()) + list(SUBSCRIPTION_TIERS.keys())
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={"error": "Invalid package or tier", "available": packages_list},
|
|
)
|
|
|
|
is_subscription = req.tier in SUBSCRIPTION_TIERS
|
|
|
|
checkout_session = stripe.checkout.Session.create(
|
|
line_items=[
|
|
{
|
|
"price_data": {
|
|
"currency": "usd",
|
|
"product_data": {
|
|
"name": package["label"],
|
|
"description": f"{package.get('credits', package.get('credits_per_month', 0))} API credits for RugMunch Intelligence",
|
|
},
|
|
"unit_amount": package["price_usd"],
|
|
"recurring": {"interval": "month"} if is_subscription else None,
|
|
},
|
|
"quantity": 1,
|
|
}
|
|
],
|
|
mode="subscription" if is_subscription else "payment",
|
|
success_url=req.success_url or "https://rugmunch.io/credits?success=true",
|
|
cancel_url=req.cancel_url or "https://rugmunch.io/credits?canceled=true",
|
|
metadata={"api_key": req.api_key, "package": req.package or req.tier},
|
|
customer_email=req.email or None,
|
|
)
|
|
|
|
return {
|
|
"session_id": checkout_session.id,
|
|
"checkout_url": checkout_session.url,
|
|
"publishable_key": STRIPE_PUBLISHABLE_KEY,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Stripe checkout error: {e}")
|
|
return JSONResponse(status_code=500, content={"error": str(e)[:200]})
|
|
|
|
|
|
@router.post("/stripe/webhook")
|
|
async def stripe_webhook(request: Request):
|
|
"""Stripe webhook — called when payment succeeds."""
|
|
payload = await request.body()
|
|
sig_header = request.headers.get("stripe-signature", "")
|
|
|
|
if not STRIPE_ENABLED:
|
|
logger.warning("Stripe webhook received but Stripe not configured")
|
|
return {"received": True, "processed": False}
|
|
|
|
try:
|
|
import stripe
|
|
stripe.api_key = STRIPE_SECRET_KEY
|
|
|
|
if STRIPE_WEBHOOK_SECRET:
|
|
event = stripe.Webhook.construct_event(payload, sig_header, STRIPE_WEBHOOK_SECRET)
|
|
else:
|
|
event = json.loads(payload)
|
|
|
|
if event.get("type") == "checkout.session.completed":
|
|
session = event["data"]["object"]
|
|
api_key = session.get("metadata", {}).get("api_key", "")
|
|
package_name = session.get("metadata", {}).get("package", "")
|
|
|
|
if package_name in CREDIT_PACKAGES:
|
|
credits = CREDIT_PACKAGES[package_name]["credits"]
|
|
elif package_name in SUBSCRIPTION_TIERS:
|
|
credits = SUBSCRIPTION_TIERS[package_name]["credits_per_month"]
|
|
else:
|
|
credits = 0
|
|
|
|
if api_key and credits > 0:
|
|
_credit_store.add_credits(api_key, credits)
|
|
logger.info(f"Stripe payment: {session.get('id','')} -> {credits} credits for {api_key[:16]}")
|
|
|
|
return {"received": True, "processed": True}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Stripe webhook error: {e}")
|
|
return JSONResponse(status_code=400, content={"error": str(e)[:200]})
|
|
|
|
|
|
@router.get("/stripe/packages")
|
|
async def list_packages():
|
|
"""List available credit packages and subscription tiers."""
|
|
return {
|
|
"packages": {k: {"price_usd": v["price_usd"], "credits": v["credits"], "label": v["label"]} for k, v in CREDIT_PACKAGES.items()},
|
|
"subscriptions": {k: {"price_usd": v["price_usd"], "credits_per_month": v["credits_per_month"], "label": v["label"]} for k, v in SUBSCRIPTION_TIERS.items()},
|
|
"stripe_configured": STRIPE_ENABLED,
|
|
"publishable_key": STRIPE_PUBLISHABLE_KEY or "" (set STRIPE_PUBLISHABLE_KEY in environment),
|
|
"note": "Pay with credit card. No crypto wallet needed. Credits never expire.",
|
|
}
|
|
|
|
|
|
@router.get("/credits/balance")
|
|
async def check_balance(api_key: str = ""):
|
|
"""Check remaining credit balance for an API key."""
|
|
if not api_key:
|
|
return {"error": "api_key parameter required"}
|
|
spending = _credit_store.spending(api_key)
|
|
return {
|
|
"api_key": api_key[:16] + "...",
|
|
"balance": spending["balance"],
|
|
"total_purchased": spending["total_purchased"],
|
|
"free_tier": spending["total_purchased"] == 0,
|
|
}
|
|
|
|
|
|
@router.post("/credits/buy")
|
|
async def buy_credits(api_key: str = "", amount_usd: int = 1000):
|
|
"""Buy credits directly via API (wraps Stripe checkout).
|
|
|
|
In production, this creates a Stripe checkout session.
|
|
For testing, credits are added directly when STRIPE_ENABLED is False.
|
|
"""
|
|
if not api_key:
|
|
raise HTTPException(status_code=400, detail="api_key required")
|
|
|
|
if STRIPE_ENABLED:
|
|
import stripe
|
|
stripe.api_key = STRIPE_SECRET_KEY
|
|
session = stripe.checkout.Session.create(
|
|
line_items=[{
|
|
"price_data": {
|
|
"currency": "usd",
|
|
"product_data": {"name": "API Credits", "description": f"{amount_usd} credits"},
|
|
"unit_amount": amount_usd,
|
|
},
|
|
"quantity": 1,
|
|
}],
|
|
mode="payment",
|
|
metadata={"api_key": api_key, "manual_amount": str(amount_usd)},
|
|
)
|
|
return {"checkout_url": session.url, "session_id": session.id}
|
|
|
|
# Sandbox mode: credit directly
|
|
_credit_store.add_credits(api_key, amount_usd)
|
|
balance = _credit_store.get_balance(api_key)
|
|
return {
|
|
"credits_added": amount_usd,
|
|
"total_balance": balance,
|
|
"price_per_call": "$0.01",
|
|
"mode": "sandbox",
|
|
"note": "Stripe not configured — credits added in sandbox mode. Add STRIPE_SECRET_KEY for live payments.",
|
|
}
|
|
|
|
|
|
@router.get("/stripe/portal")
|
|
async def customer_portal(customer_id: str = ""):
|
|
"""Stripe customer portal for managing subscriptions."""
|
|
if not STRIPE_ENABLED or not customer_id:
|
|
return {
|
|
"available": STRIPE_ENABLED,
|
|
"note": "Manage your subscription at rugmunch.io/account",
|
|
}
|
|
try:
|
|
import stripe
|
|
stripe.api_key = STRIPE_SECRET_KEY
|
|
session = stripe.billing_portal.Session.create(customer=customer_id)
|
|
return {"portal_url": session.url}
|
|
except Exception as e:
|
|
return JSONResponse(status_code=500, content={"error": str(e)[:200]})
|