Turn a short brief into a polished 500‑word article in seconds, monetize it with Stripe, and launch a production‑ready service on a cheap VPS.
TL;DR
| ✅ Feature | 📦 Tech Stack |
|---|---|
API – /generate (title + keywords → 500‑word article) |
FastAPI, Groq LLM API |
| Payments – $29 PRO / $99 ENTERPRISE (monthly) | Stripe Checkout + Webhooks |
| Auth & Rate‑Limiting – 5 req/hr per Stripe customer | Stripe Customer ID, slowapi
|
| Deploy – VPS (port 8000) behind Nginx + HTTPS | Ubuntu 22.04, Nginx, Let’s Encrypt |
| Landing Page – HTML/CSS + demo video | Static files served by Nginx |
| Marketing – Dev.to post, Reddit threads, email welcome | Playwright automation, free SMTP (Mailgun) |
| Monitoring – Stripe, LLM cost, VPS load | Grafana + Prometheus (optional) |
You can copy‑paste the code snippets below, replace the placeholders, and have a live SaaS product before lunch.
1. Why an AI‑Generated Article Service?
Content marketing is still king, but writing SEO‑friendly copy is a bottleneck. With the rise of large language models (LLMs) like Groq’s Llama‑3‑8B‑Chat, you can generate high‑quality, keyword‑rich articles in under a minute. Packaging this capability as a subscription‑based SaaS gives you recurring revenue while solving a real pain point for bloggers, marketers, and startups.
Key benefits
- Speed: Turn a 5‑word brief into a ready‑to‑publish article in ~30 s.
- Consistency: Same tone, structure, and SEO optimization every time.
- Scalability: Groq’s pay‑as‑you‑go pricing lets you scale without over‑provisioning.
2. Core FastAPI App
2.1 Project Layout
ai‑article‑saas/
├─ app/
│ ├─ main.py # FastAPI entry point
│ ├─ schemas.py # Pydantic models
│ ├─ services/
│ │ └─ groq.py # Wrapper around Groq API
│ └─ stripe_webhook.py # Stripe webhook handler
├─ static/
│ └─ index.html # Landing page (served by Nginx)
├─ requirements.txt
└─ Dockerfile
2.2 requirements.txt
fastapi==0.110.0
uvicorn[standard]==0.27.0
httpx==0.27.0
python‑dotenv==1.0.1
stripe==9.9.0
slowapi==0.1.7 # rate‑limiting
pydantic==2.6.1
2.3 main.py
import os
from fastapi import FastAPI, HTTPException, Depends, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
import httpx
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from starlette.status import HTTP_429_TOO_MANY_REQUESTS
app = FastAPI()
limiter = Limiter(key_func=lambda request: request.state.customer_id)
app.state.limiter = limiter
app.add_exception_handler(HTTP_429_TOO_MANY_REQUESTS, _rate_limit_exceeded_handler)
# ---- Models -------------------------------------------------
class Brief(BaseModel):
title: str
keywords: str
class ArticleResponse(BaseModel):
article: str
# ---- Auth ---------------------------------------------------
bearer = HTTPBearer(auto_error=False)
async def get_customer_id(
credentials: HTTPAuthorizationCredentials = Depends(bearer),
request: Request = None,
):
"""
Stripe Customer ID is passed as a Bearer token.
In production you’d verify the token via Stripe’s OAuth or a JWT.
"""
if not credentials:
raise HTTPException(status_code=401, detail="Missing auth token")
request.state.customer_id = credentials.credentials
return credentials.credentials
# ---- LLM Wrapper ---------------------------------------------
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
GROQ_ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"
async def generate_article(brief: Brief) -> str:
system_prompt = (
"You are an expert copywriter. Write a 500‑word SEO‑optimized article "
"based on the given title and keywords. Use markdown headings, bullet points, "
"and a concluding paragraph."
)
user_prompt = f"Title: {brief.title}\nKeywords: {brief.keywords}"
payload = {
"model": "llama3-8b-8192",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"temperature": 0.7,
}
async with httpx.AsyncClient() as client:
r = await client.post(
GROQ_ENDPOINT,
json=payload,
headers={"Authorization": f"Bearer {GROQ_API_KEY}"},
timeout=30,
)
r.raise_for_status()
data = r.json()
return data["choices"][0]["message"]["content"]
# ---- Endpoints ------------------------------------------------
@app.post("/generate", response_model=ArticleResponse)
@limiter.limit("5/hour")
async def generate(
brief: Brief,
customer_id: str = Depends(get_customer_id),
):
article = await generate_article(brief)
return ArticleResponse(article=article)
# ---- Health ---------------------------------------------------
@app.get("/health")
async def health():
return {"status": "ok"}
Tip: The rate‑limit key is the Stripe Customer ID, ensuring each paying user gets a fair quota.
3. Stripe Checkout & Webhooks
3.1 Products in Stripe
| Plan | Price (monthly) | Stripe Product ID | Price ID |
|---|---|---|---|
| PRO | $29 | prod_PRO_XXXXX |
price_PRO_XXXXX |
| ENTERPRISE | $99 | prod_ENT_XXXXX |
price_ENT_XXXXX |
Create both products in the Stripe Dashboard and enable a 7‑day trial for the PRO plan (use Stripe’s built‑in trial days).
3.2 Checkout Session Helper
import stripe
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
def create_checkout_session(customer_id: str, price_id: str, success_url: str, cancel_url: str):
session = stripe.checkout.Session.create(
customer=customer_id,
payment_method_types=["card"],
line_items=[{
"price": price_id,
"quantity": 1,
}],
mode="subscription",
success_url=success_url,
cancel_url=cancel_url,
)
return session.url
Expose a tiny endpoint (e.g., /checkout?plan=pro) that redirects the user to the generated URL.
3.3 Webhook Handler (stripe_webhook.py)
python
from fastapi import APIRouter, Request, HTTPException
import stripe
import os
router = APIRouter()
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
endpoint_secret = os.getenv("STRIPE_WEBHOOK_SECRET")
@
Top comments (0)