How to Build a Moderation API Without Any Labelled Data
Use an LLM as a teacher to generate synthetic training data, then compile a moderation classifier that runs at sub-100ms — no labelled dataset required.
TL;DR
You don't need thousands of hand-labelled examples to build an accurate content moderation API. By using an LLM as a teacher to generate synthetic training data offline, you can train a compiled classifier that runs at 41ms p95 with 91.5% F1 — and the whole process takes hours, not months. Sparkient automates this entire pipeline: define your moderation policy, generate data, train, and deploy.
The Problem: No Data, No Model, No Time
You're building a platform with user-generated content. You know you need moderation. But you have a cold-start problem:
- No labelled data. Your platform is new, or you've never systematically tagged content.
- No ML team. You're a product engineer, not a data scientist.
- No time. You need moderation working this quarter, not next year.
The traditional ML path — collect data, hire annotators, iterate on labels, train a model — takes months and tens of thousands of dollars. Most teams skip straight to calling GPT-4o on every message, which works but costs ~$0.01 per call and adds 500-1500ms of latency. At 50,000 messages per day, that's ~$15,000/month and a noticeable delay on every submission.
You need a third option: generate the training data synthetically, train a fast classifier, and deploy it at production speeds.
The "LLM as Teacher" Approach
The core insight is simple: an LLM already knows how to moderate content. It's been trained on enough internet to understand sarcasm, coded language, harassment patterns, and edge cases. You just can't afford to call it on every message.
So instead of using the LLM in production, use it once to teach a smaller, faster model:
- Define your moderation policy — what gets approved, what needs review, what gets rejected
- Write rules for obvious cases — blocklists, rate limits, known patterns
- Use the LLM to generate synthetic examples — diverse, edge-case-heavy training data
- Train a classifier on that synthetic data — a small model that encodes the LLM's judgement
- Deploy the compiled model — sub-100ms decisions in production
The LLM is the teacher. The classifier is the student. Once the student graduates, the teacher goes home.
Step 1: Define Your Moderation Policy
Before generating any data, you need clarity on what you're moderating. This means defining:
- Decision options: What actions can the system take?
- Input schema: What data does the system see?
- Rules: What hard constraints always apply?
For a typical UGC platform:
moderation_config = {
"name": "content-moderation",
"description": "Moderate user-generated posts on a community platform",
"options": ["approve", "review", "reject"],
"input_schema": {
"type": "object",
"properties": {
"text": {"type": "string", "description": "The post content"},
"user_id": {"type": "string"},
"account_age_days": {"type": "integer"},
"previous_violations": {"type": "integer"}
},
"required": ["text"]
}
}The options matter more than you think. Three-way classification (approve/review/reject) is almost always better than binary (allow/block). The "review" bucket gives you a pressure valve for genuinely ambiguous content — and keeps your false positive rate low.
Step 2: Write Rules for Obvious Cases
Some moderation decisions don't need ML at all. A message containing a phone number in a dating app should be flagged. A user with 5 previous violations posting a link should be reviewed. These are business rules, not pattern recognition.
CEL (Common Expression Language) rules handle this layer:
// Auto-reject if user has too many violations
ctx.previous_violations >= 5 ? "reject" : null
// Auto-review new accounts posting links
ctx.account_age_days < 7 && ctx.text.contains("http") ? "review" : null
// Auto-approve very short, clean content from established users
ctx.account_age_days > 90 && size(ctx.text) < 50 ? "approve" : nullRules execute in under 1ms and catch the easy cases. The classifier handles everything else.
Step 3: Generate Synthetic Training Data
This is where the LLM earns its keep. Instead of hand-labelling thousands of examples, you ask the LLM to generate realistic content for each category.
The DIY approach — calling the LLM yourself:
import google.generativeai as genai
genai.configure(api_key="YOUR_GEMINI_KEY")
model = genai.GenerativeModel("gemini-2.5-flash")
prompt = """Generate 50 realistic examples of user-generated content for
a community platform. For each example, provide the text and the correct
moderation decision (approve, review, or reject).
Guidelines:
- "approve": Normal discussion, questions, opinions, humor that isn't harmful
- "review": Borderline content — mild insults, possible spam, ambiguous intent
- "reject": Clear harassment, hate speech, explicit threats, obvious spam
Include edge cases: sarcasm, coded language, passionate-but-acceptable debate,
subtle manipulation, and context-dependent content.
Format as JSON array with "text" and "label" fields.
"""
response = model.generate_content(prompt)For a useful training set, you need roughly 200-500 examples per class. That means generating 600-1,500 total examples across your three categories.
What makes good synthetic data:
- Diversity: Multiple tones, lengths, topics, and communication styles
- Edge cases: Content that sits on the boundary between categories
- Class balance: Roughly equal numbers per category (you can oversample rare classes)
- Realism: Content that sounds like real users, not textbook examples
The automated approach — Sparkient handles all of this. You define the decision type, and the pipeline generates, labels, augments for rare classes, and validates the data automatically:
import httpx
# Create the decision type
response = httpx.post(
"https://api.sparkient.ai/api/v1/decision-types",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"name": "content-moderation",
"description": "Moderate user-generated posts on a community platform",
"options": ["approve", "review", "reject"],
"input_schema": {
"type": "object",
"properties": {
"text": {"type": "string"},
"user_id": {"type": "string"},
"account_age_days": {"type": "integer"},
"previous_violations": {"type": "integer"}
},
"required": ["text"]
}
}
)
decision_type_id = response.json()["id"]Step 4: Train the Classifier
With synthetic data in hand, you train a fast classifier. The model architecture matters: you need something that handles text well but runs in milliseconds, not seconds.
The DIY approach — manual pipeline with scikit-learn:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import json
# Load your synthetic data
with open("synthetic_data.json") as f:
data = json.load(f)
texts = [d["text"] for d in data]
labels = [d["label"] for d in data]
# Simple TF-IDF + gradient boosting
vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1, 2))
X = vectorizer.fit_transform(texts)
X_train, X_test, y_train, y_test = train_test_split(X, labels, test_size=0.2)
clf = GradientBoostingClassifier(n_estimators=200)
clf.fit(X_train, y_train)
print(classification_report(y_test, y_train))This gets you a baseline, but TF-IDF misses semantic meaning. "You're so smart" (genuine) and "You're so smart" (sarcastic) look identical.
What Sparkient does differently: The training pipeline uses a semantic text encoder to create high-dimensional embeddings, then feeds those into a gradient-boosted classifier with automated hyperparameter tuning. This captures meaning, not just word frequency. The result: 91.5% F1 on content moderation benchmarks.
Trigger training with a single API call:
response = httpx.post(
f"https://api.sparkient.ai/api/v1/decision-types/{decision_type_id}/train",
headers={"Authorization": "Bearer YOUR_API_KEY"}
)
# Training runs asynchronously — check status via the training endpointStep 5: Deploy and Call
Once training completes, the model is compiled to ONNX and deployed automatically. You call it like any API:
response = httpx.post(
"https://api.sparkient.ai/api/v1/decide",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"decision_type_id": "content-moderation",
"input": {
"text": "Check out this amazing product! Click here for 90% off!!!",
"user_id": "user_456",
"account_age_days": 2,
"previous_violations": 0
}
}
)
result = response.json()
# {
# "decision": "review",
# "confidence": 0.87,
# "latency_ms": 38,
# "stage": "classifier"
# }The response tells you exactly what happened:
- decision: The moderation verdict
- confidence: How sure the model is (0-1)
- latency_ms: Time to decide (typically under 100ms)
- stage: Which pipeline stage made the call (rules, classifier, or escalation)
What About Accuracy?
The natural concern: "Synthetic data can't be as good as real data, right?"
It depends on the task. For content moderation, Sparkient's benchmarks show:
| Metric | Compiled Model | Rules Only | Direct LLM (GPT-4o) | |--------|---------------|------------|---------------------| | F1 Score | 91.5% | ~50-55% | ~93% | | Accuracy | 91-96% | ~45-60% | ~94% | | p95 Latency | 41ms | <1ms | ~623ms | | Cost at 50K/day | $199/mo (Starter) | $0 | ~$15,000/mo |
The compiled model gets you within a few percentage points of the LLM's accuracy at a fraction of the cost and latency. And the "review" bucket catches most of the cases where the model is unsure — those go to human moderators or an LLM escalation.
Improving Over Time
Synthetic data gets you from zero to production. Real data makes you better. As your moderation system runs:
- Log decisions — Sparkient stores every decision with confidence scores
- Review low-confidence calls — Content in the "review" bucket gives you labelled data for free
- Retrain periodically — Add real examples to your training set and retrain
- Tighten rules — As you spot patterns, add CEL rules for instant handling
Each retrain improves accuracy because you're supplementing synthetic data with real-world examples from your actual user base.
FAQ
How many synthetic examples do I need? Aim for 200-500 per class. For a three-class moderation system, that's 600-1,500 total. More helps, but returns diminish after ~1,000 per class. Sparkient's generator handles this automatically and includes gap analysis to oversample underrepresented patterns.
What if my moderation policy is unusual? That's actually where this approach shines. Generic moderation APIs enforce someone else's policy. With the teacher approach, you describe your specific policy — maybe you allow strong language but not personal attacks, or you need to catch financial spam specifically. The LLM generates data matching your rules.
Can I add my own examples alongside synthetic ones? Yes. The Sparkient API lets you upload custom training examples that get mixed with generated data. If you have even 50 real examples of tricky edge cases, adding them significantly improves accuracy in those areas.
What happens when the model isn't confident? Decisions below the confidence threshold (configurable, typically 0.7) escalate to the LLM for a real-time call. This means you get LLM-quality decisions on hard cases without paying for LLM calls on every message. In practice, only 5-15% of content typically escalates.
You don't need labelled data to build accurate content moderation. You need a clear policy and a good teacher.
Start with the free tier — 5,000 credits, no credit card required. Define your moderation policy, generate synthetic data, and have a working API in an afternoon.
Ready to get started?
Start with 5,000 free credits and 250 decisions. No credit card required.
Start Free