AI-powered content moderation API built with FastAPI and DistilBERT. Sell API access to multiple companies — each gets isolated API keys, usage tracking, and flexible billing (token-based or monthly subscription).
content_moderation/
├── api/
│ ├── main.py # FastAPI app entry point, custom /docs (ReDoc)
│ ├── db.py # PostgreSQL async database layer
│ ├── models.py # Pydantic request/response schemas
│ ├── middleware/
│ │ └── auth.py # API key auth + quota enforcement
│ └── routes/
│ ├── health.py # GET / (landing page) and /health
│ ├── admin.py # Company registration, login, key management, billing
│ ├── moderate.py # POST /moderate and /moderate/batch
│ └── superadmin.py # Superadmin panel + contact form
├── src/
│ ├── filter.py # Rule-based banned/political word filter
│ ├── moderate.py # AI toxicity classification (DistilBERT)
│ └── train_transformer.py # Model training script (class-weighted)
├── data/
│ ├── banned_words.txt # Banned words list (English + Hindi)
│ ├── political_words.txt # Political words list (global + India)
│ └── training_data.csv # ~26,600 labeled examples (3 public datasets)
├── models/
│ └── transformer_model/ # Fine-tuned DistilBERT model files
├── static/
│ ├── index.html # Landing page
│ ├── dashboard.html # User dashboard panel
│ └── superadmin.html # Superadmin panel
├── build_dataset.py # Script to rebuild training_data.csv from HF datasets
├── .env # Environment variables
└── requirements.txt # Python dependencies
- Python 3.14+
- PostgreSQL 17
- Git
git clone <repository-url>
cd content_moderationpy -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # macOS/Linuxpip install -r requirements.txtOpen pgAdmin or SQL Shell (psql) and run:
CREATE DATABASE content_moderation;Edit the .env file in the project root:
DATABASE_URL=postgresql+asyncpg://postgres:YOUR_PASSWORD@localhost:5432/content_moderation
# Optional: set a fixed superadmin key (only used on first DB init)
# If blank, a random key is auto-generated and printed to console on first startup
SUPERADMIN_API_KEY=
# Superadmin web panel login credentials
SUPERADMIN_USERNAME=admin
SUPERADMIN_PASSWORD=softbender@123If your password contains special characters (e.g.
@), URL-encode them:@→%40,#→%23,$→%24
python src/train_transformer.pyThis saves the fine-tuned model to models/transformer_model/.
The training dataset (
data/training_data.csv) contains ~26,600 labeled examples merged from 3 public Hugging Face datasets. To rebuild it from scratch, runpython build_dataset.py.
uvicorn api.main:app --host 0.0.0.0 --port 8000Do not use
--reloadon Python 3.14 — it causes a multiprocessing spawn crash.
Server is now running at: http://localhost:8000
Tables are created automatically on first startup — no migrations needed. On first run, the SoftBenderAI superadmin API key is printed to the console. Save it immediately.
A demo company is seeded automatically on first startup. Use these credentials to test without registering:
| Field | Value |
|---|---|
demo@example.com |
|
| Password | password |
| API Key | cm_demo_key_for_testing_only_1234 |
| Billing | Token-based, 15,000 free tokens |
Login via dashboard: http://localhost:8000/panel → enter email + password
Use API key directly:
curl -X POST http://localhost:8000/moderate \
-H "X-API-Key: cm_demo_key_for_testing_only_1234" \
-H "Content-Type: application/json" \
-d '{"text": "Hello world, this is a test!"}'SoftBenderAI is the built-in superadmin company, seeded automatically on first startup.
- No token limits, no daily limits — completely unrestricted
- Used for platform management
- API key is printed to console on first startup (or set via
SUPERADMIN_API_KEYin.env) - All
/superadmin/*endpoints require the SoftBenderAI API key
How to get the Superadmin key: Start the server and look for this in the console output:
============================================================ SoftBenderAI Superadmin API Key: cm_xxxxxxxxxxxxxxxxxxxx Save this key — it won't be shown again! ============================================================Or set
SUPERADMIN_API_KEY=cm_yourkeyin.envbefore first startup.
Browser-based UI — no curl needed.
| Panel | URL | Who |
|---|---|---|
| Landing Page | http://localhost:8000 | Public |
| User Dashboard | http://localhost:8000/panel | Any registered company |
| Superadmin Panel | http://localhost:8000/superadmin/panel | SoftBenderAI only |
| API Docs (ReDoc) | http://localhost:8000/docs | Anyone |
- Register — Create a new company account and get your API key
- Login — Enter your email + password to access the dashboard
- Overview — Token usage, request stats, API key list (paginated)
- API Keys — Generate new keys, revoke existing ones
- Billing — Buy token packs or subscribe to monthly plans
- Test API — Moderate text directly from the browser
Login with SUPERADMIN_USERNAME / SUPERADMIN_PASSWORD from .env (default: admin / softbender@123).
- Overview — Platform-wide stats, revenue, top consumers
- Companies — List, search, view details, activate/deactivate, delete
- Manage — Set billing plans, grant tokens, generate keys for any company
- Keys — Revoke any API key platform-wide
- Plans & Pricing — View all plan tiers and token rates
- Contacts — View contact form submissions with unread badge, mark as read
| Interface | URL |
|---|---|
| ReDoc Docs | http://localhost:8000/docs |
| Health Check | http://localhost:8000/health |
| OpenAPI JSON | http://localhost:8000/openapi.json |
All endpoints (except POST /admin/register, POST /admin/login, GET /, GET /health, POST /superadmin/contact) require an API key in the request header:
X-API-Key: cm_xxxxxxxxxxxxxxxx
Every company chooses a billing mode at registration. Both modes start with 15,000 free tokens.
Pay as you go. Buy token packs whenever you need more.
| Tier | Tokens | Price |
|---|---|---|
| Free (on signup) | 15,000 tokens | ₹0 |
| Paid pack | 15,000 tokens | ₹300/pack |
- 1 API request = 1 token
- Free tokens never expire
- Buy packs via
POST /admin/topup - When tokens run out →
402 Payment Required
Fixed daily limits, billed monthly.
| Plan | Daily Limit | Price/Month |
|---|---|---|
starter |
1,000 requests/day | ₹499 |
growth |
10,000 requests/day | ₹1,499 |
business |
50,000 requests/day | ₹3,999 |
enterprise |
Unlimited | ₹9,999 |
- Subscribe via
POST /admin/plan/subscribe - Plan valid for 30 days from subscription date
- Daily limit resets at midnight UTC
- When daily limit is hit →
402 Payment Required - If
billing_mode = monthlybut no plan subscribed → falls back to token quota
POST /admin/register
Content-Type: application/json
{
"name": "Acme Corp",
"email": "admin@acme.com",
"password": "yourpassword",
"billing_mode": "token"
}billing_mode: "token" (default) or "monthly"
Response:
{
"message": "Company registered successfully",
"company_id": 1,
"company_name": "Acme Corp",
"billing_mode": "token",
"api_key": "cm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"free_tokens": 15000,
"save_key": "Save this API key — it won't be shown again."
}Save the
api_keyimmediately — it is shown only once.
POST /admin/login
Content-Type: application/json
{
"email": "admin@acme.com",
"password": "yourpassword"
}Response:
{ "api_key": "cm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }Login generates a new API key each time (keys are stored as SHA-256 hashes — irreversible).
POST /moderate
X-API-Key: cm_xxxxxxxx
Content-Type: application/json
{
"text": "Your message here",
"threshold": 0.7
}Response:
{
"allowed": true,
"result": "approved",
"blocked_reason": null,
"confidence": 0.9821,
"response_ms": 43,
"free_tokens_remaining": 14957,
"paid_tokens_remaining": 0
}POST /moderate/batch
X-API-Key: cm_xxxxxxxx
Content-Type: application/json
{
"texts": ["Hello world", "I hate you", "Great product!"],
"threshold": 0.7
}Response:
{
"results": [
{"allowed": true, "result": "approved", "blocked_reason": null, "confidence": 0.98, "response_ms": 12},
{"allowed": false, "result": "blocked", "blocked_reason": "Toxic/impolite language", "confidence": 0.91, "response_ms": 15},
{"allowed": true, "result": "approved", "blocked_reason": null, "confidence": 0.96, "response_ms": 11}
],
"total": 3,
"blocked_count": 1,
"free_tokens_remaining": 14954,
"paid_tokens_remaining": 0
}POST /admin/topup
X-API-Key: cm_xxxxxxxx
Content-Type: application/json
{ "packs": 2 }POST /admin/plan/subscribe
X-API-Key: cm_xxxxxxxx
Content-Type: application/json
{ "plan": "growth" }POST /admin/keys/generate
X-API-Key: cm_xxxxxxxxDELETE /admin/keys/{key_prefix}
X-API-Key: cm_xxxxxxxxGET /admin/stats
X-API-Key: cm_xxxxxxxxAll /superadmin/* endpoints (except /superadmin/contact and /superadmin/login) require the SoftBenderAI API key.
| Method | Endpoint | Description |
|---|---|---|
POST |
/superadmin/login |
Login with username/password → returns API key |
GET |
/superadmin/overview |
Platform-wide stats, revenue, top consumers |
GET |
/superadmin/companies |
List all companies |
GET |
/superadmin/companies/{id} |
Full stats for a specific company |
POST |
/superadmin/companies/{id}/keys/generate |
Generate API key for any company |
POST |
/superadmin/set-plan |
Set billing mode / plan / grant tokens |
DELETE |
/superadmin/keys/{prefix} |
Revoke any API key |
PATCH |
/superadmin/companies/{id}/toggle?activate=bool |
Activate or deactivate a company |
DELETE |
/superadmin/companies/{id} |
Delete a company and all its data |
GET |
/superadmin/plans |
List all plans and token pricing |
POST |
/superadmin/contact |
Submit contact form (public, no auth) |
GET |
/superadmin/contacts |
List all contact form submissions |
PATCH |
/superadmin/contacts/{id}/read |
Mark a contact message as read |
Set plan example:
POST /superadmin/set-plan
{
"company_id": 5,
"billing_mode": "monthly",
"monthly_plan": "enterprise",
"extra_tokens": 50000
}{ "text": "some text", "threshold": 0.5 }Default is 0.7. Lower = stricter, higher = more lenient.
Edit data/banned_words.txt — one word/phrase per line. Supports English and Hindi. Leet-speak detection is built-in (e.g. f4ck → fuck).
Edit data/political_words.txt — one word/phrase per line. Covers global leaders, Indian political parties, and policy terms.
python build_dataset.pyDownloads and merges 3 public Hugging Face datasets (~26,600 examples):
cardiffnlp/tweet_evalhate — ~13k English tweetsucberkeley-dlab/measuring-hate-speech— ~6k commentsgoogle/civil_comments— ~8k comments
python src/train_transformer.pyUses class-weighted loss to handle imbalanced data (toxic ~27%, clean ~73%). Trains for 4 epochs with early stopping (patience=2).
# 1. Use demo account (no registration needed)
curl -X POST http://localhost:8000/moderate \
-H "X-API-Key: cm_demo_key_for_testing_only_1234" \
-H "Content-Type: application/json" \
-d '{"text": "Hello world"}'
# 2. Register a new company
curl -X POST http://localhost:8000/admin/register \
-H "Content-Type: application/json" \
-d '{"name": "Test Co", "email": "test@test.com", "password": "test123", "billing_mode": "token"}'
# 3. Login (get a new API key)
curl -X POST http://localhost:8000/admin/login \
-H "Content-Type: application/json" \
-d '{"email": "test@test.com", "password": "test123"}'
# 4. Batch moderate
curl -X POST http://localhost:8000/moderate/batch \
-H "X-API-Key: cm_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"texts": ["Hello!", "I hate you", "Great work"], "threshold": 0.7}'
# 5. Subscribe to monthly plan
curl -X POST http://localhost:8000/admin/plan/subscribe \
-H "X-API-Key: cm_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"plan": "growth"}'
# 6. Superadmin — list all companies
curl http://localhost:8000/superadmin/companies \
-H "X-API-Key: <softbenderai_key>"
# 7. Superadmin — grant enterprise plan to company 5
curl -X POST http://localhost:8000/superadmin/set-plan \
-H "X-API-Key: <softbenderai_key>" \
-H "Content-Type: application/json" \
-d '{"company_id": 5, "billing_mode": "monthly", "monthly_plan": "enterprise", "extra_tokens": 0}'Every request goes through a 3-layer pipeline:
- Empty check — blank text is blocked immediately
- Rule-based filter (
src/filter.py) — checks againstbanned_words.txtandpolitical_words.txtwith leet-speak normalization and substring scanning - AI classifier (
src/moderate.py) — fine-tuned DistilBERT model scores toxicity probability; blocked if abovethreshold(default 0.7)
| Property | Value |
|---|---|
| Base model | distilbert-base-uncased |
| Training examples | ~22,600 (train) + ~4,000 (test) |
| Toxic examples | ~7,200 |
| Clean examples | ~19,400 |
| Training epochs | 4 (early stopping patience=2) |
| Loss function | Cross-entropy with inverse-frequency class weights |
| Optimizer | AdamW, lr=2e-5, weight_decay=0.01 |
| Table | Key Columns |
|---|---|
companies |
id, name, email, password_hash, is_superadmin, is_active, billing_mode, monthly_plan, plan_expires_at, free_tokens_used, paid_tokens_purchased, total_tokens_used |
api_keys |
id, company_id, key_hash, key_prefix, is_active, requests_today, total_requests, last_used, last_reset_date |
usage_logs |
id, company_id, api_key_prefix, text_length, result, blocked_reason, response_ms, is_free |
billing |
id, company_id, billing_type, tokens_purchased, packs, plan_name, amount_inr |
contact_messages |
id, name, email, phone, subject, message, is_read |
All tables are created automatically on first startup via init_db(). Schema changes use ALTER TABLE ... ADD COLUMN IF NOT EXISTS migrations — no manual migration needed.
getaddrinfo failed on startup
→ PostgreSQL is not running. Start it from Windows Services or pgAdmin.
Invalid or inactive API key
→ Pass the key in X-API-Key header. Check that the company is active in the superadmin panel.
402 Token quota exhausted
→ Free 15,000 tokens used up. Buy more via POST /admin/topup or subscribe via POST /admin/plan/subscribe.
402 Daily limit reached
→ Monthly plan daily limit hit. Upgrade plan or wait for midnight UTC reset.
403 Superadmin access required
→ Calling a /superadmin/* endpoint without the SoftBenderAI API key.
CUDA out of memory
→ Reduce per_device_train_batch_size in src/train_transformer.py or run on CPU.
Server crashes with --reload
→ Known Python 3.14 + uvicorn issue. Run without --reload and restart manually after code changes.
SoftBenderAI key not saved
→ Drop the api_keys rows for admin@softbenderai.com in pgAdmin, restart the server — key will be regenerated and printed again.
Demo key not working
→ The demo account is seeded on first startup. If you started the server before this version, restart once — init_db() will seed it automatically (idempotent).