How to Build a Moderation API Without Any Labelled Data
Use an LLM as a teacher to generate candidate training data, then evaluate a moderation classifier without requiring a historical customer dataset.
TL;DR
You do not need to start with thousands of hand-labelled examples to evaluate a content moderation classifier. An LLM can generate synthetic examples offline; Sparkient's public content-moderation run measured 0.900 macro F1, 91.5% accuracy, and a 41ms batch-average time per item. Define the policy, generate or add data, train, evaluate, and deploy only if it clears the project's safety thresholds.
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.
A traditional ML path can require collection, annotation, training, deployment, and monitoring. A live LLM avoids some setup but adds model latency, token usage, and a provider dependency to every request. Measure those costs on the actual project rather than assuming a universal timeline or bill.
One option to evaluate is synthetic data generation followed by a task-specific classifier. It still needs representative held-out testing before production use.
The "LLM as Teacher" Approach
An LLM can propose labelled examples for a defined moderation policy, but its labels are not ground truth. Measure the teacher against accepted examples and review consequential classes before using its output for training.
So instead of using the LLM on every production request, use it offline to help teach a smaller 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 reviewed data — a task-specific model that learns the accepted labels
- Evaluate the compiled model — measure held-out quality and full-path latency before production
The LLM is the teacher and the classifier is the student. Optional cloud escalation can still call a live model for configured low-confidence cases.
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"]
}
}A three-way policy (approve/review/reject) can be more useful than binary allow/block when the operation has a real human-review path. The review bucket creates a pressure valve for ambiguous content, but its threshold and review capacity still need evaluation.
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 usually execute in under 1ms and handle deterministic cases. The classifier handles cases that do not match a rule.
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)Sparkient defaults to a total augmentation target of number of options × 300. The appropriate size is task-specific; use a fixed held-out set to decide whether more examples help.
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 managed approach — define the decision type, request candidate examples in supported batches, review them, and check training readiness:
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"]
# Generate candidate examples in batches of at most 50.
examples = []
for _ in range(6):
response = httpx.post(
f"https://api.sparkient.ai/api/v1/decision-types/{decision_type_id}/examples/generate",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"count": 50},
)
response.raise_for_status()
examples.extend(response.json())
readiness = httpx.get(
f"https://api.sparkient.ai/api/v1/decision-types/{decision_type_id}/training-readiness",
headers={"Authorization": "Bearer YOUR_API_KEY"},
).json()
if not readiness["ready"]:
raise RuntimeError(readiness["issues"])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)
y_pred = clf.predict(X_test)
print(classification_report(y_test, y_pred))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 semantic features beyond word frequency. In the published synthetic content-moderation run, the result was 0.900 macro F1 and 91.5% accuracy.
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
With auto_deploy enabled, a completed policy deploys when any configured quality gate is met. Check the training result and your separate held-out evaluation before calling it from a consequential path:
response = httpx.post(
"https://api.sparkient.ai/api/v1/decide",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"decision_type": "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: Measured end-to-end decision time; the compiled path targets under 100ms, while escalated requests are model dependent
- 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 | Published synthetic run | What to validate | |--------|-------------------------|------------------| | Macro F1 | 0.900 | Per-class quality on representative held-out content | | Accuracy | 91.5% | Error cost for the target moderation policy | | Batch-average time per item | 41ms | End-to-end p50, p95, and p99 under target load |
This run is synthetic technical evidence. Its traditional baselines used structured fields while Sparkient also used a text encoder, so it is not a matched text-model comparison. The runner timed one batch and divided by item count rather than measuring per-request p95. It does not prove parity with a live LLM or a universal saving. Route uncertain or high-risk cases according to a policy you test explicitly.
Improving Over Time
Synthetic data can get you to an evaluation candidate. Representative reviewed data can improve the evidence for production use. As your moderation system runs:
- Log decisions — Sparkient stores every decision with confidence scores
- Review low-confidence calls — Human review can turn selected outcomes into corrected examples
- Retrain periodically — Add real examples to your training set and retrain
- Tighten rules — As you spot patterns, add CEL rules for instant handling
Retraining can improve accuracy when the reviewed examples are representative and correctly labelled, but improvement is not automatic; compare every candidate model on a fixed evaluation set before deployment.
FAQ
How many synthetic examples do I need? Start with the configured default, inspect per-class coverage and errors, and increase the target only when the evaluation supports it. Sparkient's gap analysis can generate examples for underrepresented patterns, but more synthetic data is not guaranteed to improve the model.
What if my moderation policy is unusual? That is a reason to run a policy-specific evaluation. Describe the policy—for example, allowing strong language while rejecting personal attacks—and inspect whether the generated examples and held-out results represent it correctly.
Can I add my own examples alongside synthetic ones? Yes. The Sparkient API lets you upload reviewed examples alongside generated data. Add representative difficult cases, retrain, and use the fixed evaluation set to determine whether quality actually improves.
What happens when the model isn't confident? If optional escalation is enabled, decisions below the configured confidence threshold can call the LLM in real time. Measure combined quality, escalation rate, latency, and credits on representative content instead of assuming a standard percentage.
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 the policy, train a candidate, and compare it with representative held-out content.
Ready to get started?
Start with 5,000 free credits and 250 decisions. No credit card required.
Start Free